BloC Architecture in Flutter: a Modern Architectural Approach and How We Use it at Jimdo

It’s always a challenge to choose the right architecture for a mobile app. When we started using Flutter, we were experimenting with a new framework that hadn’t been tested much by the development community.

For our Boost app, we started by using MVP because our Android developers had some experience with it and it was quite easy to catch. But once the codebase started to grow, we discovered it wasn’t the best choice. Our presenter classes were too big, we weren’t reusing a lot of code and it didn’t fit well the reactive nature of Flutter by containing lots of state and managing it all in presenter. We considered trying Redux but decided on BloC, because it’s lightweight and doesn’t require so much boilerplate code to set up the data-flow pipeline or the use of an external library, which is also pretty raw feature-wise comparing to React or Vue.js.

What is BloC?

BloC (Business Logic Component) is an architecture pattern introduced by Google at their IO conference this year. The idea behind it is to have separated components (BloC components) containing only the business logic that is meant to be easily shared between different Dart apps.

Example project

To demonstrate the implementation of BloC, I created a simple Flutter application which makes one network request (fetching the weather forecast for Hamburg) and shows how the temperature is going to change during the upcoming hours in a ListView. Here’s a link to the project.

Building Blocks of BloC

In this section, I’m going to explain the main building components of BloC in Flutter. Let's start with a diagram to show how components are connected:

BloC component

At the core of BloC architecture, is the BloC component. It’s the place where the business logic is happening. In my example app, it’s fetching the weather forecast from the API client and returning the data via Dart’s Stream. Pushing new events to the listener is handled by StreamController (if you’re familiar with BehaviorSubject or PublishSubject in Rx, it fulfills a similar role) and Stream is exposed as a get-only variable. Also, it’s worth noting that we use broadcast here to enable multiple listeners.

final _forecastController = StreamController.broadcast();
Stream get forecastStream => _forecastController.stream;

ForecastBlocState contains the forecast object and tracks if the request is already happening.

class ForecastBlocState {
 bool loading;
 Forecast forecast;

 ForecastBlocState(this.loading, this.forecast);

 ForecastBlocState.empty() {
   loading = false;
   forecast = null;
 }
}

The main method of this component - fetchForecastForCity, executes API request and publishes the state changes to the listener. StreamSubscription is used to not to execute more than one request at a time. _forecastController publishes the state changes to the listener with add method.

fetchForecastForCity(String city) {
 _fetchForecastSub?.cancel();

 _currentState.loading = true;
 _forecastController.add(_currentState);

 _apiClient.requestForecastForCity(city)
     .asStream()
     .listen((dynamic forecast) {
       if (forecast is Forecast) {
         _currentState.forecast = forecast;
       }
       _currentState.loading = false;
       _forecastController.add(_currentState);
 });
}

Tips:

  • Use StreamController in conjunction with Streams (to listen to state changes) and Sinks (to change the state)
  • Keep the class simple and reusable. I’m personally not a big fan of using RxDart, here. Instead of doing a lot of data manipulation, I do this inside State, ViewModel or Presenter—depending which additional architecture is used in the project.

BloC provider

To inject the BloC component into the Screen / Widget, we use a class called InheritedWidget. This is a cheap and elegant way for dependency injection in Flutter. Every child of InheritedWidget will have an access to the components that InheritedWidget provides. So, in the case of ForecastBloc, we create a subclass of InheritedWidget called ForecastBlocProvider and implement it like this:

class ForecastBlocProvider extends InheritedWidget {
 final ForecastBloc bloc;
 final Widget child;

 ForecastBlocProvider({this.bloc, this.child}) : super(child: child);
 static ForecastBlocProvider of(BuildContext context) =>
     context.inheritFromWidgetOfExactType(ForecastBlocProvider);

 @override
 bool updateShouldNotify(InheritedWidget oldWidget) {
   return true;
 }
}

Tips:

  • Often, more than one BloC component needs to be injected. I implemented this by making one the child of another. This isn’t a perfect solution because it introduces unnecessary hierarchy here, so if you’ve discovered a better way to do this please let me know in the comments.

Binding everything together (StreamBuilder & InheritedWidget)

To make the BloC component available for the screen widget we have to make the screen widget to be a child of InheritedWidget:

final bloc = ForecastBloc();

ForecastBlocProvider(
 bloc: bloc,
 child: WeatherListScreen(
   title: 'Weather in Hamburg',
 ),
)

The BloC component is then accessible to all the child widgets in the tree:

ForecastBloc bloc = ForecastBlocProvider.of(context).bloc;

To listen to the Stream’s state changes and rebuild the widget’s tree based on that there’s a handy class called StreamBuilder. It should have the same generic type as a BloC’s stream. We can connect it like this:

return Scaffold(
 appBar: AppBar(
   title: Text(widget.title),
 ),
 body: StreamBuilder(
     initialData: bloc.getCurrentState(),
     stream: bloc.forecastStream,
     builder: _buildBody
 ),
);

Tips:

  • I prefer to make the Screen widget a stateful widget and all the child View widget stateless. This way, they only implement the rendering and all the state handling logic happens in the top-component.

BloC: The Negatives

This development process wasn’t without its problems. Stream API is not always easy to catch and the wrong implementation can lead to memory-leaks and crashes. For example, it wasn’t obvious whether we should use broadcasting or not. Documentation sometimes lacks details, like in this issue. And an app with many screens and complicated routing might have problems because there’s no easy way to subscribe to or unsubscribe from StreamBuilder events.

Final words

Flutter is a new framework and Dart is a relatively new language. There’s definitely room for improvement but, on the flipside, it’ll also give you a chance to experiment.

 

But having a shared codebase for mobile (iOS & Android) and web apps is already reason enough to give it a try. BloC is probably one of the best ways to implement this and with Streams and Sinks you can also achieve that in a Reactive way.

 

I’m looking forward to hearing your thoughts on this one. If you’ve used BloC, do you like it? How could do to improve with our approach? Here’s the repository link:
https://github.com/artem888/bloc_example/

Please feel free to share your feedback below and let me know if you found this helpful!

Here’s a bit more about how we use Flutter at Jimdo:
https://dev.jimdo.com/flutter-at-jimdo-our-experience-and-fun-moments/

 

Happy Fluttering!

Write a comment

Comments: 2
  • #1

    Patte (Tuesday, 11 September 2018 16:06)

    Interesting and very good read!
    I just don't understand the purpose of the BlocState class and it feels kind of redundant to me.

    Can you tell me more about its purpose? Apart from that really clear and would like to see more blog posts of you regarding architecture on Flutter. :)

  • #2

    Jimdo (Wednesday, 19 September 2018 11:01)

    Hello Patte!

    Thank you very much for your comment. I agree that it might be redundant. There are probably better ways to track if request is happening - i.e. making a separate stream, which I tried as well, but had some problems with nested StreamBuilders and another one is to track it outside of the BloC component, which will make the BloC component even simpler. I've chosen the one with BlocState, because I thought it will be easier to grasp and I thought more from the UI perspective.