Rebuilder: an easy and minimalistic state-management library for Flutter

Francesco Mineo
5 min readMay 31, 2019

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 setStatecould 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 specific Rebuilder widget.
  • The RebuilderObject, bound to a specific StatesWrapper, triggers the rebuild of the Rebuilder widget associated.
The example app in the repository.

Contents:

  1. DataModel
  2. DataModelProvider and views
  3. Rebuilder widget
  4. RebuilderObject
  5. Rebuilding the rebuilder widget using its state
  6. Implementing a dynamic theme changer
  7. 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 the StateWrapper class the will be bound to a specific RebuilderWidget
  • counterUp : the variable that holds the current value of the counter.
  • _incrementCounterUp : this method is called when a button is clicked. It increments counterUp and calls the rebuild method of the StateWrapper class to trigger the rebuild of the associated RebuilderWidget.

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 the Rebuilder 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 of 2on the countersModel.
  • state.rebuild() will trigger the rebuilding of the Rebuilder widget, showing the new value of counterMul.

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 a Rebuilder:

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.

--

--

Francesco Mineo

Medical doctor, singer & music producer , software developer.