states_rebuilder 1.11.2

  • Readme
  • Changelog
  • Example
  • Installing
  • 96

states_rebuilder #

pub package

A Flutter state management combined with dependency injection solution that allows :

  • a 100% separation of User Interface (UI) representation from your logic classes
  • an easy control on how your widgets rebuild to reflect the actual state of your application. Model classes are simple vanilla dart classes without any need for inheritance, notification, streams or annotation and code generation.

states_rebuilder is built on the observer pattern for state management and on the service locator pattern for dependency injection.

Intent of observer pattern
Define a one-to-many dependency between objects so that when one object changes state (observable object), all its dependents (observer objects) are notified and updated automatically.

Intent of service locator pattern
The purpose of the Service Locator pattern is to return the service instances on demand. This is useful for decoupling service consumers from concrete classes. It uses a central container which on request returns the request instance.

states_rebuilder combines the observer and service locator patterns to get what is called explicit reactivity and implicit reactivity.

Contrary to what one might think, implicit reactivity is simpler, more efficient and more powerful than explicit reactivity.

List of article about states_rebuilder

Tutorials

1- Explicit reactivity #

In the context of observer pattern, any class that extends StatesRebuilder is the observable and StateBuilder as well as StateWithMixinBuilder widgets are the observers. StatesRebuilder notifies the observers using rebuildStates method. Observer widgets when notified rebuild themselves to reproduce the actual state.

This an example of a simple counter app without any dependency injection solution. The model is instantiated globally:

NB: model, BloC or service are used interchangeably. Thy refer to any class that hold the business logic of the app.

import 'package:flutter/material.dart';
import 'package:`states_rebuilder`/`states_rebuilder`.dart';

//The observable class
class Counter extends StatesRebuilder{
  int count = 0;
  increment() {
    count++;
    rebuildStates();
  }
}

//Global instantiation
final Counter counterModel = Counter();

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(),
        body: HomePage(),
        floatingActionButton: FloatingActionButton(
          child: Icon(Icons.add),
          onPressed: () => counterModel.increment(),
        ),
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: 
      //The observer widget
      StateBuilder(
        //Registering the observer widget in the counterModel observable class.
        models: [counterModel],
        builder: (context, _) {
          return Text('${counterModel.count}');
        },
      ),
    );
  }
}

Regardless of the effectiveness of the state management solution, it must rely on a reliable dependency injection system.

states_rebuilder uses the service locator pattern, but using it in a way that makes it aware of the widget's lifecycle. This means that models are registered when needed in the initState method of aStatefulWidget and are unregistered when they are no longer needed in the dispose method. Models once registered are available throughout the widget tree as long as the StatefulWidget that registered them is not disposed. The StatefulWidget used to register and unregister models is the Injector widget.

To consume a registered model, you use the static method :

final T model = Injector.get<T>()

//If the model is registered with custom name :
final T model = Injector.get<T>(name:'customName');

The example above becomes:

//The observable class
class Counter extends StatesRebuilder {
  int count = 0;
  increment() {
    count++;
    //notifying the observers
    rebuildStates();
  }
}

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Injector(
      //Register (inject) the model using the 'Inject' class.
      inject: [Inject(() => Counter())],
      builder: (context) {
        //Getting the registered singleton of the 'Counter' type.
        final counterModel = Injector.get<Counter>();
        return MaterialApp(
          home: Scaffold(
            appBar: AppBar(),
            body: HomePage(),
            floatingActionButton: FloatingActionButton(
              child: Icon(Icons.add),
              onPressed: () => counterModel.increment(),
            ),
          ),
        );
      },
    );
  }
}

class HomePage extends StatelessWidget {
  //Getting the registered singleton outside the build method.
  //This is possible because the context is not used to get registered models.
  //If the type can be inferred by dart, you can omit the generic type.
  final Counter counterModel = Injector.get();

  @override
  Widget build(BuildContext context) {
    return Center(
      child: StateBuilder(
        models: [counterModel],
        builder: (context, _) {
          return Text('${counterModel.count}');
        },
      ),
    );
  }
}

With Injector you can inject multiple dependent or independent models (BloCs, Services) at the same time. Also you can inject stream and future.

Injector( 
  inject: [
    //The order is not mandatory even for dependent models.
    Inject<ModelA>(() => ModelA()),
    Inject(() => ModelB()),//Generic type in inferred.
    Inject(() => ModelC(Injector.get<ModelA>())),// Directly inject ModelA in ModelC constructor
    Inject(() => ModelC(Injector.get())),// Type in inferred.
    Inject<IModelD>(() => ModelD()),// Register with Interface type.
    //You can inject streams and future and make them accessible to all the widget tree.
    Inject<bool>.future(() => Future(), initialValue:0),// Register a future.
    Inject<int>.stream(() => Stream()),// Register a stream.
    Inject(() => ModelD(),name:"customName"), // Use custom name
  ],
  .
  .
);

Models are registered lazily by default. That is, they will not be instantiated until they are first used. To instantiate a particular model at the time of registration, you can set the isLazy variable of the class Inject to false.

In addition to its injection responsibility, the Injector widget gives you a convenient facade to manage the life cycle of the widget as well as the application:

Injector( 
  initState: (){
    // Function to execute in initState of the state.
  },
  dispose: (){
    // Function to execute in dispose of the state.
  },
  afterInitialBuild: (BuildContext context){
    // Called after the widget is first inserted in the widget tree.
  },
  appLifeCycle: (AppLifecycleState state){
    /*
    Function to track app life cycle state. It takes as parameter the AppLifeCycleState
    In Android (onCreate, onPause, ...) and in IOS (didFinishLaunchingWithOptions, 
    applicationWillEnterForeground ..)
    */
  },
  // If true all model will be disposed when the widget is removed from the widget tree
  disposeModels: true,
  .
  .
);

For more information on the Dependency Injection capabilities of the Injector see the dependency injection.

The Injector.get method searches for the registered singleton using the service locator pattern. For this reason, BuildContext is not required. The BuildContext is optional and it is useful if you want to subscribe the widget that has the BuildContext to the obtained model.

In the HomePage class of the example, we can remove StateBuilder and use the BuildContext to subscribe the the widget.

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // The BuildContext of this widget is subscribed to the Counter class.
    // Whenever the Counter class issues a notification, this widget will be rebuilt.
    final Counter counterModel = Injector.get(context: context);
    return Center(
      child: Text('${counterModel.count}'),
    );
  }
}

Once the context is provided, states_rebuilder searches up in the widget tree to find the nearest Injector widget that has registered an Inject of the type provided and register the context (Inject class is associated with InheritedWidget). So be careful in case the InheritedWidget is not available, especially after navigation.

To deal with such situation, you can remove the context parameter and use the StateBuilder widget, or in case you want to keep using the context you can use the reinject parameter of the Injector.

Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => Injector(
      //reinject an already injected model
      reinject: [counterModel],
      builder: (context) {
        return PageTwo();
      },
    ),
  ),
);

states_rebuilder uses the observer pattern. Notification can be filtered so that only widgets meeting the filtering criteria will be notified to rebuild. Filtration is done through tags. StateBuilder can register with one or more tags and StatesRebuilder can notify the observer widgets with a specific list of tags, so that only widgets registered with at least one of these tags will rebuild.

class Counter extends StatesRebuilder {
  int count = 0;
  increment() {
    count++;
    //notifying the observers with 'Tag1'
    rebuildStates(['Tag1']);
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final Counter counterModel = Injector.get(context: context);
    return Column(
      children: <Widget>[
        StateBuilder( // This StateBuilder will be notified
          models: [counterModel],
          tag: ['tag1, tag2'],
          builder: (_, __) => Text('${counterModel.count}'),
        ),
        StateBuilder(
          models: [counterModel],
          tag: 'tag2',
          builder: (_, __) => Text('${counterModel.count}'),
        ),
        StateBuilder(
          models: [counterModel],
          tag: MyEnumeration.tag1,// You can use enumeration
          builder: (_, __) => Text('${counterModel.count}'),
        )
      ],
    );
  }
}

2- Implicit reactivity #

What we have seen so far is called explicit reactivity, because our model must extend to StatesRebuilder and explicitly notify the observers using the rebuildStates methods.

With states_rebuilder, you can use pure vanilla dart classes for your logic, and states_rebuilder implicitly adds reactivity to your models so widgets can subscribe to your models and models can notify observer widgets.

This is the same example rewritten with implicit reactivity.

//Pure dart class. No inheritance, no notification, no streams, and no code generation
class Counter {
  int count = 0;
  increment() => count++;
}

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Injector(
      inject: [Inject<Counter>(() => Counter())],
      builder: (context) {
        //Use of 'getAsReactive' to get the model.
        //the suffix RM in counterModel means Reactive model.
        final ReactiveModel<Counter> counterModelRM = Injector.getAsReactive<Counter>();
        return MaterialApp(
          home: Scaffold(
            appBar: AppBar(),
            body: HomePage(),
            floatingActionButton: FloatingActionButton(
              child: Icon(Icons.add),
              //To mutate the state, use `setState` method.
              //setState notifies observers after state mutation.
              onPressed: () => counterModelRM.setState((state) => state.increment()),
            ),
          ),
        );
      },
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final ReactiveModel<Counter> counterModelRM = Injector.getAsReactive<Counter>(context: context);
    return Center(
      //use the `state` getter to get the model state.
      child: Text("${counterModelRM.state.count}"),
    );
  }
}

Compared to the case of explicit reactivity, states_rebuilder uses the same concepts for implicit reactivity, with the following differences:

  • The model is a pure vanilla dart class, regardless of the complexity of the class, even though there are many asynchronous methods with error handling.
  • The injected models are consumed using getAsReactive method instead of get in the explicit case.
ReactiveModel<T> modelRM = Injector.getAsReactive<T>()

The returned type is ReactiveModel<T>. The method getAsReactive returns the registered singleton of the model wrapped with reactive environnement:

Injector.get<T>() returns the raw singleton of type T of the registered model.
Injector.getAsReactive<T>() returns the reactive singleton of type ReactiveModel<T> of the registered model.

The reactive environment adds the following getters and methods:

The getters are : 

  • state: returns the registered raw singleton of the model.
  • connectionState : It is of type ConnectionState (a Flutter defined enumeration). It takes three values:
    1- ConnectionState.none: Before executing any method of the model.
    2- ConnectionState.waiting: While waiting for the end of an asynchronous task.
    3- ConnectionState.done: After running a synchronous method or the end of a pending asynchronous task.
  • isIdle : It's of bool type. it is true if connectionState is ConnectionState.none
  • isWaiting : It's of bool type. it is true if connectionState is ConnectionState.waiting
  • hasError: It's of bool type. it is true if the asynchronous task ends with an error.
  • error: Is of type dynamic. It holds the thrown error.
  • hasData: It is of type bool. It is true if the connectionState is done without any error.

The fields are:

  • customStateStatus: It is of type dynamic. It holds your custom-defined state status. For example, in a timer app, you can define custom states such as 'plying', 'paused, 'finished'.
  • joinSingletonToNewData : It is of type dynamic. It holds data sent from new reactive instance to the reactive singleton.
  • subscription : it is of type StreamSubscription<T>. It is not null if you inject streams using Inject.stream constructor. It is used to control the injected stream.

The methods are:

  • setState(T state): return a Future<void>. It takes the state as a parameter that corresponds to the singleton instance of the injected model. It is used to mutate the state and notify listeners after state mutation.
  • whenConnectionState Exhaustively switch over all the possible statuses of [connectionState]. Used mostly to return [Widget]s. It has four required parameters (onIdle, onWaiting, onData and onError).

setState is used whenever you want to trigger an event or an action that will mutate the state of the model and ends by issuing a notification to the observers.

reactiveModel.setState(
  (state) => state.increment(),
  filterTags: ['Tag1', Enumeration.Tag2],
  //set to true, you want to catch error, and not break the app.
  catchError: true 
  watch: (Counter counter) {
    //Specify the parts of the state to be monitored so that the notification is not sent unless this part changes
    return counter.count; //if count value is not changed, no notification will be emitted.
  },
  onSetState: (BuildContext context) {
    /* 
    Side effects to be executed after sending notification and before rebuilding the observers. Side effects are navigating, opening the drawer, showing snackBar , ..

    You can use another nested setState here.
    */
  },
  onRebuildState: (BuildContext context) {
    //The same as in onSetState but called after the end rebuild process.
  },

  onData: (BuildContext context, T model){
    //Callback to be execute if the reactive model has data.
  }

  onError: (BuildContext context, dynamic error){
    //Callback to be execute if the reactive model throws an error.
    //You do not have to set the parameter catchError to true. By defining onError parameter 
    //states_rebuilder catches the error by default.
  }
  //When a notification is issued, whether to notify all reactive instances of the model
  notifyAllReactiveInstances: true, 
  /*
  If defined, when a new reactive instance issues a notification, it will change the state of the reactive singleton in two ways.
  1- JoinSingleton.withNewReactiveInstances
  2- JoinSingleton.withCombinedReactiveInstances
  */
  joinSingletonWith: JoinSingleton.withNewReactiveInstance,

  //message to be sent to the reactive singleton
  dynamic joinSingletonToNewData,
),

It is important to understand that states_rebuilder caches two singletons.

  • The raw singleton of the registered model, obtained using Injector.get method.
  • The reactive singleton of the registered model (the raw model decorated with reactive environnement), obtained using Injector.getAsReactive.

With states_rebuilder, you can create, at any time, a new reactive instance, which is the same raw cashed singleton but decorated with a new reactive environment.

To create a new reactive instance of an injected model use:
1- Injector.getAsReactive with the parameter asNewReactiveInstance equals true

// get a new reactive instance
ReactiveModel<T> modelRM2 = Injector.getAsReactive<T>(asNewReactiveInstance: true);

2- StateBuilder with generic type and without models property.

StateBuilder<T>(
  builder:(BuildContext context, ReactiveModel<T> newReactiveModel){
    return YourWidget();
  }
)

Important notes about reactive singleton and new reactive instances:

  • The reactive singleton and all the new reactive instances share the same raw singleton of the model, but each one decorates it with a different environment.
  • Unlike the reactive singleton, new reactive instances are not cached so they are not accessible outside the widget in which they were instantiated.
  • To make new reactive instances accessible throughout the widget tree, you have to register it with the Injector with a custom name:
final modelRM =Injector.getAsReactive<Counter>(asNewReactiveInstance: true);

return Injector(
inject: [
  Inject( () => modelRM, name: 'newModel1'),
],
)
// Or
Injector(
inject: [
  Inject<Counter>(() => Counter()),
  Inject(
    () => Injector.getAsReactive<Counter>(asNewReactiveInstance: true),
    name: Enum.newModel1,
  ),
],

At later time if you want to consume the injected new reactive instance you use:

// get the injected new reactive instance
ReactiveModel<T> modelRM2 = Injector.getAsReactive<T>(name : 'newModel1');
//Or
ReactiveModel<T> modelRM2 = Injector.getAsReactive<T>(name : Enum.newModel1);
  • You can not get a new reactive model by using getAsReactive(context: context) with a defined context. It will throw because only the reactive singleton that can subscribe a widget using the context.

  • With the exception of the raw singleton they share, the reactive singleton and the new reactive instances have an independent reactive environment. That is, when a particular reactive instance issues a notification with an error or with ConnectionState.awaiting, it will not affect other reactive environments.

  • states_rebuilder allows reactive instances to share their notification or state with the reactive singleton. This can be done by:
    1- notifyAllReactiveInstances parameter of setState method. If true, each time a notification is issued by the reactive instance in which setState is called, all other reactive instances are notified.
    2- joinSingletonWith parameter of setState method or Inject class. This time, new reactive instances, when issuing a notification, can clone their state to the reactive singleton.

    • If joinSingletonWith is set toJoinSingleton.withNewReactiveInstance, this means that the reactive singleton will have the state of the new reactive instance issuing the notification.
    • If joinSingletonWith is set to JoinSingleton.withCombinedReactiveInstances, this means that the singleton will hold a combined state of all the new reactive instances.
      The combined state priority logic is:
      Priority 1- The combined ReactiveModel.hasError is true if at least one of the new instances has error
      Priority 2- The combined ReactiveModel.connectionState is awaiting if at least one of the new instances is awaiting.
      Priority 3- The combined ReactiveModel.connectionState is 'none' if at least one of the new instances is 'none'.
      Priority 4- The combined ReactiveModel.hasDate is true if it has no error, it isn't awaiting and it is not in 'none' state.
  • joinSingletonWith can be defined in setState method or in Inject class constructor. When set in the setState method, it means that only the new instance, wheresetState is called, is joined to the reactive singleton. Whereas if joinSingletonWith is defined in theInject constructor, this means that all new reactive instances will be joined to the reactive singleton.

  • New reactive instances can send data to the reactive singleton. joinSingletonToNewData parameter of reactive environment hold the sending message.

StateBuilder #

In addition to its state management responsibility, StateBuilder offers a facade that facilitates the management of the widgets lifecycle.

StateBuilder<T>(
  onSetState: (BuildContext context, ReactiveModel<T> model){
  /*
  Side effects to be executed after sending notification and before rebuilding the observers. Side effects are navigating, opening the drawer, showing snackBar,...  
  
  It is similar to 'onSetState' parameter of the 'setState' method. The difference is that the `onSetState` of 'setState' method is called once after executing the 'setState'. But this 'onSetState' is executed each time a notification is send from one of the observable model this 'StateBuilder' is subscribing.
  
  You can use another nested setState here.
  */
  },
  onRebuildState: (BuildContext context, ReactiveModel<T> model){
  // The same as in onSetState but called after the end rebuild process.
  },
  initState: (BuildContext context, ReactiveModel<T> model){
  // Function to execute in initState of the state.
  },
  dispose: (BuildContext context, ReactiveModel<T> model){
  // Function to execute in dispose of the state.
  },
  didChangeDependencies: (BuildContext context, ReactiveModel<T> model){
  // Function to be executed  when a dependency of state changes.
  },
  didUpdateWidget: (BuildContext context, ReactiveModel<T> model, StateBuilder oldWidget){
  // Called whenever the widget configuration changes.
  },
  afterInitialBuild: (BuildContext context, ReactiveModel<T> model){
  // Called after the widget is first inserted in the widget tree.
  },
  afterRebuild: (BuildContext context, ReactiveModel<T> model){
  /*
  Called after each rebuild of the widget.

  The difference between onRebuildState and afterRebuild is that the latter is called each time the widget rebuilds, regardless of the origin of the rebuild. 
  Whereas onRebuildState is called only after rebuilds after notifications from the models to which the widget is subscribed.
  */
  },
  // If true all model will be disposed when the widget is removed from the widget tree
  disposeModels: true,

  // A list of observable objects to which this widget will subscribe.
  models: [model1, model2]

  // Tag to be used to filer notification from observable classes.
  // It can be any type of data, but when it is a List, 
  // this widget will be saved with many tags that are the items in the list.
  tag: dynamic

   watch: (ReactiveModel<T> model) {
    //Specify the parts of the state to be monitored so that the notification is not sent unless this part changes
  },

  builder: (BuildContext context, ReactiveModel<T> model){
    /// [BuildContext] can be used as the default tag of this widget.

    /// The model is the first instance (model1) in the list of the [models] parameter.
    /// If the parameter [models] is not provided then the model will be a new reactive instance.
  },
  builderWithChild: (BuildContext context, ReactiveModel<T> model, Widget child){
    ///Same as [builder], but can take a child widget containing the part of the widget tree that we do not want to rebuild.
    /// If both [builder] and [builderWithChild] are defined, it will throw.

  },

  //The child widget that is used in [builderWithChild].
  child: MyWidget(),

)

StateWithMixinBuilder #

StateWithMixinBuilder is similar to StateBuilder and extends it by adding mixin (practical case is animation),

StateWithMixinBuilder<T>( {
  Key key, 
  dynamic tag, // you define the tag of the state. This is the first way
  List<StatesRebuilder> models, // You give a list of the logic classes (BloC) you want this this widget to listen to.
  initState: (BuildContext, String,T) {
     // for code to be executed in the initState of a StatefulWidget
  },
  dispose (BuildContext, String,T) {
    // for code to be executed in the dispose of a StatefulWidget
  }, 
  didChangeDependencies: (BuildContext, String,T) {
    // for code to be executed in the didChangeDependencies of a StatefulWidget
  }, 
  didUpdateWidget: (BuildContext, String,StateBuilder, T){
    // for code to be executed in the didUpdateWidget of a StatefulWidget
  },
  didChangeAppLifecycleState: (String, AppLifecycleState){
    // for code to be executed depending on the life cycle of the app (in Android : onResume, onPause ...).
  },
  afterInitialBuild: (BuildContext, String,T){
    // for code to be executed after the widget is inserted in the widget tree.
  },
  afterRebuild: (BuildContext, String) {
    // for code to be executed after each rebuild of the widget.
  }, 
  @required MixinWith mixinWith
});

Available mixins are: singleTickerProviderStateMixin, tickerProviderStateMixin, AutomaticKeepAliveClientMixin and WidgetsBindingObserver.

List of article about states_rebuilder

Dependency Injection #

states_rebuilder uses the service locator pattern for injecting dependencies using theinjector with is a StatefulWidget. To understand the principle of DI, it is important to consider the following principles:

  1. Injector adds classes to the container of the service locator ininitState and deletes them in the dispose state. This means that if Injector is removed and re-inserted in the widget tree, a new singleton is registered for the injected models. If you injected streams or futures using Inject.stream or Inject.future and when a the Injector is disposed and re-inserted, the streams and futures are disposed and reinitialized by states_rebuilder and do not fear of any memory leakage.

  2. You can use nested injectors. As the Injector is a simple StatefulWidget, it can be added anywhere in the widget tree. A typical use is to insert the Injector deeper in the widget tree just before using the injected classes.

  3. Injected classes are registered lazily. This means that they are not instantiated after injection until they are consumed for the first time using Injector.get orInjector.getAsReactive.

  4. For each injected class, you can consume the registered instance using Injector.get or the reactive model wrapper of the injected instance usingInjector.getAsReactive. As the raw instance and the reactive instance are registered lazily, if you consume a class using only Injector.get and notInjector.getAsReactive, the reactive instance will never be instantiated.

  5. You can register classes with concrete types or abstract classes.

That said:

It is possible to register a class as a singleton, as a lazy singleton or as a factory simply by choosing where to insert it in the widget tree.

  • To save a singleton that will be available for all applications, insert the Injector widget in the top widget tree. It is possible to set the isLazy parameter to false to instantiate the injected class the time of injection.

  • To save a singleton that will be used by a branch of the widget tree, insert the Injector widget just above the branch. Each time you get into the branch, a singleton is registered and when you get out of it, the singleton will be destroyed. Making profit of the behavior, you can clean injected models by defining a dispose() method inside them and set the parameter disposeModels of the Injectorto true.

  • With Injector, you can inject futures and use whenConnectionState to display useful information to the user and finally you can get the registered raw singleton using Injector.get method anywhere in the app. This is useful for instantiating plug-ins such as SharedPreferences. So you do not have to make the main function async and wait before calling runApp and use WidgetsFlutterBinding.ensureInitialized(). For example, you can show a splash screen informing the user that something is instantiating and display a helping error message if a plug-in fails to initialize. example :

void main() {
  runApp(
    //global injection
    Injector(
      inject: [
        Inject.future(() => SharedPreferences.getInstance(),
        isLazy: false,
        ),
      ],
      builder: (context) {
        return MyApp();
      },
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Getting sharedPreferences Reactive Model
    final sharedPreferencesRM =
        Injector.getAsReactive<SharedPreferences>(context: context);
    return sharedPreferencesRM.whenConnectionState(
      onIdle: () => Text('onIdle'),
      onWaiting: () => SplashScreen(),
      onError: (error) => MyErrorWidget(error),
      onData: (_) {
        //sharedPreferences instance is available and can be used any where in the app
        //You can consume it using simply Injector.get<SharedPreferences>().

        return Injector(
          inject: [Inject(() => MyClass(sharedPreferences: Injector.get()))],
          builder: (_) {
            
            ....
          },
        );
      },
    );
  }
}

1.11.2 (2020-01-10) #

  • Add the static method StatesRebuilderDebug.printInjectedModel() to debugPrint all registered model in the service locator.
  • Add the static method StatesRebuilderDebug.printObservers(observable) to )debugPrint all subscribed observers to the provided observable.
  • Refactor watch logic to work with asynchronous tasks as well as with List, Map, Set types.

1.11.1 (2020-01-05) #

  • Add onData(BuildContext, T) parameter to setState method. It is a shortcut to:
    onSetState(context){
      if(reactiveModel.hasData){
        .....
      }
    }
    

1.11.0 (2020-01-05) #

  • Inject.get for injected streams and future will no longer throw, it will return the the current value.
  • If whenConnectionState is defined, catchError is set true automatically.
  • Add watch parameter to StateBuilder widget and. watch allows to link the rebuild process to the variation of a set of variables.(Experimental feature).
  • Remove deprecated getAsModel and hasState.
  • Update docs and add Dependency Injection section in the readme file.

1.10.0 (2019-12-30) #

  • Add whenConnectionState method to the ReactiveModel. IT exhaustively switch over all the possible statuses of connectionState. Used mostly to return a Widget. (Pul request of ResoCoder).

1.9.0 (2019-12-28) #

  • Add assertion error helpful messages.
  • Add isIdle getter to the ReactiveModel as a shortcut to : connectionState == ConnectionState.none
  • Add isWaiting getter to the ReactiveModel as a shortcut to : connectionState == ConnectionState.waiting
  • Add onError(BuildContext, dynamic) parameter to setState method.
  • Add joinSingletonToNewData parameter to setState method.
  • Refactor codes to remove bugs and to use Flutter 1.12 version.

1.8.0 (2019-12-06) #

1- Add the following features: (See readme fille).

  • onSetState and onRebuildState parameters to the StateBuilder.
  • The BuildContext is the default tag of StateBuilder.
  • JoinSingleton, inheritedInject, initialCustomStateStatus parameters to Inject
  • reinject and getAsReactive to Injector. 2- Remove the following parameters:(Breaking changes)
  • tagID parameter from StateBuilder. before
StateBuilder(
  builder: (BuildContext context, String tagID){
    // code
  }
)

after

StateBuilder<T>(
  models: [firstModel, secondModel],
  builder: (BuildContext context, ReactiveModel<T> model){
    /// No more need for the `tagID` because the `context` is used as `tagID`.
    /// the model is the first instance (firstModel) in the list of the [models] parameter.
    /// If the parameter [models] is not provided then the model will be a new reactive instance
    /// See readme file for more information
  }
)
  • The model parameter of the Injector.builder method. before
Injector(
  builder: (BuildContext context, T model){
    // code
  }
)

after

Injector(
  builder: (BuildContext context){
    // no need for model parameter. It has less boilerplate.
  }
)
  • Injector.getAsModel, StateBuilder.viewModel and StatesRebuilder.hasState are deprecated, and replaced by Injector.getAsReactive, StateBuilder.models and StatesRebuilder.hasObservers respectively.

1.7.0 (2019-11-14) #

1- Add onSetState parameter to the setState method to define a callback to be executed after state mutation. The callBack takes the context so you can push/pop routes, show dialogs or snackBar. (see example folder).

2- Add catchError parameter to the setState method to define whether to catch error while mutining the state or not.(see example folder). If an error is thrown, hasError getter is true and the error can be obtained via the error getter (see point 5 below).

3- Add the getter connectionState to the ModelStatesRebuilder<T>to get the asynchronous status of the state. it can be ConnectionState.none before executing the Future, ConnectionState.waiting while waiting for the Future and ConnectionState.done after resolving the Future.

4- Add the field stateStatus to the ModelStatesRebuilder<T> class. It allows defining a custom status of the state other than those defined by the connectionState getter.

5- add the getter hasError, hasData and error to the ModelStatesRebuilder<T> class.

6- Change the name blocs to models.

7- Refactor the code and fix bugs.

8- Update docs and examples.

1.6.1 (2019-10-22) #

  • Add watch parameter to setState method and Inject.stream constructor. watch allows to link the rebuild process to the variation of a set of variables.
  • Update docs

1.6.0+1 (2019-10-18) #

  • Add Injector.getAsModel method. When called with the context parameter, the calling widget is automatically registered as a listener.
  • Add setState(Function(state)) to mutate the state and update the dependent the views from the UI.
  • Model class have not to extend StatesRebuilder to get reactivity.
  • Add the named constructorInject.future which take a future and update dependents when future completes.
  • Add the named constructorInject.stream which take a steam and update dependents when stream emits a value.
  • Injector.get or Injector.getAsModel now throws if no registered model is found. This can be silent by setting the parameter silent to true
  • Injected model ara lazily instantiated. To do otherwise set the parameter isLazy of the Inject widget to false.

1.5.1 (2019-09-14) #

  • add afterInitialBuild and afterRebuild parameters to the StateBuilder, StateWithMixinBuilder and Injector widgets.afterInitialBuildandafterRebuild` are callBack to be executed after the widget is mounted and after each rebuild.

1.5.0+1 (2019-09-12) #

  • Use ObservableService and hasState instead of Observable and hasObserver, because the latters are widely used and can lead to conflict

1.5.0 (2019-09-06) #

  • Add hasStates getter to check if the StatesRebuilder object has listener.
  • Add inject parameter to the Injector widget as an alternative to the models parameter. With inject you can register models using interface Type.
  • Add observable interface. Any service class can implement it to notify any ViewModel to rebuild its corresponding view.
  • Refactor the library to make it design patterns wise and hence make it testable.
  • Test the library

1.3.2 (2019-06-24) #

  • Add appLifeCycle argument to Injector to track the life cycle of the app.
  • Refactor the code.

1.3.1 (2019-06-13) #

  • remove rebuildFromStreams.
  • Initial release of Streaming class
  • The builder closure of the Injector takes (BuildContext context, T model) where T is the generic type.
  • Fix typos

1.3.0 (2019-06-04) #

  • Initial release of rebuildFromStreams method.
  • Initial release of Injector for Dependency Injection.
  • deprecate blocs parameter and use viewModels instead
  • StateBuilder can have many tags.

1.2.0 (2019-05-23) #

  • Remove stateID and replace it by tag parameter. tag is optional and many widgets can have the same tag.
  • rebuildStates() when called without parameters, it rebuilds all widgets that are wrapped with StateBuilder and StateWithMixinBuilder.
  • Each StateBuilder has an automatically generated cached address. It is stored in the second parameter of the builder, initState, dispose, and other closures. You can call it inside the closures to rebuild that particular widget.
  • add StateWithMixinBuilder widget to account for some of the most used mixins.
  • Optimize the code and improve performance

1.1.0 (2019-05-13) #

  • Add withTickerProvider parameter to StateBuilder widget.

1.0.0 (2019-05-12) #

  • Add BlocProviderto provide your BloCs.
  • You can use enums to name your StateBuilder widgets.
  • rebuildStates now has only one positioned parameter of List
  • If rebuildStates is given without parameter, it will rebuild all widgets that have stateID.
  • improve performance.

0.1.4 #

  • improve performance

0.1.3 #

  • Add getter and setter for the stateMap.

0.1.2 #

  • Remove print statements

0.1.1 #

  • Change readme.md of the example

0.1.0 #

  • Initial version

example/README.md

1- Simple Counter App #

Simple counter with showing the Snackbar when the value of the counter reaches 10. This example shows the use of:

  • the getter state
  • The method setState
  • The parameter onSetState
import 'package:flutter/material.dart';
import 'package:states_rebuilder/states_rebuilder.dart';

//Pure dart class. No inheritance, no notification, no streams, and no code generation
class Counter {
  int count = 0;
  increment() => count++;
}
void main() => runApp(MaterialApp(home: App()));

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Injector(
      inject: [Inject<Counter>(() => Counter())],
      builder: (context) {
        final counter = Injector.getAsReactive<Counter>();
        return Scaffold(
          appBar: AppBar(
            title: Text(" Counter App"),
          ),
          body: MyHome(),
          floatingActionButton: FloatingActionButton(
            child: Icon(Icons.add),
            onPressed: () => counter.setState((state) => state.increment(),
            //with osSetState you can define a callback to be executed after mutating the state.
              onSetState: (context) {
                if (counter.state.count >= 10) {
                  Scaffold.of(context).showSnackBar(
                    SnackBar(
                      content: Text("You have reached 10 taps"),
                    ),
                  );
                }
              },
            ),
          ),
        );
      },
    );
  }
}

class MyHome extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counter = Injector.getAsReactive<Counter>(context: context);
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text("You have pushed this many times"),
          Text("${counter.state.count}"),
        ],
      ),
    );
  }
}

2- Counter App with Future #

asynchronous counter with showing CircularProgressIndicator while waiting for the Future to resolve. This example shows the use of:

  • the getter connectionState. It take the following values:
    ConnectionState.none : before executing async task.
    ConnectionState.waiting : while executing async task.
    ConnectionState.done : when async task resolves.
    Listener are notified to rebuild after each change of connectionState.
import 'package:flutter/material.dart';
import 'package:states_rebuilder/states_rebuilder.dart';

class Counter {
  int count = 0;
  increment() async {
    await Future.delayed(Duration(seconds: 1));
    count++;
  }
}
void main() => runApp(MaterialApp(home: App()));

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Injector(
      inject: [Inject<Counter>(() => Counter())],
      builder: (context) {
        final counter = Injector.getAsReactive<Counter>();
        return Scaffold(
          appBar: AppBar(
            title: Text(" Counter App"),
          ),
          body: MyHome(),
          floatingActionButton: FloatingActionButton(
            child: Icon(Icons.add),
            onPressed: () => counter.setState((state) => state.increment()),
          ),
        );
      },
    );
  }
}

class MyHome extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counter = Injector.getAsReactive<Counter>(context: context);
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text("You have pushed this many times"),
          //counter has connectionState getter.
          //`counter.connectionState` equals:
          // ConnectionState.none    : before executing async task.
          // ConnectionState.waiting : while executing async task.
          // ConnectionState.done    : when async task resolves.
          counter.connectionState == ConnectionState.waiting
              ? CircularProgressIndicator()
              : Text(counter.state.count.toString()),
        ],
      ),
    );
  }
}

3- Counter app : catching errors (show AlertDialog) #

asynchronous counter with possibility of throwing an error. An alert dialog is shown with the error message. This example shows the use of:

  • The parameter onSetState
  • The getter hasError
  • The getter error
import 'dart:math';

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

class Counter {
  int _count = 0;
  int get count => _count;
  increment() async {
    //Simulating async task
    await Future.delayed(Duration(seconds: 1));
    //Simulating error (50% chance of error);
    bool isError = Random().nextBool();

    if (isError) {
      throw Exception("A fake network Error");
    }
    _count++;
  }
}
void main() => runApp(MaterialApp(home: App()));

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Injector(
      inject: [Inject<Counter>(() => Counter())],
      builder: (context) {
        final counterModel = Injector.getAsReactive<Counter>();
        return Scaffold(
          appBar: AppBar(
            title: Text(" Counter App"),
          ),
          body: MyHome(),
          floatingActionButton: FloatingActionButton(
            child: Icon(Icons.add),
            onPressed: () => counterModel.setState(
              (state) => state.increment(),
              catchError: true, //catch the error
              onSetState: (context) {
                // onSetState will be executed after mutating the state.
                if (counterModel.hasError) {
                  showDialog(
                    context: context,
                    child: AlertDialog(
                      title: Text("Error!"),
                      content: Text("${counterModel.error}"),
                    ),
                  );
                }
              },
            ),
          ),
        );
      },
    );
  }
}

class MyHome extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counterModel = Injector.getAsReactive<Counter>(context: context);
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Builder(
            builder: (context) {
              if (counterModel.isWaiting) {
                return CircularProgressIndicator();
              }

              return Text(
                "${counterModel.state.count}",
                style: TextStyle(fontSize: 50),
              );
            },
          ),
        ],
      ),
    );
  }
}

4- Counter app : watching variable #

Simple counter with watching the change of the count variable. This example shows the use of:

  • The parameter watch
import 'dart:math';

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

Color _color() {
  return Color.fromRGBO(
      Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);
}

class Counter {
  int _count1 = 0;
  int get count1 => _count1 <= 5 ? _count1 : 5;
  increment() {
    _count1++;
  }
}
void main() => runApp(MaterialApp(home: App()));

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Injector(
      inject: [Inject<Counter>(() => Counter())],
      builder: (context) {
        final counter = Injector.getAsReactive<Counter>();
        return Scaffold(
          appBar: AppBar(
            title: Text(" Counter App"),
          ),
          body: MyHome(),
        );
      },
    );
  }
}

class MyHome extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counter = Injector.getAsReactive<Counter>(context: context);
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Container(
            width: double.infinity,
            height: 50,
            color: _color(),
            child: Center(
                child: Text("Random Color. It changes with each rebuild")),
          ),
          Text(
            "${counter.state.count1}",
            style: TextStyle(fontSize: 50),
          ),
          if (counter.state.count1 > 4)
            Column(
              children: <Widget>[
                Text("your have reached the maximum"),
                Divider(),
                Text(
                    "If you tap on the left button, nothing changes, and the rebuild process is stopped because the counter value is not changed.\nIf you tap on the right button, the random color changes because the counter value is not watched."),
              ],
            ),
          Divider(),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: <Widget>[
              RaisedButton(
                child: Text("increment and watch"),
                onPressed: () => counter.setState((state) => state.increment(),
                //watch variable `count1`, if it changes update the UI, else do not update the UI.
                    watch: (state) => state.count1),
                // you can watch many variables : ` watch : (state) => [state.count1, state.count2]`
              ),
              RaisedButton(
                child: Text("increment without watch"),
                onPressed: () => counter.setState((state) => state.increment()),
              )
            ],
          )
        ],
      ),
    );
  }
}

5- Injecting Futures and Streams #

This example shows the use of:

  • The parameter Inject.stream and Inject.future
import 'package:flutter/material.dart';
import 'package:states_rebuilder/states_rebuilder.dart';
void main() => runApp(MaterialApp(home: App()));

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Injector(
      inject: [
        Inject.stream(
          () => Stream<int>.periodic(Duration(seconds: 1), (num) => num)
              .take(10),
        ),
        Inject<bool>.future(
            () => Future.delayed(Duration(seconds: 5), () => true),
            initialValue: false),
      ],
      builder: (context) {
        final futureSnap = Injector.getAsReactive<bool>(context: context).snapshot;
        return Scaffold(
          appBar: futureSnap.data == false
              ? AppBar(
                  title: Text(" awaiting a Future"),
                  backgroundColor: Colors.red,
                )
              : AppBar(
                  title: Text("Future is completed"),
                  backgroundColor: Colors.blue,
                ),
          body: MyHome(),
        );
      },
    );
  }
}

class MyHome extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final streamSnap = Injector.getAsReactive<int>(context: context).snapshot;
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text("Counter From stream"),
          streamSnap.hasData
              ? Text("${streamSnap.data}")
              : Text("Waiting for data ..."),
        ],
      ),
    );
  }
}

6- Creating new reactive instances and commination with reactive singleton #

This example shows the use of:

  • The parameters joinSingleton, JoinSingleton.withCombinedReactiveInstances, joinSingletonToNewData and notifyAllReactiveInstances.
  • Creating new reactive instances.
import 'package:flutter/material.dart';
import 'package:states_rebuilder/states_rebuilder.dart';

class CounterModel {
  int counter = 0;
  void increment() => counter++;
  Future<void> incrementAsync() async {
    await Future.delayed(Duration(seconds: 1));
    counter++;
  }
}

void main() => runApp(MaterialApp(home: App()));

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Injector(
        inject: [
          Inject<CounterModel>(
            () => CounterModel(),
            joinSingleton: JoinSingleton.withCombinedReactiveInstances,
          )
        ],
        builder: (_) => Scaffold(
          appBar: AppBar(
            title: Center(
              child: StateBuilder<CounterModel>(
                models: [Injector.getAsReactive<CounterModel>()],
                builder: (_, model) {
                  if (model.isWaiting) {
                    return CircularProgressIndicator(
                      backgroundColor: Colors.white,
                    );
                  }
                  return Padding(
                    padding: EdgeInsets.only(right: 10),
                    child: Text(
                      '${model.joinSingletonToNewData ?? "Tap on A Counter"}',
                      style: TextStyle(fontSize: 30),
                    ),
                  );
                },
              ),
            ),
          ),
          body: CounterGrid(),
        ),
      ),
    );
  }
}

class CounterGrid extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20),
      child: Column(
        children: <Widget>[
          Expanded(
            child: GridView.count(
              crossAxisCount: 3,
              children: <Widget>[
                for (var i = 0; i < 12; i++)
                  StateBuilder<CounterModel>(
                    builder: (context, model) {
                      return GridItem(
                        count: model.isWaiting
                            ? null
                            : model.state.counter,
                        onTap: () {
                          if (i % 2 == 0)
                            model.setState(
                              (state) => state.increment(),
                              notifyAllReactiveInstances: true,
                              joinSingletonToNewData : 'I am Counter ${i + 1} I hold ${model.state.counter}';
                            );
                          else
                            model.setState(
                              (state) => state.incrementAsync(),
                              onSetState: (context) {
                                model.joinSingletonToNewData =
                                    'I am Counter ${i + 1} I hold ${model.state.counter}';
                              },
                            );
                        },
                      );
                    },
                  ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class GridItem extends StatelessWidget {
  final int count;
  final Function onTap;
  GridItem({this.count, this.onTap});
  @override
  Widget build(BuildContext context) {
    return InkWell(
      child: Container(
        margin: EdgeInsets.all(5),
        decoration: BoxDecoration(
          color: Colors.lightBlue,
          border:
              Border.all(color: Theme.of(context).primaryColorDark, width: 4),
          borderRadius: BorderRadius.circular(6),
        ),
        child: Center(
          child: count != null
              ? Text(
                  "$count",
                  style: TextStyle(
                    color: Colors.white,
                    fontSize: 50,
                  ),
                )
              : CircularProgressIndicator(
                  backgroundColor: Colors.white,
                ),
        ),
      ),
      onTap: onTap,
    );
  }
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  states_rebuilder: ^1.11.2

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:states_rebuilder/states_rebuilder.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
92
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
96
Learn more about scoring.

We analyzed this package on Jan 24, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.7.0
  • pana: 0.13.4
  • Flutter: 1.12.13+hotfix.5

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.2.2 <3.0.0
flutter 0.0.0
Transitive dependencies
collection 1.14.11 1.14.12
meta 1.1.8
sky_engine 0.0.99
typed_data 1.1.6
vector_math 2.0.8
Dev dependencies
flutter_test