Table of contents
- setState
- Provider package
- ChangeNotifier
- ChangeNotifierProvider
- Consumer
- Provider.of
- Combine providers
- Final considerations
- BLoC pattern
- Implementing the pattern
- Bloc
- Cubit
- Combine Blocs
- Widgets
- BlocProvider
- BlocBuilder, BlocListener, and other widgets to react to state changes
- BlocProvider.of<Bloc> and context shortcuts
- Observe all blocs
- Insights
- About Us
Let's see some of the commonly used techniques to manage states in our applications
setState
This example shows the most common situation where a stateful widget has to share its state with some stateless sub widgets and this is accomplished by passing props. If we want to update the state from a sub-widget we have to share also a method to update it, and we can pass it in props too. It's a common pattern that you can find in almost all declarative UI frontend frameworks.
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
// state
int count = 0;
// method to update state
void increment() {
setState(() {
count++;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Center(
child: Parent(
count: count,
increment: increment,
),
),
);
}
}
class Parent extends StatelessWidget {
final int count;
final void Function() increment;
const Parent({Key? key, required this.count, required this.increment})
: super(key: key);
@override
Widget build(BuildContext context) {
return Child(
count: count,
increment: increment,
);
}
}
class Child extends StatelessWidget {
final int count;
final void Function() increment;
const Child({Key? key, required this.count, required this.increment})
: super(key: key);
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: increment,
child: Text("$count"),
);
}
}
For an app state that you need to modify from many different places, you’d have to pass around a lot of callbacks, and this gets old and messy pretty quickly
We can also pass Widgets as props With widget composition, we can solve part of this problem, with this trick the child component can access the state without getting it from the parent
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
// state
int count = 0;
// method to update state
void increment() {
setState(() {
count++;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Center(
child: Parent(
child: Child(
count: count,
increment: increment,
),
),
),
);
}
}
class Parent extends StatelessWidget {
final Widget child;
const Parent({Key? key, required this.child}) : super(key: key);
@override
Widget build(BuildContext context) {
return child;
}
}
class Child extends StatelessWidget {
final int count;
final void Function() increment;
const Child({Key? key, required this.count, required this.increment})
: super(key: key);
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: increment,
child: Text("$count"),
);
}
}
Drilling props could also have some performance issues, because every time you update a state, all the chain of widgets from the stateful widget will be rebuilt
And what if we need to share some global states, maybe persistent through route changes, this can be hard to handle only by drilling props
So setState is useful for widget-specific, ephemeral states. For global and persistent states we need something more
Provider package
https://pub.dev/packages/provider
Flutter indeed has mechanisms for widgets to provide data and services to their descendants with some low-level widgets (InheritedWidget, InheritedNotifier, InheritedModel, and more)
But instead of using those low-level widgets, we can use this package called Provider. We just need to understand these 3 concepts:
- ChangeNotifier
- ChangeNotifierProvider
- Consumer / Provider.of
ChangeNotifier
ChangeNotifier is a simple mixin included in the Flutter SDK which provides change notifications to its listeners
To notify the listeners you have to call the notifyListeners() method from ChangeNotifier every time you update the state
class Counter with ChangeNotifier {
// state
int count = 0;
// increment counter
void increment() {
count++;
notifyListeners();
}
}
ChangeNotifier doesn't require widgets to work so you can test it standalone
test('increment counter', () {
final model = Counter();
final count = model.count;
model.addListener(() {
expect(model.count, greaterThan(count));
});
model.increment();
});
ChangeNotifierProvider
ChangeNotifierProvider is the widget that provides an instance of a ChangeNotifier to its descendants. It comes from the Provider package.
Usually, you don’t want to place ChangeNotifierProvider higher than necessary, but just above the widgets that need to access it
ChangeNotifierProvider(
create: (context) => Counter(),
child: const MyApp(),
),
If you want to provide more than one ChangeNotifier, you can use MultiProvider
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => Counter(),
),
...
],
child: const MyApp(),
),
Consumer
To listen to state changes we can use the Consumer widget, specifying which model you want to access in the template
Consumer<Counter>(
builder: (context, model, child) {
return Column(
children: [
ElevatedButton(
onPressed: model.increment,
child: Text("${model.count}"),
),
// ExpensiveWidget without rebuilding every time.
child,
],
);
},
child: const ExpensiveWidget(),
)
- builder is the function that is called for rebuilding the returned widgets, whenever the ChangeNotifier notifies events (when you call notifyListeners() in your model)
- child parameter, is there for optimization. If you have a large widget subtree under your Consumer that doesn’t change when the model changes, you can construct it once and get it through the builder.
It is best practice to put your Consumer widgets as deep in the tree as possible. You don’t want to rebuild large portions of the UI just because some detail somewhere changed.
Provider.of
Another way to access the state and attach widgets as listeners to ChangeNotifier is by calling Provider.of(context) inside the build function of a widget
...
Widget build(BuildContext context) {
// access the state and register listener to ChangeNotifier similar to using Consumer as a wrapper of this widget
final model = Provider.of<Counter>(context)
...
}
Sometimes you don't want to register a listener that rebuilds the current widget on every change, but you just want to access the current state, for example in callbacks
void myFunction() {
final model = Provider.of<Counter>(context, listen: false);
...
}
Combine providers
ProxyProvider is a provider that combines multiple values from other providers into a new object and sends the result to Provider This new object will then be updated whenever one of the providers we depend on gets updated.
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Counter()),
ChangeNotifierProxyProvider<Counter, SubCounter>(
create: (context) => SubCounter(0),
update: (context, counter, subcounter) => SubCounter(counter.count),
),
],
child: Foo(),
);
class SubCounter with ChangeNotifier {
SubCounter(this.start);
final int start;
int count = 0;
int get total => start + count;
void increment() {
count++;
notifyListeners()
}
}
Final considerations
With Provider and ChangeNotifier you can accomplish all your needs in terms of state management, but having your models and their business logic in the same class is kind of messy in complex projects
BLoC pattern
This design pattern helps to separate presentation from business logic. Following the BLoC pattern facilitates testability and reusability
Data will be flowing from the BLOC to the UI or even from UI to BLOC in form of streams
To visualize the notion of a stream you can consider a pipe with two ends, only one allowing to insert something into it. When you insert something into the pipe it flows inside and gets out from the other end The concept is the same as rxjs Observables if you are familiar with
Implementing the pattern
https://pub.dev/packages/flutter_bloc
This package uses Provider under the hood to share the streams of your changing states inside the application
Cubit and Bloc are the classes in this package to implement these streams
Both Blocs and Cubits will ignore duplicate states so it's a good practice to create comparable states, overriding ==operator and hashValue or using equatable package
It's a good practice to emit immutable objects on streams
Bloc
Implementing the Bloc class creates 2 streams, one to emit the state from the Bloc to the UI and one to emit events from the UI to the Bloc
In the constructor, we can define how the Bloc should react to the events emitted from the UI and we can set the initial state
With the on\ method, we can listen to events coming from the UI, and in the callback, we can emit a new state using the emitter that you will receive from the parameters or you can emit errors with addError method
Inside a Bloc, we can access the current state via the state getter
From the UI we can emit new events with add method exposed by the Bloc class
We can retrieve our Bloc from the UI using Provider
Events are handled by default concurrently, but we can trace them very well with the Bloc class and we can observe the Bloc and do stuff by overriding methods like:
- onEvent
- onChange (you will receive the old state and the new one)
- onTransition (similar to onChange but it contains also the event that triggered the state change)
- onError methods
You can also provide a custom EventTransformer to change the way incoming events are processed by the Bloc. Check out package bloc_concurrency for an opinionated set of event transformers
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterState(0)){
on<CounterIncrementEvent>(_increment, transformer: sequential());
}
void _increment(CounterEvent event, Emitter<CounterState> emit) async {
emit(CounterState(state.value + 1));
// addError(Exception('increment error!'), StackTrace.current);
}
@override
void onEvent(CounterEvent event) {
super.onEvent(event);
print(event);
}
@override
void onChange(Change<int> change) {
super.onChange(change);
print(change);
}
@override
void onTransition(Transition<CounterEvent, CounterState> transition) {
super.onTransition(transition);
print(transition);
}
@override
void onError(Object error, StackTrace stackTrace) {
print('$error, $stackTrace');
super.onError(error, stackTrace);
}
}
@immutable
abstract class CounterEvent {}
class CounterIncrementEvent extends CounterEvent {}
@immutable
class CounterState {
final int value;
const CounterState(this.value);
@override
int get hashCode => value.hashCode;
@override
bool operator ==(Object other) {
if (other is CounterState) {
return other.value == value;
}
return false;
}
}
bloc_test is a usefull package for testing Blocs https://pub.dev/packages/bloc_test
blocTest<CounterBloc, CounterState>(
'increment counter',
build: () => CounterBloc(),
act: (bloc) => bloc
..add(CounterIncrementEvent())
..add(CounterIncrementEvent()),
expect: () => <CounterState>[
CounterState(1),
CounterState(2),
],
);
Cubit
This is a subset of the Bloc class, it reduces complexity and simplifies boilerplate for managing simpler states It eliminates the event stream and uses functions instead, to update the state
We can define a new Cubit specifying the object type to be transmitted into the stream (in this case CounterState) and emitting new values with emit method or emitting errors with addError
In the constructor, it requires an initial state
Inside a Cubit, we can access the current state via the state getter
We can observe the Cubit and do stuff like in Bloc but we can override only the onChange and onError methods
Compared with Bloc we lose some traceability to reduce complexity
class CounterCubit extends Cubit<CounterState> {
CounterCubit() : super(const CounterState(0));
void increment() {
emit(CounterState(state.value + 1));
// addError(Exception('increment error!'), StackTrace.current);
}
@override
void onChange(Change<CounterState> change) {
super.onChange(change);
print(change);
}
@override
void onError(Object error, StackTrace stackTrace) {
print('$error, $stackTrace');
super.onError(error, stackTrace);
}
}
@immutable
class CounterState {
final int value;
const CounterState(this.value);
@override
int get hashCode => value.hashCode;
@override
bool operator ==(Object other) {
if (other is CounterState) {
return other.value == value;
}
return false;
}
}
You can test it in the same way as Bloc with the bloc_test package
blocTest<CounterCubit, CounterState>(
'increment counter',
build: () => CounterCubit(),
act: (bloc) => bloc
..increment()
..increment(),
expect: () => <CounterState>[
CounterState(1),
CounterState(2),
],
);
If you don't know if you should implement your state as Cubit or Bloc, just start with Cubit and the eventually refactor to Bloc on needs
Combine Blocs
You should not do this, sibling dependencies between two entities in the same architectural layer should be avoided because are hard to maintain, no Bloc should know about any other Bloc
But if you have to combine Blocs and get notified of state changes, we can subscribe to other Bloc dependencies in the Bloc constructor These dependencies have to be provided in the constructor's arguments
class FilteredCounterBloc extends Bloc<FilteredCounterEvent, FilteredCounterState> {
final counterBloc CounterBloc;
StreamSubscription counterSubscription;
FilteredCounterBloc({@required this.counterBloc}) {
counterSubscription = counterBloc.listen((state) {
if (state is CounterState) {
add(this.counterUpdated);
}
});
}
void counterUpdate(CounterState state) {
// do something
}
// ...
Widgets
BlocProvider
Same as with Provider we have to provide our Blocs to be accessible in our application You don’t want to place BlocProvider higher than necessary, but just above the widgets that need to access it
BlocProvider(
create: (context) => CounterCubit(), // or CounterBloc()
child: MyApp(),
)
If you want to provide more than one Bloc, you can use MultiBlocProvider
MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => CounterCubit(),
),
BlocProvider(
create: (context) => CounterBloc(),
),
...
],
child: const MyApp(),
),
RepositoryProvider is a Flutter widget that provides a repository to its children via RepositoryProvider.of(context). It is used as dependency injection (DI) widget so that a single instance of a repository can be provided to multiple widgets within a subtree. BlocProvider should be used to provide Blocs whereas RepositoryProvider should only be used for repositories. If you want to provide more than one Repository, you can use MultiRepositoryProvider
BlocBuilder, BlocListener, and other widgets to react to state changes
To listen to state changes we can use the BlocBuilder widget specifying which Bloc you want to access and the emitted data type in the template
BlocBuilder<CounterCubit, CounterState>(
builder: (context, state) {
return ElevatedButton(
onPressed: context.read<CounterCubit>().increment,
child: Text("${state.value}"),
);
},
)
BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return ElevatedButton(
onPressed: () => context.read<CounterBloc>().add(CounterIncrementEvent()),
child: Text("${state.value}"),
);
},
)
Use BlocListener (or MultiBlocListener) if the UI child isn't directly affected by state changing and you just need to do something in response to state changes like navigation or showing modals etc...
BlocListener<CounterCubit, CounterState>(
listener: (context, state) {
if(state.value == 10) {
Navigator.of(context).pushNamed("/someroute");
}
},
child: ChildWidget(),
)
You can use BlocConsumer if you need both builder and listener
BlocConsumer<CounterCubit, CounterState>(
listener: (context, state) {
if(state.value == 10) {
Navigator.of(context).pushNamed("/someroute");
}
},
builder: (context, state) {
return Text("${state.value}");
},
)
You can use BlocSelector if you need to react only to a part of state changing
BlocSelector<CounterCubit, CounterState, bool>(
selector: (state) {
return state.value > 0;
},
builder: (context, state) {
return Text("is greater than zero? ${state}");
},
)
BlocProvider.of and context shortcuts
As anticipated, we can access our Blocs and Cubits from the UI using Provider
final cubit = BlocProvider.of<CounterCubit>(context);
Or more simply using context
// do not use it in the build method to access the state, use it to access the Bloc in callbacks
final cubit = context.read<CounterCubit>();
If we want the widget to be notified of state change
// use it in the build method to rebuild the widget on state changes, not in callbacks only to access the state
final cubit = context.watch<CounterCubit>();
In addition, context.select can be used to retrieve part of a state and react to changes only when the selected part changes.
// use it in build method to rebuild the widget on selected state changes, not in callbacks only to access the state
final isCounterGreaterThenZero = context.select((CounterBloc b) => b.state.value > 0);
Observe all blocs
BlocObserver can be used to observe all Blocs
class MyBlocObserver extends BlocObserver {
@override
void onCreate(BlocBase bloc) {
super.onCreate(bloc);
print('onCreate -- ${bloc.runtimeType}');
}
@override
void onEvent(Bloc bloc, Object? event) {
super.onEvent(bloc, event);
print('onEvent -- ${bloc.runtimeType}, $event');
}
@override
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
print('onChange -- ${bloc.runtimeType}, $change');
}
@override
void onTransition(Bloc bloc, Transition transition) {
super.onTransition(bloc, transition);
print('onTransition -- ${bloc.runtimeType}, $transition');
}
@override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
print('onError -- ${bloc.runtimeType}, $error');
super.onError(bloc, error, stackTrace);
}
@override
void onClose(BlocBase bloc) {
super.onClose(bloc);
print('onClose -- ${bloc.runtimeType}');
}
}
void main() {
BlocOverrides.runZoned(
() {
// Bloc instances will use MyBlocObserver instead of the default BlocObserver.
},
blocObserver: MyBlocObserver(),
);
}
Insights
Flutter dev - state management options
About Us
This article is part of the Mònade Developers Blog, written by Mattia Cecchini.