cubes 1.3.1 copy "cubes: ^1.3.1" to clipboard
cubes: ^1.3.1 copied to clipboard

outdated

Simple State Manager (Focusing on simplicity and rebuilding only the necessary)

pub package

Cubes

Cubes #

Simple State Manager with dependency injection and no code generation required.

About #

Manage the state of your Flutter application in a simple and objective way, rebuilding the widget tree only where necessary!

Cubes makes use of ChangeNotifier since it is a feature already available in Flutter and for its simplicity. Cubes doesn't rely on RxDart.

Install #

To use this plugin, add cubes as a dependency in your pubspec.yaml file.

Counter Example #


import 'package:cubes/cubes.dart';
import 'package:flutter/material.dart';

void main() {
 
  Cubes.registerDependency((i) => CounterCube());

  runApp(MaterialApp(
      title: 'Cube Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: CounterScreen(),
    ),
  );
}

class CounterCube extends Cube {

  final count = 0.obsValue;

  void increment() {
    count.modify((value) => value + 1); // or count.update(newValue);
  }
  
}

class CounterScreen extends CubeWidget<CounterCube> {
  @override
  Widget buildView(BuildContext context, CounterCube cube) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            SizedBox(height: 20),
            cube.count.build<int>((value) {
              return Text(value.toString());
            }),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: cube.increment,
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

Usage #

Creating a Cube #

Cube is the class responsible for handling the business logic of your view.

To create your Cube, just make a class that extends from Cube as follows:


class CounterCube extends Cube {
   
}

In Cubes, you control elements in the view using ObservableValues. Creating such variables is easy:


class CounterCube extends Cube {
    final count = 0.obsValue;
    // final myList = <MyModel>[].obsValue;
    // final viewModel = ViewMidel().obsValue;
}

You can modify these ObservableValues and then your view will react to these changes. For example:


class CounterCube extends Cube {
    final count = 0.obsValue;
    
    void increment() {
      count.modify((value) => value + 1); // or count.update(newValue);
    }
}

It's a common practice to query an API or do something else once the View is ready. In Cubes, this is super simple to achieve. Just override the method onReady and your code will be called once the View is ready.


class CounterCube extends Cube {
    final count = 0.obsValue;
    
    void increment() {
      count.modify((value) => value + 1); // or count.update(newValue);
    }
    
    @override
    void onReady(Object arguments) {
      // do anything when view is ready
      super.ready(arguments);
    }
}

The arguments property is taken from the view and, if ommited, it will be taken from ModalRoute.of(context).settings.arguments;

Creating a View #

Creating a widget that represents a View is very simple. Make a class that extends from CubeWidget<CubeName> passing the Cube name that this view will use. For example:


class CounterScreen extends CubeWidget<CounterCube> {
   
}

Your IDE will force you to implement a mandatory method called buildView, just like this:


class CounterScreen extends CubeWidget<CounterCube> {

 @override
  Widget buildView(BuildContext context, CounterCube cube) {
    // TODO: implement buildView
    throw UnimplementedError();
  }
   
}

This method is similar to the 'build' method from StatelessWidget and State. There you will return your widget tree and will have access to Cube for listening your ObservableValues.

The final result looks like this:


class CounterScreen extends CubeWidget<CounterCube> {

  @override
  Widget buildView(BuildContext context, CounterCube cube) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            cube.count.build<int>((value) {
              return Text(value.toString());
            }),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: cube.increment,
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

}

Note that listening to an ObservableValue is very simple:


   cube.count.build<int>((value) {
     return Text(value.toString());
   })

By listening to the ObservableValue count, every time this variable is changed the View is notified by running the following code again:

  return Text(value.toString());

This guarantees that only the necessary is rebuilt in the whole widget tree.

Registering Cubes and dependencies #

Did you notice that we never created an instance of CounterCube? This is because Cubes works with dependency injection. So for everything to work properly, we have to register the Cube used and its dependencies inside main().


import 'package:cubes/cubes.dart';
import 'package:flutter/material.dart';

void main() {
  // Register your Cube
  Cubes.registerDependency((i) => CounterCube());

  // Example: register a singleton Cube
  // Cubes.registerDependency(
  //    (i) => CounterCube(),
  //    type: DependencyRegisterType.singleton,
  // );

  // Example: register repositories or something else
  // Cubes.registerDependency((i) => SingletonRepository(i.getDependency());

  runApp(MaterialApp(
      title: 'Cubes Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Home(),
    ));
}


For those of you who don't like to depend your projects too much in a package, there are other ways to work with it:

  • You can use the CubeConsumer widget, see this example;
  • To work with StatefulWidget you can use the mixin CubeStateMixin<StatefulWidget,Cube>. See this example;
  • For a minimalist approach, you can use SimpleCube. See this example.

Listening observable variables #

You can listen to observables in two ways: using the extension build as mentioned earlier or using the CObserver widget:

Extension 'build' #

  cube.count.build<int>(
     (value) => Text(value.toString()),                              // Here you build the widget and it will be rebuilt every time the variable is modified and will leave the conditions of `when`.
     animate: true,                                                  // Setting to `true`, fadeIn animation will be performed between widget changes.
     when: (last, next) => last != next,                             // You can decide when rebuild widget using previous and next value. (For a good functioning of this feature use immutable variables)
     transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder,   // Here you can modify the default animation which is FadeIn.
     duration: Duration(milliseconds: 300),                          // Sets the duration of the animation.
  ),

Widget CObserver #

  return CObserver<int>(
      observable: cube.count,
      builder: (value)=> Text(value.toString()),
      when: (last, next) => true,
      animate:true,
      transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder,
      duration: Duration(milliseconds: 300),
  );

Provider #

To get the reference of a specific Cube from CubeConsumer or CubeWidget, you can use Cubes.of<MyCube>(context);

Methods: Inner Cube #

onAction #

onAction is used to send any type of action or message to a view. You simply create an 'action' extending from CubeAction.


  class NavigationAction extends CubeAction {
      final String route;
      NavigationAction({this.route});
  }

Then, inside your cube:


class MyCube extends Cube {

   void navToGome(){
      // sending action
     onAction(NavigationAction(route: "/home"));
   }
}

  

Finally, you will receive this action in the View through the method:


class MyScreen extends CubeWidget<MyCube> {

  @override
  Widget buildView(BuildContext context, MyCube cube) {
    return ...;
  }
  
   @override
   void onAction(BuildContext context, MyCube cube, CubeAction action) {
     if(action is NavigationAction) Navigator.pushNamed(context, (action as NavigationAction).route);
     super.onAction(context, cube, data);
   }

}

This approach will be useful for navigation, for complex animations among other features that the View may need to perform.

runDebounce #

This method will help you to debounce the execution of something.

   runDebounce(
     'increment', // identify
     ()  => print(count.value),
     duration: Duration(seconds: 1),
   );

listen #

Use it to listen ObservableValues.

  listen(count,(value){
      // do anything
  });

listenActions #

Use it to listen to the Action sent to the view.

  listenActions((action){
      // do anything
  });

Useful Widgets #

CAnimatedList #

This is a version of AnimatedList that simplifies its use for the Cube context.


  CAnimatedList<String>(
    observable: cube.todoList,
    itemBuilder: (context, item, animation, type) {
      return ScaleTransition(
        scale: animation,
        child: _buildItem(item),
      );
    },
  )

Full usage example here.

CFeedBackManager #

Use this widget if you want to reactively control your Dialog, BottomSheet and SnackBar using an ObservableValue.

Create the observable to control:


final bottomSheetControl = CFeedBackControl(data:'test').obsValue;
final dialogControl = CFeedBackControl(data:'test').obsValue;
final snackBarControl = CFeedBackControl<String>().obsValue;

Now just add the widget to your tree and its settings:


CFeedBackManager(
   dialogControllers:[  // You can add as many different dialogs as you like
       CDialogController<String>(
           observable: cube.dialogControl,
           // dismissible: bool,
           // barrierColor: Color,
           // routeSettings: RouteSettings,
           // useRootNavigator: bool,
           // useSafeArea: bool,
           builder: (data, context) {
               return Container(height: 200, child: Center(child: Text('Dialog: $data')));
           },
       ),
   ],
   bottomSheetControllers: [  // You can add as many different BottomSheets as you like
       CBottomSheetController<String>(
           observable: cube.bottomSheetControl,
           // dismissible: bool,
           // useRootNavigator: bool,
           // routeSettings: RouteSettings,
           // barrierColor: Color,
           // backgroundColor: Color,
           // elevation: double,
           // shape: ShapeBorder,
           // clipBehavior: Clip,
           // enableDrag: bool,
           // isScrollControlled: bool,
           // useSafeArea: bool,
           builder: (data, context) {
               return Container(height: 200, child: Center(child: Text('BottomSheet: $data')));
           },
       ),
   ],
   snackBarControllers: [
       CSnackBarController<String>(
           observable: cube.snackBarControl,
           builder: (data, context) {
               return SnackBar(content: Text(data));
           },
       ),
   ],
   child: ...
)

To show or hide:


    bottomSheetControl.show(); // or hide();
    dialogControl.show(); // or hide();
    snackBarControl.show(data: 'Success');

Full usage example here.

CTextFormField #

Widget created to use TextFormField with ObservableValue. With it you can work reactively with your TextFormField, being able to modify and read its value, set error, enable and disable it.


 /// code in Cube

 final textFieldControl = CTextFormFieldControl(text: '').obsValue;

 //  final text = textFieldControl.text; // get text
 //  textFieldControl.text = 'New text'; // change text
 //  textFieldControl.error = 'error example'; // set error
 //  textFieldControl.enable = true; // enable or disable
 //  textFieldControl.enableObscureText = true; // enable or disable obscureText

 // code in Widget

 CTextFormField(
   observable: cube.textFieldControl,
   obscureTextButtonConfiguration: CObscureTextButtonConfiguration(  // use to configure the hide and show content icon in case of obscureText = true.
     align: CObscureTextAlign.right,
     iconHide: Icon(Icons.visibility_off_outlined),
     iconShow: Icon(Icons.visibility_outlined),
   ),
   decoration: InputDecoration(hintText: 'Type something'),
   // ... All other TextFormField attributes
 ),

It is exactly the same as the conventional TextFormField with two more fields, the observable and obscureTextButtonConfiguration.

Full usage example here.

Internationalization support #

With Cubes you can configure internationalization in your application in a simple way using JSON files.

Using #

Create a folder named lang in the root folder of your project and put your files named by the language and locale, just like this:

Example json file;

Add the path in your pubspec.yaml:


  # To add assets to your application, add an assets section, like this:
  assets:
   - lang/

In your MaterialApp you can configure the CubesLocalizationDelegate:


    final cubeLocation = CubesLocalizationDelegate(
      [
        Locale('en', 'US'),
        Locale('pt', 'BR'),
      ],
    );

    @override
    Widget build(BuildContext context) {
      return MaterialApp(
        title: 'My app',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        localizationsDelegates: cubeLocation.delegates, // see here
        supportedLocales: cubeLocation.supportedLocations, // see here
        home: Home(),
      );
    }

All done! Your application already supports internationalization. Now you can retrieve the strings as follows:


  String text = Cubes.getString('welcome');
  // or
  String text = 'welcome'.tr();

You can replace parts of the string using params, just like this:


  // String in json file:
  // {'welcome':'Hello @name! Welcome to my app!'}

  String text = Cubes.getString('welcome',params:{'name':'Kevin'});
  // or
  String text = 'welcome'.tr(params:{'name':'Kevin'});

Custom dependency injection #

By default, Cubes uses get_it to manage dependencies. if you want to use another one, you can overwrite the Injector:


  class MyInjector extends CInjector {
   @override
    T getDependency<T>({String dependencyName}) {
      // your implementation
    }

    @override
    void registerDependency<T>(DependencyInjectorBuilder<T> builder, {String dependencyName, bool isSingleton = false}) {
      // your implementation
    }

    @override
    void reset() {
      // your implementation
    }
  }

  Cubes().injector = MyInjector();

Useful extensions #


    // BuildContextExtensions

    context.goTo(Widget());
    context.goToReplacement(Widget());
    context.goToAndRemoveUntil(Widget(),RoutePredicate);

    context.mediaQuery; // MediaQuery.of(context);
    context.padding; // MediaQuery.of(context).padding;
    context.viewInsets; // MediaQuery.of(context).viewInsets;

    context.sizeScreen; // MediaQuery.of(context).size;
    context.widthScreen; // MediaQuery.of(context).size.width;
    context.heightScreen; // MediaQuery.of(context).size.height;

    context.theme;
    context.scaffold;
    context.showSnackBar(SnackBar());
    context.arguments;
    context.getCube<MyCube>(); // get Cube from provided

Testing #


import 'package:flutter_test/flutter_test.dart';

void main() {
  CounterCube cube;
  setUp(() {
    cube = CounterCube();
  });

  tearDown(() {
    cube?.dispose();
  });
  test('initial value', () {
    expect(cube.count.value, 0);
  });

  test('increment value', () {
    cube.increment();
    expect(cube.count.value, 1);
  });

  test('increment value 3 times', () {
    cube.increment();
    cube.increment();
    cube.increment();
    expect(cube.count.value, 3);
  });
}

Example with asynchronous call here

Example widget test using Robot here

If there are still doubts, you should be able to find what you're looking for in the full example.

40
likes
0
pub points
61%
popularity

Publisher

unverified uploader

Simple State Manager (Focusing on simplicity and rebuilding only the necessary)

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, flutter_localizations, get_it

More

Packages that depend on cubes