Content:
Inject.previous
Constructor- Shortcuts; reducing boilerplate
- Like
FutureBuilder
andStreamBuilder
but more powerful - For performance consideration,
observe
andobserveMany
instead ofmodels
parameter - ReactiveModel keys
- Working with immutable state
With the named constructor Inject.previous
, you can inject and reinject objects by keeping track of their previous values before reinjection. Inject.previous
is very useful when combined with the reinjectOn
parameter.
Let's say we are in a shopping app, where a list of products will be displayed based on the authenticated user.
Our basic app will have an Auth
class and a Products
class. The Products
class depends on the Auth class.
To inject the Products
return Injector(
inject: [
//Injecting the Auth object
Inject(()=>Auth()),
Inject.previous(
(Products previous) => Products(
//used to fetch data in the Products object
token: Injector.get<Auth>().token,
// authenticated user ID
userId: Injector.get<Auth>().userId,
//List of all items we want to keep from the previous Products object
items: previous.items ?? [],
//We can establish more then one dependence
otherProperty: Injector.get<OtherModel>().otherProperty,
),
),
],
//Here the connection between Products and Auth objects is established
//Whenever authRM emits a notification, the injected Products instance
//is override by a new one as defined in the Inject.previous constructor.
reinjectOn: [RM.get<Auth>(), RM.get<OtherModel>()],
//shouldNotifyOnReinjectOn:true, //this is the default behavior
builder: (_) => ....
)
reinjectOn
takes a list ofReactiveModel
s, so theoretically you can linkProducts
to an infinite number of objects
By default the Products widget listeners will be notified when any of the models defined in the reinjectOn parameter issues a notification. To override this behavior set
shouldNotifyOnReinjectOn
parameter to false
As you know, for each injected model there are two lazily registered singletons: the pure object instance and the reactive one:
To consume the pure registered instance we use Injector.get<T>()
.
From this update on there is a shortcut to it: IN.get<T>()
; IN stands for Injector
For the ReactiveModel, we used to use:
Injector.getAsReactive<T>()
orReactiveModel<T>()
, to get the ReactiveModel instance of type TReactiveModel<T>.create(myModel)
to create a local ReactiveModelReactiveModel<T>.future(myFuture)
to create a local future ReactiveModel.ReactiveModel<T>.stream(myStream)
to create a local stream ReactiveModel.
Now as a Shortcuts:
RM.get<T>()
, to get the ReactiveModel instance of type T.RM.create<T>(myModel)
, to create a local ReactiveModel.RM.future<T>(myFuture)
, to create a local future ReactiveModel.RM.stream<T>(myStream)
, to create a local stream ReactiveModel.
To notify widget observers, we use setState method;
If you are to get the ReactiveModel and call setState only once, you can use the new shortcut:
RM.getSetState<T>((s)=>s.method())
this is a shortcut of :RM.get<T>().setState((s)=>s.method())
Fluter core API has FutureBuilder
and StreamBuilder
to handle futures and streams respectively.
class Foo {
Future<userID> login() async {
await api.login(token);
}
}
In the UI and after instantiating the Foo object using our dependence injection (Global instance, InheretedWidget, or Provider, get_it, ..), we use FutureBuilder
FutureBuilder(
future : foo.login();
builder : (context, snapshot){
if(snapshot.isWaiting){
return SplashScreen();
}
if(snapshot.hasError){
return Text('An Error has happened'),
}
if(snapshot.hasData){
return Text('${snapshot.data}');
}
}
)
In states_rebuilder world, after injection the Foo object using Injector, use can use one of the availbale wdiget:
StateBuilder
, the default widget listener;WhenRebuilder
, to exhaustively go throw all the available state status (onIdle, onWaiting, onError, onData);WhenRebuilderOr
, to selectively go throw any of the available state status:OnSetStateListener
to execute side effects.
WhenRebuilder<Foo>(
models : [ReactiveModel<Foo>()],
//we used the initState to trigger the future
iniState : (ctx, fooRM) => fooRM.setState((s)=>s.login()),
onIdle : ()=> Text('Welcoming Screen'),
onWaiting : ()=> SplashScreen(),
onError: (e)=> Text('An Error has happened'),
onDate : (fooRM){
return Text('${fooRM.state.userID}');
}
)
Or; by creating a future ReactiveModel form the login method
WhenRebuilder<Foo>(
models : [ReactiveModel.future<int>(Injector.get<Foo>().login())],
onIdle : ()=> Text('Welcoming Screen'),
onWaiting : ()=> SplashScreen(),
onError: (e)=> Text('An Error has happened'),
onData : (userIdRM){
return Text('${userIdRM.value}');
}
)
WhenRebuilder<Foo>(
//consider using observe instead of models see next section
models : [RM.future<int>(Injector.get<Foo>().login())],
onIdle : ()=> Text('Welcoming Screen'),
onWaiting : ()=> SplashScreen(),
onError: (e)=> Text('An Error has happened'),
onData : (userId){
return Text('${userIdRM}');
}
)
in RM.future<int>(Injector.get<Foo>().login())
the first generic type (Foo) defines the type of the injected model, and the second generic type (int) defined the resulting type of the value resolved by the future.
The same thing is obtained for stream using RM.getStream
WhenRebuilder<int>(
models : [RM.stream<int>(IN.get<Foo>().fireStoreStream())],
onIdle : ()=> Text('Welcoming Screen'),
onWaiting : ()=> SplashScreen(),
onError: (error)=> Text('An Error has happened'),
onData : (userId){
return Text('${userId}');
}
)
As I said you can use StateBuilder
, WhenRebuilderOr
, OnSetStateListener
.
Although Flutter is performant by default, build ()
can be called frequently during the life cycle of the widget. We must be prepared for unwanted rebuilds.
In the case of FutureBuilder
we read this caution in the Flutter docs:
The future must have been obtained earlier, ..... If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.
In many tutorials I see something like this :
FutureBuilder(
future : foo.asyncMethod();
builder : (context, snapshot){
}
)
This is not good, because each time the build process is invoked the async method is called and the builder of the FutureBuilder
is invoked.
With states_rebuilder the same unwanted behavior is expected:
StateBuilder(
models : [RM.getFuture<Foo,void>((f)=> f.asyncMethod())],
builder :(context, rm){
}
)
states_rebuilder is a little better, when a parent of StateBuilder
is rebuilding the builder of StateBuilder
will not be called, but still the async method will be called.
To remedy this, the models
parameter should be of void callback type:
StateBuilder(
models : [() => RM.getFuture<Foo,void>((f)=> f.asyncMethod())],
builder :(context, rm){
}
)
By simply adding () =>
we gain performance and the future is entirely controlled by the StateBuilder
widget and will never be called by an unwanted rebuilds.
In this context, I intended to add two parameters:
observe
which is of typeStatesRebuilder Function()
. to register to one observable model.observeMany
which is of typeList<StatesRebuilder Function()>
. to register to more than one observable model.
justification of the chosen names:
- As states_rebuilder use the observer pattern,
observe
is more descriptive thanmodels
. - both are verbs, because verbs are used for actions and callbacks are actions.
- In most cases one will subscribe to one observable model, so it is convenient to use
observe: ()=> ..
instead ofobserve: [()=> ...]
With this new parameters :
WhenRebuilder<Foo>(
observe : ()=> RM.getStream<Foo,int>((f)=> f.fireStoreStream()),
onIdle : ()=> Text('Welcoming Screen'),
onWaiting : ()=> SplashScreen(),
onError: (error)=> Text('An Error has happened'),
onData : (userId){
return Text('${userId}');
}
)
even with other types of ReactiveModel
, observe
is more efficient than models
:
WhenRebuilder<Foo>(
//old
//models : [RM.get<Foo>()], // this get will be called for each parent rebuild.
//new
observe : () => RM.get<Foo>(),//this get is called once at the time of creation
onIdle : ()=> Text('Welcoming Screen'),
onWaiting : ()=> SplashScreen(),
onError: (error)=> Text('An Error has happened'),
onData : (foo){
...
}
)
Think of the performance difference:
- with
models : [RM.get<Foo>()]
theget
method is called at the time of the creation and each time the parent widget rebuilds (hot restart, window sizing in web, routing, ...). If the invoked method does heavy calculation the performance loose is critical. - with
observe : ()=> RM.get<Foo>()
theget
method is called only once at the time of creation.
observe
and observeMany
are experimental, and the models
is maintained and may be deprecated in a future release.
states_rebuilder
is based on the concepts of ReactiveModel
.
A ReactiveModel
can be injected and used when needed anywhere in the widget tree.
To get an injected model we use:
final modelRM = Injector.getAsReactive<T>();
// or more concisely:
final modelRM = ReactiveModel<T>();
// or even more concisely (since this release):
final modelRM = RM.get<T>();
In another hand, ReactiveModel
can be created locally:
//creating a reactive model from a boolean value
final switchRM = ReactiveModel<bool>.create(true);
// or more concisely (since this release)
final switchRM = RM.create<bool>(true);
with
states_rebuilder
we can locally createReactiveModel
from primitive values, objects, futures or streams.
To consume the created ReactiveModel
, we use one of the available widget observers : StateBuilder
, WhenRebuilder
, WhenRebuilderOr
or OnSetStateListener
.
Let's display a Flutter Switch button:
Scaffold(
appBar: AppBar(
title: StateBuilder<bool>(
observe: () => RM.create(true),
builder: (ctx, switchRM) {
return Switch(
value: switchRM.value,
onChanged: (value) {
switchRM.value = value;
},
);
},
),
),
body: Container(),
)
One may argue that we can use a simple StatefulWidget
!. Yes, but notice that with states_rebuilder
the only part that rebuilds is the Switch
button.
Let's complicate the situation a bit. Let's say we want two switches one on the appBar and the other in the bottomSheet and we want the two switches to be synchronized. Let's add a button in the center of the body to toggle the switches.
If using StatefulWidget
, we must rebuild the entire Scaffold to fulfill the requirements.
With states_rebuilder
and using the new concept of reactive model keys, we can limit the rebuild to the switches only.
Widget build(BuildContext context) {
//define a reactive model key with initial value of true
final switchKey = RMKey(true);
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: StateBuilder<bool>(
observe: () => RM.create(true),
//assign the key to this StateBuilder widget
rmKey: switchKey,
builder: (ctx, switchRM) {
return Switch(
value: switchRM.value,
onChanged: (value) {
switchRM.value = value;
},
);
},
),
),
body: Center(
child: RaisedButton(
child: Text('Toggle'),
onPressed: () {
//set the value and notify listeners
switchKey.value = !switchKey.value ;
},
),
),
bottomSheet: StateBuilder<bool>(
//subscribe the this StateBuilder using the defined key
observe: () => switchKey,
builder: (ctx, switchRM) {
return Switch(
value: switchRM.value,
onChanged: (value) {
switchRM.value = value;
},
);
},
),
),
);
}
Similar to global keys in flutter, reactive model key (RMKey
) are used to control a state_builder
observer widget from outside.
final switchKey = RMKey(true);
Here we created a ReactiveModel
key and initialize it to be true.
title: StateBuilder<bool>(
observe: () => RM.create(true),
//assign the key to this StateBuilder widget
rmKey: switchKey,
builder: (ctx, switchRM) {
return Switch(
value: switchRM.value,
onChanged: (value) {
switchRM.value = value;
},
);
},
),
The StateBuilder
has a new parameter called rmKey
that receives the defined RMKey
.
By assigning a RM Key to a StateBuilder
widget, we can notify it to rebuild from outside the builder of the StateBuilder
widget.
RMKey
inherits all The ReactiveModel
functionality, such as setState
, setValue
, and state
, value
getters.
RaisedButton(
child: Text('Toggle'),
onPressed: () {
//set the value and notify listeners
switchKey.value = !switchKey.value ;
},
),
You can even subscribe widgets to a RMKey
so that wehner the RMKey
or the ReactiveModel
associated with it emits a notification, the subscribed widget will rebuild:
bottomSheet: StateBuilder<bool>(
//subscribe the this StateBuilder using the defined key
observe: () => switchKey,
builder: (ctx, switchRM) {
return Switch(
value: switchRM.value,
onChanged: (value) {
switchRM.value = value;
},
);
},
),
RMKey
has a new functionality called refresh. Let create a future like we used in the example above.
//createa RMKey
final fetchProductsKey = RMKey();
//..
StateBuilder(
models : [() => RM.getFuture<Foo,void>((f)=> f.fetchProducts())],
//associate it with this widget
rmKey: fetchProductsKey
builder :(context, rm){
}
)
When the StateBuilder
widget is inserted in the widget tree, the fetchProducts()
is invoked to get the list of products and the builder closure of the StateBuilder
widget will be called once the future resolves.
What if we want to refresh the list of products and call fetchProducts()
again.
With the concept of RMKey
, we just call the builtin refresh method:
RaisedButton(
child: Text('refresh products'),
onPressed: () {
fetchProductsKey.refresh();
},
If you have a counter with RMKey
, when refresh(), is called the counter is reset to its initial value.
Immutable state has its fun and advantages, with this update you can work with immutable states just as fine as you do with mutable state.
Let's see this imaginary example: Fetching for a list of products and add a product to the list and persist the new list.
This is the fake repository used to fetch and persist the list of products
//fake repository
List<Product> _products = [Product('prod1'), Product('prod2')];
Future<List<Product>> fetchProductsRepo() {
//simulate a delay
return Future.delayed(
Duration(seconds: 1),
() {
//simulate an error
if (Random().nextBool()) {
throw Exception('A Custom Message');
}
return _products;
},
);
}
Future addProductRepo(Product product) async {
//simulate a delay
await Future.delayed(
Duration(seconds: 1),
() {
//simulate an error
if (Random().nextBool()) {
throw Exception('A Custom Message');
}
_products.add(product);
},
);
}
This is the simple Product
model
//Product model
@immutable
class Product {
final name;
Product(this.name);
}
This is the immutable list Products state class
@immutable
class ProductsState {
final List<Product> products;
ProductsState(this.products);
//methods in the ProductsState must return a new state of ProductsState.
//We use Future want to show a CircularProgressIndicator while waiting for Products,
Future<ProductsState> getProducts() async {
final result = await fetchProductsRepo();
return ProductsState(result);
}
//We use stream generator because we want to yield back the old state on error
Stream<ProductsState> addProduct(Product product) async* {
//yield the new state so States_rebuilder will update the UI to display it,
yield ProductsState([...products, product]);
try {
await addProductRepo(product);
} catch (e) {
//yield the old state
yield this;
rethrow;
}
}
}
Notice that methods in the ProductsState
are the event that trigger the transform from the actual state to the next state. Methods in the ProductState
must returns a new instance of ProductsState
.
In fetchProducts
we returned Future because we want to wait for the asynchronous method to end and display a CircularProgressIndicator
.
Whereas in addProduct
we used a stream generator because we want to instantly display the new list (yield the new list) and execute in the background the asynchronous method. It is if the asynchronous method ends with an error, then we want to display the old list (yield the old list) and display a snackBar of the error.
Notice that in the same fashion as with mutable state, the business logic is a simple dart class.
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Injector(
//inject ProductsState with empty list of product (this is the initial state)
inject: [Inject(() => ProductsState([]))],
builder: (context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(),
body: Center(
child: WhenRebuilderOr<ProductsState>(
//get the registered instance of ProductsState ReactiveModel
observe: () => RM.get<ProductsState>()
//use the cascade operator and invoke the future method to call getProducts
..future(
(productState) => productState.getProducts(),
//If the future ends with error show an alert dialog
).onError(_showErrorDialog),
//While waiting for the future to end, display a CircularProgressIndicator
onWaiting: () => Center(
child: CircularProgressIndicator(),
),
builder: (context, productStateRM) {
final products = productStateRM.value.products;
return ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
return ListTile(
title: Text('${products[index].name}'),
);
},
);
},
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
final newProduct = Product('prod ${Random().nextInt(100)}');
//get the registered ProductsState ReactiveModel
RM.get<ProductsState>()
//invoke the stream method and define the error side effect
.stream(
(productState) => productState.addProduct(newProduct),
).onError(_showSnackBar);
},
),
),
);
},
);
}
void _showErrorDialog(BuildContext context, dynamic error) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: Text('error.message'),
);
},
);
}
void _showSnackBar(BuildContext context, dynamic error) {
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('error.message'),
),
);
}
}
First, we injected ProductsState
object with an initial state of an empty list.
//inject ProductsState with empty list of product (this is the initial state)
inject: [Inject(() => ProductsState([]))],
Then we called the fetchProducts
method form the observe
parameter of the WhenRebuilderOr
widget.
observe: () => RM.get<ProductsState>()
//use the cascade operator and invoke the future method to call getProducts
..future(
(productState) => productState.getProducts(),
//If the future ends with error show an alert dialog
).onError(_showErrorDialog),
- By
RM.get<ProductsState>()
we get the actual registeredReactiveModel
of the state in the service locator. - By
..future((productState) => productState.getProducts(),)
we triggered thegetProducts
method add told state_builder that this method is a future, so state_builder changes the state status of theReactiveModel
toisWaiting
to display theCircularProgressIndicator
and when the future ends with data,state_builder
change the registered instance in the service locator to hold the new state and notify observing widgets. - By
.onError(_showErrorDialog),
we toldstate_builder
what to do if the future ends with and error.
See docs for more information on error handling.
To add a product in the onPressed callback :
onPressed: () {
final newProduct = Product('prod ${Random().nextInt(100)}');
//get the registered ProductsState ReactiveModel
RM.get<ProductsState>()
//invoke the stream method and define the error side effect
.stream(
(productState) => productState.addProduct(newProduct),
).onError(_showSnackBar);
},
- By
RM.get<ProductsState>()
we get the actual registeredReactiveModel
of the state in the service locator. - By
.stream((productState) => productState.addProduct(newProduct),)
we toldstate_builder
thataddProduct
is a stream so it will subscribe to and notify observing widget on data emitting (yield). - By
.onError(_showSnackBar),
we toldstate_builder
what to do if the stream emits an error.
Stream are automatically disposed of after they are done or when the ReactiveModel
is disposed. So do not fear memory leakage.