Flutter State Management: setState(fn) is the easiest and the most powerful

Flutter is an elegant, lightweight framework for building cross-platform mobile apps. It offers a great developer experience, especially when it comes to building UIs. Thanks to the many different widgets provided by the framework, this task is fairly simple to do. When it comes to state management, opinions diverge. State management is one of the hottest topics in the Flutter Community right now, with many differing method and view points.

Even the Flutter documentation states:

” This is a complex topic with strong, and differing, opinions”

The product of any good state management system is to separate business logic form the UI/Presentation one. There are many state management techniques in the world of Flutter with the most basic and easiest one being setState(fn) which is rapidly abundant in favor of more elaborate techniques when dealing with more complicated apps. Jorge Coca has a good article to help you understand and choose the right state management solution for your app.

In this article, I try to make you reconsider setState(fn) and try to convince you that it is more powerful then what it is was thought to be. In addition to this, I will try to show that managing state is the easiest part in your fluttering time.


The main idea is to pass the states as well as the setState(fn) method to the BloC classes so that one can use setState(fn) after state mutation to rebuild any widgets that one wants to be updated from the BloC.

To do so, I will demonstrate the power of setState(fn) with the help of a typical counter app that will have this widgets hierarchy:

Widget hierarchy of the demo App.The app has three BloC classes, statefulWidgets (red rectangles) and statelessWidgets (white rectangles).

MainBloc is the top most BloC and it will be available to all widgets of the app during the lifetime of the app. Here is the code of this class:

// File : ‘lib/blocs/main_bloc.dart’
import './bloc_setting.dart';
class MainBloc extends BloCSetting {
final String title = "setState() is powerful";
int counter1 = 0;
int counter2 = 0;
}
MainBloc mainBloc; // do not instantiated it

The MainBloc class extends a BloCSetting class that I will talk about it later. It defines a String field that holds the title of the app, also it defines two fields for tracking the values of counter one and two. In the last line I declared a variable of type MainBloc. It is important to not instantiate it at this stage.

The cout1Bloc is defined as follows:

// File : ‘lib/blocs/count1_bloc.dart’
import 'bloc_setting.dart';
import 'main_bloc.dart';
class Count1Bloc extends BloCSetting {
int counter1 = 0;
incrementCounter(state) {
rebuildWidgets(
setStates: () {
counter1++;
},
states: [state],
);

mainBloc.counter1 = counter1;
}
}
Count1Bloc count1Bloc;

Here I define a counter1 field and an incrementCounter method witch takes an argument of name state and of type dynamic (it ends to be State). The rebuildWidgets(this) is defined in the BloCSetting class. The rebuildWidgets() role is to commit the state mutation and update the widgets of the states that are listed in the second argument of the method. I will talk later about the implementation of this method.

This is the bloC part of the counter1 widget. As you can see there is no reference to any UI components neither to witch widgets nor classes it will be used in.

Let’s move the UI starting by the MyApp class:

// File : ‘lib/main.dart’
import 'package:flutter/material.dart';
import 'blocs/main_bloc.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
mainBloc = MainBloc();
}
@override
void dispose() {
mainBloc = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "SetState management",
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}

MyAppis a StatefulWidget type for the simple reason to instantiate the mainBloc in the initState() method and destroy it in the dispose() method to release resources and let it face its fate with dart garbage collector. Now on, any widget below the MyApp widget (all the app) has free access to the mainBloc.

for MyHomePage, there is nothing spatial. It defines tow buttons to navigate to counter one or two pages.

// File : ‘lib/main.dart’
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(mainBloc.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
child: Text('Go To Counter 1'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Counter1(),
),
);
},
),
RaisedButton(
child: Text('Go To Counter 2'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Counter2(),
),
);
},
)
],
),
),
);
}
}

The most interesting part is in Counter1 definition:

// File : 'lib/counter1.dart'
import 'package:flutter/material.dart';
import 'blocs/main_bloc.dart';
import 'blocs/count1_bloc.dart';
class Counter1 extends StatefulWidget {
@override
_Counter1State createState() => _Counter1State();
}
class _Counter1State extends State<Counter1> {
@override
void initState() {
super.initState();
count1Bloc = Count1Bloc();
}
@override
void dispose() {
count1Bloc = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(mainBloc.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button, on total, this many times:',
),
Text(
'${mainBloc.counter1}',
style: Theme.of(context).textTheme.display1,
),
Text(
'You have pushed the button this many times:',
),
Text(
'${count1Bloc.counter1}',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => count1Bloc.incrementCounter(this),
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}

It is a StatefulWidget that instantiates the count1Bloc in initState() method and destructs it in the dispose() method. The count1Bloc is said to be provided to all the widgets underneath the widget where it is instantiated (Counter1 in our case).

The title of the page is provided from the mainBloc.title, the total cumulative count of counter1 is taken form mainBloc.counter1 and the actual count is saved in count1Bloc.counter1.

The incrementCounter method is called with “this” as argument, “this” here means this state of the statefulWidget.

Now to understand how the setState(fn) is passed to the BloC, let’s look at the rebuildWidgets() in incrementCounter(state) method:

incrementCounter(state) {
rebuildWidgets(
setStates: () {
counter1++;
},
states: [state],
);
mainBloc?.counter1 = counter1;
}

The first argument of rebuildWidgets() looks like the conventional setState(fn) in flutter inside it state variables are mutated. The states argument is a list of states to be rebuilt as a consequence of this state change. I choose to pass a list to the states argument to make it available to me to rebuild many widgets at the same time.

The “this” in incrementCounter(this) is available because the FAB button is inside the same statefulWidget where I want it to be rebuilt. What if this is not the case. For example, let’s move the FAB to another statelessWidget. Counter2 widget is implemented in such way.

First let’s look at Cout2Bloc:

// File : 'lib/blocs/count2_bloc.dart'
import 'bloc_setting.dart';
import 'main_bloc.dart';
class Count2Bloc extends BloCSetting {
var counter2State;
int counter2 = 0;
incrementCounter() {
rebuildWidgets(
setStates: () {
counter2++;
},
states: [counter2State],
);
mainBloc?.counter2++;
}
}
Count2Bloc count2bloc;

First I declared the conter2State variable to hold the state of counter2 which will be initialized form the counter2 widget,

The remain is the same as in counter1 with the exception that “this” is removed from the incrementCounter() arguments.

// File : 'lib/counter2.dart'
import 'package:flutter/material.dart';
import 'blocs/main_bloc.dart';
import 'blocs/count2_bloc.dart';
class Counter2Init extends StatefulWidget {
@override
_Counter2InitState createState() => _Counter2InitState();
}
class _Counter2InitState extends State<Counter2Init> {
@override
void initState() {
super.initState();
count2Bloc = Count2Bloc();
}
@override
void dispose() {
count2Bloc = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return Counter2();
}
}

As usual I instantiate and destroy the count2Bloc in statefulWidget.

 // File : 'lib/counter2.dart'
class Counter2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(mainBloc.title),
),
body: Center(
child: Counter2Text(),
),
floatingActionButton: FloatingActionButton(
onPressed: count2Bloc.incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}

Counter2 is a statelessWidget and FAB calls the count2Bloc.incrementCounter with no argument.

Counter2Text is a statefulWidget that is used to display counter values:

// File : 'lib/counter2.dart'
class Counter2Text extends StatefulWidget {
@override
_Counter2TextState createState() => _Counter2TextState();
}
class _Counter2TextState extends State<Counter2Text> {
@override
void initState() {
super.initState();
count2Bloc.counter2State = this;
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button, on total, this many times:',
),
Text(
'${mainBloc.counter2}',
style: Theme.of(context).textTheme.display1,
),
Text(
'You have pushed the button this many times:',
),
Text(
'${count2Bloc.counter2}',
style: Theme.of(context).textTheme.display1,
),
],
);
}
}

The crucial step here occurs inside initState() method. Here I initialized the count2Bloc.counter2State to hold “this” state.

The remainder or the class is very easy to understand.

Go back and see the BloC classes and remark they are free of any UI related component. In the same fashion, UI widget classes are clean of any BloC mess. This allows me to declare a 100% separation of concerns.

It remains to me to show you how the BloCSetting class is implements:

import 'package:flutter/material.dart';
class BloCSetting extends State {
rebuildWidgets({VoidCallback setStates, List<State> states}) {
if (states != null) {
states.forEach((s) {
if (s != null && s.mounted) s.setState(setStates ??(){});
});
}
}
@override
Widget build(BuildContext context) {
print(
"This build function will never be called. it has to be overriden here because State interface requires this");
return null;
}
}

BloCSetting extends State abstract class to make the use of flutter setState(fn) allowed. Inside it there is only on active method rebuildWidgets() which takes a list of states, iterate between them and execute setState(fn) with the function provided in the setStates argument.

That’s it. Hoping I convinced you of this technique. For myself I am very convinced of the reliably of the technique and I used it to reproduce many of published state management demonstration apps such as:

1- Didier Boelens’ movie online catalogue for Bloc;

2- Filip Hracek’s state expiremnts;

3- Brian Egan’s Flutter Architecture Samples;

4- All the show cases in the flutter State management documentation page;

Remarks:

1- For those how do not like the way I provided the Blocs, they can provide them using the InheritedWidget class provider. (I would be very grateful if they show me why they prefer InheritedWidget method).

2- Adapting this approach, I found myself extensively using statefullWidget which pushes me to extend this approch to work with statelessWidget. How to do that is a seed of other possible articale;

3- 1- I am not only constrained to use conventional variable types in my BloCs. I have the ability to use flutter predefined classes and enums. I also have the ability to conditionally style the UI from the bloc. For example I can conditionally change the icon of FAB from the bloc.

This is the BloC part:

IconData counter1Icon = Icons.add;
incrementCounter(state) {
counter1++;
counter1Icon = (counter1 < 10) ? Icons.add : Icons.add_circle;
mainBloc?.counter1 = counter1;

rebuildWidgets([state]);
}

And the user interface becomes:

floatingActionButton: FloatingActionButton(
onPressed: () => count1Bloc.incrementCounter(this),
tooltip: 'Increment',
child: Icon(count1Bloc.counter1Icon),
),

4- rebuildWidgets can be called after awaiting async task and to update the UI accordingly. for exemple:

incrementCounter(state) {
counter1++;
mainBloc?.counter1 = counter1;
await Future.delayed(Duration(seconds: 1));
    rebuildWidgets([state]);
}

6- The helper class BloCSetting can be converted to library package or integrated into the flutter default framework to be available when importing the material library.

7- To work only with statelessWidgets, as I mentioned in point 2 above, BloCSetting needs to be changed a little bit.

8- you find the hole app code in this github repo: https://github.com/GIfatahTH/Flutter-basic-and-powerful-state-management