Flutter State Management with BLoC Pattern

29 Mar, 2020 | 8 minutes read

When developing an application, one of the most important things is state management. You might ask why? Well, state development helps you in keeping the code clean, avoiding accruing technical debt, and providing an easy way of sharing states through the widgets by being a powerful Jedi developer. So, how can you do state management in Flutter?

In our blog post, we are going to talk about Business Logic Component (BLoC) pattern as one of the ways for state management.

What is BLoC pattern?

A BLoC pattern is a pattern created by Google and announced at Google I/0’18. It uses reactive programming to handle data flow within the app and helps developers to separate states from the UI. BloC basically is a box where events come in from one side and states come out from another side. Or simpler events go in the BloC and states go out from the BLoC. BLoC operates on the events to figure out which state it should output.

Flutter State Management with BLoC Pattern

BLoC contains two main components: Sink and Stream. Both are provided from StreamController who can be accessed from dart:async library. We add streams of data inputs into the Sink and listen for them as stream data output through a Stream.

Implementing the BLoC pattern

  • Creating Events

First of all, we create Events who will come into the BLoC. 

 abstract class MovieEvent {}
 
class MovieCategorySelectedEvent extends MovieEvent {
   final String movieCategory;

   MovieCategorySelectedEvent(this.movieCategory);
 }

As we can see from the code above, we have created an abstract class called MovieEvent which is the base class that all events will inherit. We have one event called MovieCategorySelectedEvent which is fired when the user changes the movie category. This event inherits MovieEvent and has one field of type string called movieCategory.

  • Creating BLoC

Next, we create a BLoC. A BLoC is a simple Dart class.

import 'dart:async';

 class MovieBloc {
   ...

   final _movieEventController = StreamController<MovieEvent>();
   StreamSink<MovieEvent> get inMovieEvent => _movieEventController.sink;

   final _movieCategoryesStateController = StreamController<List<Movie>>();
   StreamSink<List<Movie>> get _inMovies => _movieCategoryesStateController.sink;
   Stream<List<Movie>> get outMovies => _movieCategoryesStateController.stream;

   final _dropDownValueStateController = StreamController<String>();
   StreamSink<String> get _inDropDownValue => _dropDownValueStateController.sink;
   Stream<String> get outDropDownValue => _dropDownValueStateController.stream;

   MovieBloc(){
    _movieEventController.stream.listen(_mapEventToState);
    ...
   }

   void _mapEventToState(MovieEvent event){
     if(event is MovieCategorySelectedEvent){
       _onMovieCategorySelected(event.movieCategory);
     }
   }

   void _onMovieCategorySelected(String newValue) async {
     if(newValue != dropdownValue){
       dropdownValue = newValue;
       _inDropDownValue.add(dropdownValue);

       final _response = await _repo.getMoviesForCategory(newValue);
       if(_response != null){
         _inMovies.add(_response.movies);
       }
     }
   }

 void dispose(){
    _movieEventController.close();
    _movieCategoryesStateController.close();
    _dropDownValueStateController.close();
  }

  ...

 }

In our case, the BLoC class is named MovieBloc. It contains 3 StreamController’s, which come from the dart:async library.

The first StreamController is named _movieEventConttroler and exposes only the Sink for events that enter into BloC. In the constructor of MovieBloc we listen for the outputs of _movieEventConttroler.stream and then we map events that come from Stream into an appropriate state in a simple function called _mapEventToState which accepts events of type MovieEvent.

The second StreamController named _movieCategoryesStateController, is used for updating the list of movies when the movie category is changed. We create a private getter for the Sink of this controller with the name _inMovies, and public getter for Stream for this controller with name outMovies.

The third StreamController named _dropDownValueStateController, is for the currently selected movie category. We create a private getter for the Sink of this controller with the name _inDropDownValue, and public getter for Stream for this controller with the name outDropDownValue.

In the dispose() function we close all 3 StreamController’s by calling the close() function on each of them.

  • Creating the UI

To access the data from Streams we use StreamBuilder widget, we pass the Streams to the stream property of StreamBuilder, and we access the stream data in the builder function of the StreamBuilder.

class _MoviePageState extends State<MoviePage> {
   MovieBloc _movieBloc;

   _MoviePageState(){
     _movieBloc = MovieBloc();
   }

   @override
   Widget build(BuildContext context) {
     return Scaffold(
       appBar: AppBar(
         title: Text(
           'Movie Database'
         ),
       ),
       body: _getBody(),
     );
   }

   Widget _getBody(){
     return Column(
       mainAxisAlignment: MainAxisAlignment.start,
       mainAxisSize: MainAxisSize.max,
       crossAxisAlignment: CrossAxisAlignment.stretch,
       children: <Widget>[
         StreamBuilder(
           stream: _movieBloc.outMovies,
           initialData: null,
           builder: (BuildContext context, AsyncSnapshot<List<Movie>> snapshot){
             if(snapshot.data != null){
               if(snapshot.data.length > 0){
                 return SingleChildScrollView(
                   scrollDirection: Axis.horizontal,
                   child: Row(
                     crossAxisAlignment: CrossAxisAlignment.start,
                     children: snapshot.data.map<Widget>((movie){
                       return _getViewCell(movie);
                     }).toList(),
                   )
                 );
               }
                                ...
           },
         ),
Expanded(
           child: StreamBuilder(
             stream: _movieBloc.outDropDownValue,
             initialData: _movieBloc.dropdownValue,
             builder: (BuildContext context, AsyncSnapshot<String> snapshot){
               return Container(
                 child: Center(
                   child: DropdownButton<String>(
                     value: snapshot.data,
                     elevation: 16,
                     style: const TextStyle(
                       color: Colors.black54
                     ),
                     underline: Container(
                       height: 2,
                       color: Colors.black54,
                     ),
                     onChanged: (newValue)
 => _movieBloc.inMovieEvent.add(MovieCategorySelectedEvent(newValue)),
                     items: _movieBloc.movieCategories
 .map<DropdownMenuItem<String>>((MovieCategory value) {
                       return DropdownMenuItem<String>(
                         value: value.value,
                         child: Text(
                           value.label
                         ),
                       );
                     }).toList(),
                   )
                )
               );
             },
           )
         )
       ],
     );
   }

   @override
   void dispose(){
     super.dispose();
     _movieBloc.dispose();
   }

    ...

 }

To avoid memory leaks don’t forget to call the dispose() function on _movieBloc inside the dispose() function in widget state.

Running the app

Running the app

When we run the application we see the list of movies for a selected movie category. By clicking on the dropdown button we can change the movie category and get movies for that selected category.

Running the app
Running the app