Flutter state management using the BLoC pattern

Posted on Sun Mar 31 2019

I have been learning mobile development with Flutter for a while and throughout my journey, I have learned how to manage the state using the BLoC pattern and RxDart.

Flutter state management using the BLoC pattern

Learning Flutter was not that hard for me, the docs are great, lots of helpful articles and videos, and for a javascript developer learning dart was not that hard.

Building an app with Flutter is really fun, but it can turn into a mess when you have lots of pages and nested widgets, then it will be a pain to keep track of all the states and pass them around to other widgets that make use of them.

I have been there and I had to deal with this mess at some point, and that's when I stopped building the app and started my research to find solutions that will make Flutter development maintainable and less painful.

Reactive Programming

You might have heard about this a lot during your career as a developer. Reactive programming is everywhere now, from javascript, java, swift, dart, C#, and almost any other language.

Reactive programming is programming using events instead of the order of lines in the code. This is accomplished by using observables to emit a stream of data or events and an observer (subscriber) which acts upon the emitted data or event.

If you have used angular before this might seem familiar to you because angular uses RxJS which is the implementation of reactive programming in javascript.

There are two ways to implement reactive programming in Flutter:

  • Using Flutter's built-in Streams
  • Using the RxDart package

According to RxDart's official GitHub repository:

Dart comes with a very decent Streams API out-of-the-box; rather than attempting to provide an alternative to this API, RxDart adds functionality on top of it.

Before we dive into the RxDart package, I'm gonna give you a brief introduction to streams.

What is a stream?

A stream is like a pipe with two ends, one end is the entrance and the other end is the exit. When data enters the stream, it flows to the other end of the stream where the subscriber receives it.

The advantage of using a stream over simply passing a variable is that you can keep sending data using it and every time you do, the widget that is subscribed to this stream will re-render.

Using streams in Flutter

To use streams in Flutter you need a stream controller. A stream controller gives you access to two main properties, the sink which is the entrance of data, and the stream which is well the stream.

import 'dart:async';

// Create a new StreamController instance
final StreamController ctrl = StreamController();

// Add data to the stream using the sink property of the controller
ctrl.sink.add('Hello World');
ctrl.sink.add(458712);
final user = User(
    name: 'Hassan Saleh',
    age: 25,
);
ctrl.sink.add(user);

// Subscribe to the stream
final StreamSubscription subscription = ctrl.stream.listen((data) => print(data));

ctrl.sink.add('I\'m Late to the party');

ctrl.close();

As you can see a stream can transfer any type of data and they all get received by the subscriber.

By default, a stream controller can have one subscriber, but it can be used to broadcast a stream to multiple subscribers.

final StreamController ctrl = StreamController.broadcast();

And can also be used to transfer a single type of data

final StreamController<int> ctrl = StreamController<int>.broadcast();

A stream subscriber can filter incoming data by using a condition

  final StreamSubscription subscription = ctrl.stream
      .where((value) => (value == someCondition))
    .listen((value) => print(value));

Stream controllers must always be closed when not needed anymore to prevent memory leaks.

Using RxDart

RxDart has 3 main types of StreamControllers, all of them are broadcast StreamControllers and they all return an Observable instead of a stream but they have some differences.

  1. Publish Subject: Publish Subject provides subscribers with data that gets added to stream the after they have subscribed.
  2. Behavior Subject: Behavior Subject provides subscribers with the last piece of data that has been added to the stream before they have subscribed in addition to new data that gets added.
  3. Replay Subject: Replay Subject provides subscribers with all the data that has been added to the stream before they have subscribed in addition to new data that gets added.

Example:

import 'dart:async';
import 'package:rxdart/rxdart.dart';

// Create a new instance of a Behavior Subject
// .seeded(true) sets the initial value of the stream to true
BehaviorSubject<bool> _isLoading = BehaviorSubject<bool>.seeded(true);

// Subscribe to the stream
final StreamSubscription subscription = _isLoading.stream.listen((data) => print(data));

// Add data to the stream
_isLoading.add(false);

// Close the Behavior Subject
_isLoading.close();

As you can see, RxDart is very similar to Flutter's StreamController and that's because it's built on top of it.

The RxDart subjects give us access to lots of features that are not found in the original StreamController and you can learn about them here.

We can get the current value of the Subject without listening to the stream by using the value method.

bool currentValue = _isLoading.value;

This will be very helpful when you have a parent widget that gets rebuilt when the stream broadcast a new value and a child widget that also needs access to this value. The child is also getting rebuilt so there's no need for it to also listen to the stream, it only needs access to the current value.

A more complete example of using streams is by creating a class like so

class Counter {
    BehaviorSubject<int> _count = BehaviorSubject<int>.seeded(0);

    Observable<int> get stream => _count.stream;
    int get count => _count.value;

    void increment() {
        _count.add(count + 1);
    }

    void decrement() {
        _count.add(count - 1);
    }

    void dispose() {
        _count.close();
    }
}

Now you can create an instance of this class and use it your widget to get and manipulate the count.

Using a stream inside a widget

To listen to a stream inside a widget, you can use Flutter's StreamBuilder inside a stateless widget.

import 'package:flutter/material.dart';
import './Counter.dart';

const counter = new Counter();

class MyCounter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
      stream: counter.stream,
      builder: (BuildContext context, AsyncSnapshot snapshot) {
        if (snapshot.hasData) {
          return Column(
              children: [
                  Text(`${snapshot.data}`),
                  FlatButton(
                      child: Text('Increment'),
                      onPressed: () => counter.increment(),
                  ),
                  FlatButton(
                      child: Text('Decrement'),
                      onPressed: () => counter.decrement(),
                  ),
              ],
          );
        } else {
          return CircularProgressIndicator();
        }
      },
    );
  }
}

This specific approach is just for demonstrating the StreamBuilder, remember we always have to close a StreamController. We will use a better approach later in this article.

A StreamBuilder takes a stream and a builder method that gets executed every time a new value is received. The builder method provides a context and a snapshot which holds information about the new value like the hasData method which tells if the value received is not null.

In this case, the stream always has data because we initialized it with a value of 0 and the BehaviorSubject will return the last value added, so the progress indicator will never be shown. I just wanted to give you an idea about the snapshot.

This is called the BLoC pattern (Business Login Component), as you can see in the above example all the business logic has been moved outside the widget, and the state (which is the Counter class in this case) can now be accessed from other widgets in the file.

But, there are still have some problems that need fixing, the BehaviorSubject must be closed and one instance of it should be accessible by all nested widgets.

Application State

Now that you have a clear understanding of the BLoC pattern and how to move the state outside the widget, I'm going to show you how to share this state between multiple widgets across your app.

To do this, Flutter has a third type of widgets other than stateful and stateless, and it is the inherited widget.

You have probably come across the of method in your journey using Flutter when accessing the theme of the app or getting the size of the screen.

Theme.of(context).primarySwatch;
MediaQuery.of(context).size;

Theme and MediaQuery are both inherited widgets and you can access them anywhere in your app.

And we want to access the BLoC class the same way.

Counter.of(context).stream;

But, there's a small problem. The inherited widget doesn't provide a dispose method like the stateful widget. So, we can't dispose of the StreamController or BehaviorSubject from inside an inherited widget.

That's why I use a stateful widget with an inherited widget as a child. The stateful widget is responsible for disposing of the controller and the inherited widget will provide the class to all nested widgets.

I wouldn't have figured this out without the help of Didier Boelens, check out his article about the BLoC pattern here.

To accomplish this, we need to create a stateful widget called BlocProvider which takes two arguments a child widget and a bloc. Inside this widget's build function, we return an inherited widget and pass it the child and the bloc;

import 'package:flutter/widgets.dart';

Type _typeOf<T>() => T;

abstract class BlocBase {
  void dispose();
}

class BlocProvider<T extends BlocBase> extends StatefulWidget {
  BlocProvider({
    Key key,
    @required this.child,
    @required this.bloc,
  }) : super(key: key);

  final Widget child;
  final T bloc;

  @override
  _BlockProviderState<T> createState() => _BlockProviderState<T>();

  static T of<T extends BlocBase>(BuildContext context) {
    final type = _typeOf<_BlocProviderInherited<T>>();
    _BlocProviderInherited<T> provider =
        context.ancestorInheritedElementForWidgetOfExactType(type).widget;
    return provider.bloc;
  }
}

class _BlockProviderState<T extends BlocBase> extends State<BlocProvider<T>> {
  @override
  void dispose() {
    widget.bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: _BlocProviderInherited<T>(
        child: widget.child,
        bloc: widget.bloc,
      ),
    );
  }
}

class _BlocProviderInherited<T> extends InheritedWidget {
  _BlocProviderInherited({
    Key key,
    @required Widget child,
    @required this.bloc,
  }) : super(key: key, child: child);

  final T bloc;

  @override
  bool updateShouldNotify(_BlocProviderInherited oldWidget) => false;
}

This might seem overwhelming at first glance, but it's just a stateful widget with a bloc parameter that must extend a class called BlocBase which will ensure that the bloc has a dispose method.

BolcProvider also has an of method that looks for an inherited widget that has a bloc with the same type passed to it (T).

Now you can modify the Counter class to extend BlocBase

import './BlocProvider.dart';
class Counter extends BlocBase {
    // ...
}

And you can use the BlocProvider to share this class in your app

import 'package:flutter/material.dart';
import './BlocProvider.dart';
import './Counter.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Counter',
        theme: ThemeData(
            primarySwatch: Colors.blue,
        ),
        home: BlocProvider(
            bloc: Counter(),
            child: MyCounterWidget(),
        ),
    );
  }
}

class MyCounterWidget extends StatelessWidget {
    @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: StreamBuilder(
          stream: BlocProvider.of<Counter>(context).stream,
          builder: (BuildContext context, AsyncSnapshot snapshot) {
            if (snapshot.hasData) {
              return Column(
                  children: [
                      Text(`${snapshot.data}`),
                      FlatButton(
                          child: Text('Increment'),
                          onPressed: () => BlocProvider.of<Counter>(context).increment(),
                      ),
                      FlatButton(
                          child: Text('Decrement'),
                          onPressed: () => BlocProvider.of<Counter>(context).decrement(),
                      ),
                  ],
              );
            } else {
              return CircularProgressIndicator();
            }
          },
        ),
    );
  }
}

And just like that, you can use the Counter class anywhere in your app.

Any nested widget below this one that needs access to the count value can use the getter method we defined in the Counter class without the need to subscribe to the stream.

BlocProvider.of<Counter>(context).count

You can also use multiple BLoCs using this provider.

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Counter',
        theme: ThemeData(
            primarySwatch: Colors.blue,
        ),
        home: BlocProvider(
            bloc: Counter(),
            child: BlocProvider(
                bloc: AnotherBloc(),
                child: MyCounterWidget(),
            ),
        ),
    );
  }
}

Then access each of them the same way we did before

BlocProvider.of<Counter>(context)
BlocProvider.of<AnotherBloc>(context)

References

What is reactive programming

Reactive Programming — Streams — BLoC

RxDart

List of state management approaches

Reactive Programming - Streams - BLoC - Practical Use Cases

Introduction to declarative UI

If you feel that you have learned anything from this article, don't forget to share it and help other people learn.