Rebuilder: an easy and minimalistic state-management library for Flutter
UPDATE (02/06/2019): you can find an example of a quiz game built with this package here.
Since I started using Flutter I adopted the BLoC pattern (or something similar) for three main reasons: maximize the code sharing between Flutter and AngularDart, separation business logic / UI and the streams. But, after the release of Flutter web and played a little bit with it, and ported some little app made with Flutter, I realized that the first reason it is not so important anymore. Being evaluated the alternatives with this in mind, I found what could substitute it: the setState. Exactly.
The choice to come back to the old dear setState
could sound a little bit strange, but I think that for most apps it is sufficient if used in a slightly different manner.
There are two things that I like better of the BLoC pattern: the separation between UI / business logic, and the possibility, using the StreamBuilder
, to rebuild only a subset of the widgets on the screen. This is something hard to realize using the setState
in the classic way, this library aims to make it possible in an easier way.
It consists essentially of five elements.
- The
DataModel
where live the business logic for the UI - The
DataModelProvider
the provides this data model to the widgets tree - The
Rebuilder
widget that, similarly to the StreamBuilder, rebuild itself triggered by an event. - The
StateWrapper
that holds the state of a specificRebuilder
widget. - The
RebuilderObject
, bound to a specificStatesWrapper
, triggers the rebuild of theRebuilder
widget associated.
Contents:
- DataModel
- DataModelProvider and views
- Rebuilder widget
- RebuilderObject
- Rebuilding the rebuilder widget using its state
- Implementing a dynamic theme changer
- Conclusion
1. DataModel
Similarly to the single “BLoC” of the BLoC pattern, this model contains the business logic of the UI. Let’s see an example to implement a counter (it taken from the example in the package where you have multiple counters, here is shown only one) :
class CountersModel extends DataModel {
final counterUpState = StateWrapper();
int counterUp = 0;
void incrementCounterUp() {
counterUp++;
counterUpState.rebuild();
} @override
void dispose() {}
}
counterUpState
: it’s an instance of a theStateWrapper
class the will be bound to a specificRebuilderWidget
counterUp
: the variable that holds the current value of the counter._incrementCounterUp
: this method is called when a button is clicked. It incrementscounterUp
and calls therebuild
method of theStateWrapper
class to trigger the rebuild of the associatedRebuilderWidget
.
2. DataModelProvider and views
It is a simple data model provider that extends a StatefulWidget and use an InheritedWidget
to provide the model to the widgets on the tree.
final countersModel = CountersModel()DataModelProvider<CountersModel>(
dataModel: countersModel,
child: const CountersPage(),
);
Then, in the CountersPage
or other widgets in the tree you can get the countersModel
from the context, basically in the build
or the didChangeDependencies
.
Widget build(BuildContext context) {
final countersModel = DataModelProvider.of<CountersModel>(context);
From now on, you can access the counterModel
:
RaisedButton(
child: const Text(‘+’),
onPressed: countersModel.incrementCounterUp,
),
3. Rebuilder widget
The Rebuilder
widget basically rebuilds a “block” of widgets every time the rebuild
method of the StateWrapper
instance associated to it (the one passed to its parameter rebuilderState
) is called.
Rebuilder<CountersModel>(
dataModel: countersModel,
rebuilderState: countersModel.counterUpState,
builder: (state, data) {
return Text(‘${data.counterUp}’);
}),
Every time the users click on the RaisedButton
and the incrementCounterUp
is called, the counterUp
is incremented and the rebuild
method of the counterUpState
is called, rebuilding the Rebuilder
widget. The Text
widget inside the builder
of the Rebuilder
widget will show the new value of the counterUp
property.
4. RebuilderObject
We can implement the previous counter by using a RebuilderObject
to bind this to a specific Rebuilder
widget and avoid to manually call the rebuild
method. In the example app, you can find how this class is used to implement a dynamic theme changer.
CountersModel
class CountersModel extends DataModel {
CountersModel() {
// Initialize the instance of the `RebuilderObject` with
// with an instance of a `StateWrapper` that will be bound
// to a `Rebuilder` widget.
counterDown = RebuilderObject<int>.init(
rebuilderState: counterDownState,
initialData: 100,
onChange: _onCounterDownChange);
}
final counterDownState = StateWrapper(); RebuilderObject<int> counterDown;
void decrementCounterDown() {
counterDown.value--;
// Using the `RebuilderObject` the `rebuild` method of the
// counterDownState will automatically be called.
//
// states.counterDownState.rebuild();
}
void _onCounterDownChange() {
print('Value changed: ${counterDown.value}');
} @override
void dispose() {}
}
Let’s see in the details the initialization of the RebuilderObject
:
counterDown = RebuilderObject<int>.init(
rebuilderState: counterDownState,
initialData: 100,
onChange: _onCounterDownChange);
rebuilderState
: it takes the same state of theRebuilder
widget it has to be bound to.initialData
: the initial value of the counter.onChange
: every time a new value is set will be called the function passed as an argument. In this case, when the button is clicked, in the console will be print the value of the counter.
View
Column(
children:[
const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
‘CounterDown:’,
style: TextStyle(fontWeight: FontWeight.w500),
),
),
Rebuilder<RebuilderObject>(
dataModel: countersModel.counterDown,
rebuilderState: countersModel.counterDownState,
builder: (state, data) {
return Text(‘${data.value}’);
}),
RaisedButton(
child: const Text(‘-’),
onPressed: countersModel.decrementCounterDown,
)
],
),
5. Rebuilding the rebuilder widget using its state
Another way to rebuild only the widgets inside the builder function of the Rebuilder
widget, is to use the its rebuild
method:
Rebuilder<CountersModel>(
dataModel: countersModel,
rebuilderState: countersModel.counterMulState,
builder: (state, data) {
return Column(children: <Widget>[
Text(‘${data.counterMul}’),
RaisedButton(
child: const Text(‘*’),
onPressed: () {
data.counterMul *= 2;
if (data.counterMul > 65536) {
data.counterMul = 2;
}
state.rebuild();
}),
]);
}),
counterMul
is a property of type integer initialized with the value of2
on thecountersModel
.state.rebuild()
will trigger the rebuilding of theRebuilder
widget, showing the new value ofcounterMul
.
6. Implementing a dynamic theme changer
This code is taken from the example app of the library. It uses a RebuilderObject
to change the theme of the app.
Step 1 — Themes
final themes = {
‘Default’: ThemeData(
brightness: Brightness.light,
backgroundColor: Colors.blue[50],
scaffoldBackgroundColor: Colors.blue[50],
primaryColor: Colors.blue,
primaryColorBrightness: Brightness.dark,
accentColor: Colors.blue[300],
),
‘Teal’: ThemeData(
brightness: Brightness.light,
backgroundColor: Colors.teal[50],
scaffoldBackgroundColor: Colors.teal[50],
primaryColor: Colors.teal[600],
primaryColorBrightness: Brightness.dark,
accentColor: Colors.teal[300],
),
'Orange’: ThemeData(
brightness: Brightness.light,
backgroundColor: Colors.orange[50],
scaffoldBackgroundColor: Colors.orange[50],
primaryColor: Colors.orange[600],
primaryColorBrightness: Brightness.dark,
accentColor: Colors.orange[300],
),
‘Dark’: ThemeData.dark(),
};
Step 2 — AppModel
E.g. declare a StateWrapper for the `MaterialApp` widget to rebuild it when a new app theme is set. This is then given to the `rebuilderState` parameter of the [Rebuilder] widget.
In the AppModel
:
class AppModel extends DataModel {
AppModel() {
chosenTheme = RebuilderObject<String>.init(
rebuilderState: materialState,
initialData: 'Default',
onChange: () => print('changedTheme ${chosenTheme.value}'));
} final materialState = StateWrapper(); RebuilderObject<String> chosenTheme; void changeTheme(String theme) {
// This will automatically rebuild the MaterialApp
chosenTheme.value = theme;
}}
Step 3— Wrapping the
MaterialApp
in aRebuilder
:
Rebuilder(
rebuilderState: appModel.materialState,
builder: (state, _) {
return MaterialApp(
title: ‘Rebuilder example’,
theme: themes[appModel.chosenTheme.value],
home: MainPage());
});
Step 4 — Settings page
Scaffold(
body: Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(28.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children:[
const Padding(
padding: EdgeInsets.all(8.0),
child: Text(‘Choose a theme:’,
style: TextStyle(fontWeight: FontWeight.w500),
),
),
DropdownButton<String>(
value: appModel.chosenTheme.value,
items: [
for (var theme in themes.keys)
DropdownMenuItem<String>(
value: theme,
child: Text(theme,
style: const TextStyle(fontSize: 14.0)),
)
],
onChanged: appModel.changeTheme,
),
],
),
),
);
7. Conclusion
This library is in its very first version and needs to be improved, in this repository on GitHub you can find the source code, and the example app. For any reason, feel free to fill an issue in the repo.