StoreConnector
The connector pattern can easily be implemented with the BuildContext extensions,
as previously discussed.
However, AsyncRedux optionally provides three classes to help you implement the connector pattern in a more structured way:
ViewModel: Contains only the information your dumb widget needs.VmFactory: Creates the view-model from the store state, by separating and transforming the state.StoreConnector: Connects the store to the dumb widget, by using the factory to create the view-model, and then passing the view-model to the dumb widget.
Steps
- The smart widget reads the store state, and uses a "factory" to prepare the "view-model".
- The view-model is simply an object that contains only the information the dumb widget needs.
- The information in the view-model is in a convenient format for the dumb-widget to work with.
- The view-model may also contain function callbacks that the dumb widget needs to dispatch actions.
- The dumb widget then uses the view-model to build itself.
- The dumb widget knows nothing about the store or the state.
The process
Each time some action reducer changes the store state, all the widgets in the screen that use that state should rebuild.
This means that all widgets that are currently in the screen (in the widget tree),
and that contain StoreConnectors, will create a factories object, and as this
factory to create them a new view-model object, from the most current state.
The factory itself is simply an object that is specialized in creating the view-model from the store state.
Once the view-model is created, the StoreConnector will compare it with the previous view-model
created with the previous state. Only if the view-model changed, the connector rebuilds,
sending the new view-model to the dumb widget to rebuild.
If the view-model didn't change, the connector doesn't rebuild. This makes sense because the view-model contains all the information the dumb widget needs, and if that information didn't change, there is no need to rebuild the dumb widget.
Not triggering the StoreConnectors
If you use the notify: false parameter when dispatching an action,
then the StoreConnectors will not be triggered (will not calculate the current view-model
and will not rebuild) as a consequence of that action changing the store state:
dispatch(MyAction1(), notify: false);
Note: When
notifyis true or is omitted, all theStoreConnectors will at least recalculate their view-model whenever the state changes. This means that creating the view-model should be a fast operation. If it's not, you should consider using a cached "selector" to optimize the process (more on that later).
Example
Let's create a dumb widget called MyCounter, which is a counter widget.
It displays a description for the current counter number,
and uses an onIncrement function to increment the counter when a button is pressed:
MyCounter(
counter: 2,
description: "2 is the first non-zero even number",
onIncrement: () { ... },
);
The connector will be called MyCounterConnector.
It contains a StoreConnector, with a builder method that creates
the MyCounter widget:
class MyCounterConnector extends StatelessWidget {
Widget build(BuildContext context) {
return StoreConnector(
... MyCounter(...)
The StoreConnector has a few interesting parameters, but the most important are
the vm and builder parameters:
class MyCounterConnector extends StatelessWidget {
Widget build(BuildContext context) {
return StoreConnector(
vm: () => Factory(this),
builder: (...) => MyCounter(...)
The vm parameter is a function that when called should return a factory of type VmFactory:
AsyncRedux will use the vm parameter to create a factory,
and then will use this factory to create the current view-model of type Vm.
The current view-model will be compared with the previous view-model. In case they
are different, AsyncRedux will call the builder and pass it the new view-model.
Finally, the builder will use the view-model to create the dumb widget.
This is the complete connector code:
class MyCounterConnector extends StatelessWidget {
Widget build(BuildContext context) {
return StoreConnector<AppState, ViewModel>(
vm: () => Factory(this),
builder: (BuildContext context, ViewModel vm) => MyCounter(
counter: vm.counter,
description: vm.description,
onIncrement: vm.onIncrement,
));
}}
View-model
From the above code it's obvious that the view-model should contain the counter, description
and onIncrement fields. It must extend Vm:
class ViewModel extends Vm {
final int counter;
final String description;
final VoidCallback onIncrement;
ViewModel({
required this.counter,
required this.description,
required this.onIncrement,
}) : super(equals: [counter, description]);
}
As discussed, AsyncRedux needs to compare the previous and current view-models.
This means your ViewModel object needs to implement the operator == method.
If you don't, all view-models will be considered different from each other, which will result in the connector always rebuilding, even when not necessary. This won't create any visible problems to your app, but is inefficient and may be slow.
The operator == method may be implemented in three ways:
-
By typing
ALT+INSERTin IntelliJ IDEA and choosing==() and hashcode. You can't forget to update this whenever new parameters are added to the model. -
You can use the built_value package to ensure they are kept correct, without you having to update them manually.
-
Just add all the fields you want (which are not functions/callbacks) to the
equalsparameter to theViewModel'sequalparameter. This will allow the view-model to automatically create its ownoperator ==implicitly. For example:
ViewModel({
required this.field1,
required this.field2,
}) : super(equals: [field1, field2]);
The VmEquals interface
Each state passed in the equals parameter will, by default, be compared by equality (==).
This is almost always what you want.
However, you can provide your own comparison method, if you want. To that end, your state classes
must implement the VmEquals interface. As a default, objects of type VmEquals are compared by
their own VmEquals.vmEquals() method, which by default is an identity comparison.
You may then override this method to provide your custom comparisons.
For example, here description will be compared by equality, while myObj will be compared by
its info length:
class ViewModel extends Vm {
final String description;
final MyObj myObj;
ViewModel({
required this.description,
required this.myObj,
}) : super(equals: [description, myObj]);
}
...
class MyObj extends VmEquals<MyObj> {
String info;
bool operator ==(Object other) => info.length == other.info.length;
int get hashCode => 0;
}
Factory
You also need to create a factory object to create the view-model from the store state.
It must extend VmFactory and implement the fromStore method:
class Factory
extends VmFactory<AppState, MyCounterConnector, ViewModel> {
Factory(connector) : super(connector);
ViewModel fromStore() => ViewModel(
counter: state.counter,
description: state.description,
onIncrement: () => dispatch(IncrementAndGetDescriptionAction()),
);
}
The fromStore method is called automatically by AsyncRedux, when necessary.
Note it has direct access to the state and to all dispatch methods
like dispatch, dispatchAndWait
etc.
You can also create helper methods to assist you in creating the view-model.
For example, here description and the onIncrement method are created by helper methods:
class Factory
extends VmFactory<AppState, MyCounterConnector, ViewModel> {
Factory(connector) : super(connector);
ViewModel fromStore() => ViewModel(
counter: state.counter,
description: _description(),
onIncrement: _onIncrement,
);
String _description() => state.description;
void _onIncrement() => dispatch(IncrementAndGetDescriptionAction());
}
This may not seem necessary in this simple example, but for more complex view-models it can be very useful being able to separate the view-model creation into smaller methods.
The complete example
Here is the complete example:
class MyCounterConnector extends StatelessWidget {
Widget build(BuildContext context) {
return StoreConnector<AppState, ViewModel>(
vm: () => Factory(this),
builder: (BuildContext context, ViewModel vm) => MyCounter(
counter: vm.counter,
description: vm.description,
onIncrement: vm.onIncrement,
));
}
}
class ViewModel extends Vm {
final int counter;
final String description;
final VoidCallback onIncrement;
ViewModel({
required this.counter,
required this.description,
required this.onIncrement,
}) : super(equals: [counter, description]);
}
class Factory extends VmFactory<AppState, MyCounterConnector, ViewModel> {
Factory(connector) : super(connector);
ViewModel fromStore() => ViewModel(
counter: state.counter,
description: state.description,
onIncrement: () => dispatch(IncrementAndGetDescriptionAction()),
);
}