Architecture Patterns in Flutter
Wednesday Solutions
Posted on November 4, 2022
Flutter has an ever-increasing ecosystem of state management solutions. The flutter documentation itself lists out more than 10 options! It is a daunting task to figure out which solution to choose.
Before we get into any more details let's first get a very basic concept out of the way. Flutter is a declarative UI framework. That means we write how the UI should look given a particular state.
UI = f(*state)*
Keeping this in mind helps in writing better flutter code that plays well with the framework.
Now let's take a look at two of the most widely adopted state management patterns.
- Business Logic Component (BLoC) pattern
- Provider pattern
Note: This article assumes familiarity with flutter and the dart programming language.
Business Logic Component (BLoC)
Why was BLoC created?
Google introduced the BLoC pattern at the 2018 Google I/O developer conference. This pattern was originally used by the Google Ads team for their flutter app. It was created to handle complex state changes, which was difficult to do with the solutions present at that time.
What is BLoC?
BLoC is a direct implementation of the UI = f(state) concept. It combines this concept with a fundamental part of the dart language - Streams.
A stream is a sequence of asynchronous events. We can consider UI interactions within an app as a stream of events as well.
The BLoC receives these events, performs some processing on the events, and outputs a stream of states.
flutter_bloc is one such state management package that makes it easy to implement the BLoC pattern. Since everything is built on top of dart streams, it has great support for testing.
The flutter bloc package also has some companion packages that help in implementing some complex use cases such as restoring state and adding undo-redo functionality.
One thing to note is that BLoC is just a component that consumes events and emits states. We still need to use something to provide this BLoC in the widget tree. Usually, provider is used for this purpose.
How does BLoC work?
Let us create a BLoC for searching cities and displaying the list of search results.
- First, we create events that the BLoC will consume.
abstract class CitiesEvent {}
class SearchCities extends CitiesEvent {
String searchTerm;
SearchCities({
required this.searchTerm,
});
}
class SelectCity extends CitiesEvent {
int cityId;
SelectCity({
required this.cityId,
});
}
- Create the states that the UI will consume
abstract class CitiesState {}
class CitiesLoading extends CitiesState {}
class CitiesLoaded extends CitiesState {
List<City> cities;
CitiesLoaded({
required this.cities,
});
}
- Create the BLoC. Here we process the incoming events and return states.
class CitiesBloc extends Bloc<CitiesEvent, CitiesState> {
@override
Stream<CitiesState> mapEventToState(
CitiesEvent event,
) async* {
if (event is SearchCities) {
// Set state to Loading
yield CitiesLoading();
// Process the event
final citiesSearchResults = await fetchCities(event.searchTerm);
// Set the state to Loaded with the search results
yield CitiesLoaded(cities: citiesSearchResults);
}
// Handle other states
}
}
- To access the bloc in the widget tree, we provide it with BlocProvider
BlocProvider(
create: (_) => CitiesBloc(),
child: Column(
children: const [
CitiesSearchBar(),
CitiesListPage(),
],
),
),
- We retrieve the bloc provided above. Add new states to the BLoC from the UI. The BLoC will process these events and emit states in return.
@override
Widget build(BuildContext context) {
final CitiesBloc citiesBloc = Provider.of<CitiesBloc>(context);
return Padding(
padding: const EdgeInsets.only(left: 10.0),
child: TextFormField(
onChanged: (searchTerm) =>
citiesBloc.add(CitiesEvent.searchCities(searchTerm)),
),
);
}
- The UI will consume these states and update the view
BlocBuilder<CitiesBloc, CitiesState>(
builder: (context, state) {
if (state is CitiesLoading) return CircularProgressIndicator()
if (state is CitiesLoaded) return CitiesList(state.cities)
},
),
First we define the events that can occur within the app. We also defines the states the app is supposed to display.Then as the user interacts with the app, events get added to the BLoC which performs the necessary actions. The BLoC then emits states which are received by the UI.
In short, Events go into the BLoC and States come out of the BLoC.
Provider
Why was the Provider Pattern created?
BLoC was difficult to understand for many. Always using streams of data did not seem like an elegant solution in most use-cases. A simpler solution was needed to handle state in flutter.
The second problem was that providing some data down the widget tree was difficult to do using the InheritedWidget.
What is the Provider Pattern?
Although, we call this approach the provider pattern, the provider package in itself is just a wrapper around InheritedWidget with some added functionality. It can provide any type of object down the widget tree. The provider itself does not hold or mutate state. For that, we can use a ChangeNotifier. A ChangeNotifier is a class that provides listening capabilities to any class that extends it or uses it as a mixin.
Alternative to ChangeNotifier
Adding listeners to a change notifier is an O(1) operation. While removing listeners and dispatching updates to its listeners is an O(n) operation.
Instead of a change notifier, we can also use a State Notifier. state_notifier is a package created by the same author who created the provider package, so it integrates very well with it. It is more efficient than a change notifier and makes testing easier.
How does the provider pattern work?
Let's implement the same cities search use-case
- First we create a class that extends ChangeNotifier.
class CitiesSearchModel extends ChangeNotifier {
List<City> citiesList = [];
void searchCities(String searchTerm) async {
citiesList = await fetchCities(searchTerm);
notifyListeners();
}
}
- Similar to BLoC we first need to provide this class in the widget tree. A special type of provider call the ChangeNotifierProvider is used for this.
ChangeNotifierProvider<CitiesSearchModel>(
builder: (context) => CitiesSearchModel(),
child: CitiesListPage(),
);
- We then retrieve the the model class where we need and call the searchCities function. The model will do the necessary fetching of data and then call notifyListeners().
@override
Widget build(BuildContext context) {
final CitiesBloc citiesBloc = Provider.of<CitiesSearchModel>(context);
return Padding(
padding: const EdgeInsets.only(left: 10.0),
child: TextFormField(
onChanged: (searchTerm) =>
citiesBloc.searchCities(searchTerm),
),
);
}
- The UI will listen to the model and update the views. The consumer widget adds a listener to the model. Whenever notifyListeners() is called, the builder will be called again and the view will update.
Consumer<CitiesSearchModel>(
builder: (context, model, child) {
return CitiesList(model.citiesList);
},
)
When to use what
Any good state management solution should satisfy these basic requirements
- Separate the UI and business logic
- Make the code easier to test
- Make the code more readable
Both of these approaches satisfy all three requirements. But each has its own set of drawbacks.
The BLoC pattern has a steep learning curve. It adds a lot of boilerplate to the codebase.
The provider pattern gets slower as we add more listeners. The ideal solution would be a combination of both. We need to pick the right tool for the job at hand.
BLoC is very powerful with streams as its backbone. But you won't call a fire truck to put out a campfire.
Use BLoC in situations where you need the capabilities it provides. Such as having more than 2-3 listeners active at a time or maintaining and navigating through the history of state changes or debouncing and transforming the incoming events.
Use the provider pattern for most if not all of your state management needs. It's a more than capable solution and you may never need to use BLoC.
Recent Developments
With both approaches becoming more mature over the past few years, there have been some updates to these patterns that make it easier to work with them.
BLoC is now a subclass of the Cubit class. Cubit works the same as BLoC but it attempts to reduce some of the boilerplate that we need to write with BLoC by replacing event classes with simple functions.
The creator of the provider package has created a new package called riverpod. The tag line on its website reads "Provider, but different". It works on the same concepts as the provider but tries to fix some pitfalls of the original package. But the original provider package is still the most popular and the recommended solution in official flutter documentation.
Where to go from here
Head over to the Flutter Template on Wednesday's GitHub to view the complete implementation of the examples we saw in this article.
This article was originally posted on the Wednesday Solutions blog. You can check out the original article here.
Posted on November 4, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.