Architecture Patterns in Flutter

wednesdaysol

Wednesday Solutions

Posted on November 4, 2022

Architecture Patterns in Flutter

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.

  1. Business Logic Component (BLoC) pattern
  2. 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.

  1. 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,
  });
}
Enter fullscreen mode Exit fullscreen mode
  1. 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,
  });
}
Enter fullscreen mode Exit fullscreen mode
  1. 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
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. To access the bloc in the widget tree, we provide it with BlocProvider
BlocProvider(
  create: (_) => CitiesBloc(),
  child: Column(
    children: const [
      CitiesSearchBar(),
      CitiesListPage(),
    ],
  ),
),
Enter fullscreen mode Exit fullscreen mode
  1. 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)),
    ),
  );
}
Enter fullscreen mode Exit fullscreen mode
  1. 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)
  },
),
Enter fullscreen mode Exit fullscreen mode

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

  1. 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();
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. 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(),
);
Enter fullscreen mode Exit fullscreen mode
  1. 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),
    ),
  );
}
Enter fullscreen mode Exit fullscreen mode
  1. 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);
  },
)
Enter fullscreen mode Exit fullscreen mode

When to use what

Any good state management solution should satisfy these basic requirements

  1. Separate the UI and business logic
  2. Make the code easier to test
  3. 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.

💖 💪 🙅 🚩
wednesdaysol
Wednesday Solutions

Posted on November 4, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

What was your win this week?
weeklyretro What was your win this week?

November 29, 2024

Where GitOps Meets ClickOps
devops Where GitOps Meets ClickOps

November 29, 2024