Flutter with BLoC and Redux

Lukas Kristianto
5 min readJul 24, 2019

Welcome back, At the first part, we learn state management using 2 methods (setState and ScopedModel). Now in the second part, we learn about BLoC and Redux, and how to combine them into one (BLoC_Redux).
Before you continue, you should read the first part first.

BLoC

At the first chapter, we know about handling state management local with the simple method (setState and ScopedModel). But setState and ScopedModel can’t handle for big project application and complex state.
Google recommends using BLoC method, it’s a powerful solution and it will help us to achieve a few things:

  • Separate business logic from view logic
  • Can using the asynchronous nature of UI Apps
  • Modular

The idea behind BLoC is very simple, there are using 3 components:

  • Sink<T> APIs to describe async input to our component
  • Stream<T> APIs to describe async outputs from our component
  • And then we can use StreamBuilder widget to manage the stream of data

Let’s create our first bloc component

class CounterBloc extends BlocBase{final _counter = new BehaviorSubject<int>.seeded(0);
final _addCounterController = StreamController();
CounterBloc(){
_addCounterController.stream.listen((data) =>
_counter.sink.add(_counter.value+1)
);
}
// expose data from stream
Stream<int> get counter => _counter.stream;
StreamSink get addCounter => _addCounterController.sink;@override
void dispose() {
_counter.close();
_addCounterController.close();
}
}

At the source code above we use Stream and Sink, and using StreamController() for handling event when addCounter function called. So when we subscribe stream we must dispose of the stream subscription and dispose of the counter object for clean the memory. For read more you about BLoC you can read here.

After create Bloc component, then we must use at view widget.

class BlocExamplePage extends StatelessWidget{
@override
Widget build(BuildContext context) {
final bloc = BlocProvider.of<CounterBloc>(context);
return new Scaffold(
appBar: new AppBar(
title: new Text('Example BLoC'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
StreamBuilder<int>(
stream: bloc.counter,
builder: (context, snapshot) {
return Text(
'${snapshot.data}',
style: Theme.of(context).textTheme.display1,
);
}
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
bloc.addCounter.add(null);
},
tooltip: 'Increment',
child: Icon(Icons.add),
)
);
}

}
StreamBuilder<int>(
stream: bloc.counter,
builder: (context, snapshot) {
return Text(
'${snapshot.data}',
style: Theme.of(context).textTheme.display1,
);
}
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
bloc.addCounter.add(null);
},
tooltip: 'Increment',
child: Icon(Icons.add),
)
);
}

}

Redux

Redux is a unidirectional data flow architecture that makes it easy to develop, maintain and test applications. In Redux there’s a Storewhich holds a Stateobject that represents the state of the whole application. Every application event (either from the user or external) is represented as an Actionthat gets dispatched to a Reducerfunction. This Reducerupdates the Storewith a new Statedepending on what Actionit receives. And whenever a new Stateis pushed through the Storethe Viewis recreated to reflect the changes.

Redux

With Redux most components are decoupled, making UI changes very easy to make. In addition, the only business logic sits in the Reducerfunctions. A Reduceris a function that takes an Actionand the current application Stateand it returns a new Stateobject, therefore it is straightforward to test because we can write a unit test that sets up an initial Stateand checks that the Reducerreturns the new and modified State.

At flutter you can use this package :

  • flutter_redux: this is a Flutter-specific package which provides additional components on top of the redux library which are useful for implementing Redux in Flutter, such as: StoreProvider (the base Widget for the app that will be used to provide the Store to all the Widgets that need it), StoreBuilder (a Widget that receives the Store from the StoreProvider) and StoreConnector (a very useful Widget that can be used instead of the StoreBuilder as you can convert the Store into a ViewModel to build the Widget tree and whenever the State in the Store is modified, the StoreConnectorwill get rebuilt).

Let’s us code with Redux Pattern

First we create our State, Reducer, and Store. As you know, these entities are global, but State is also immutable, which means that you have to recreate it whenever making any changes. In my particular implementation, AppState serves as host for the States of screen modules:

import 'package:flutter/material.dart';

class AppState{
final int count;
AppState({@required this.count});

AppState.initialState() : count = 0;
}

After we create AppState, then we need create AppReducer.

int incrementReducer(int state, action){
if(action == Actions.Increment) return state + 1;
return state;
}

AppState appStateReducer(AppState state, action){
return AppState(count: incrementReducer(state.count, action));
}

Each TypedReducer’s second argument is Actions. Actions are entities that change the AppState. There are likely to be many of them, as only Actions can trigger State changes. Usually, Actions are simple data classes that may or may not contain data. Here are some of the Actions I created for this case:

enum Actions{Increment}

We define a ViewModelclass that contains a view-specific representation of the data we need to display, as well as the actions the user can do. This ViewModelgets created from the Store:

class _ViewModel{
final Function() increment;
final int count;
_ViewModel({this.count, this.increment});

factory _ViewModel.create(Store<AppState> store) {
_onIncrement(){
store.dispatch(Actions.Increment);
}

return _ViewModel(
count: store.state.count,
increment: _onIncrement
);
}
}

Now we can use the ViewModelclass to display the to-do list. Notice that we wrap our Widgets inside a StoreConnectorwhich allows us to create the ViewModelfrom the Storeand build our UI using the ViewModel:

class ReduxExamplePage extends StatelessWidget{
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, _ViewModel>(
converter: (store) => _ViewModel.create(store),
builder: (context, _ViewModel viewModel) => new Scaffold(
appBar: new AppBar(
title: new Text('Example Redux'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${viewModel.count}',
style: Theme.of(context).textTheme.display1,
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => viewModel.increment(),
tooltip: 'Increment',
child: Icon(Icons.add),
)
),
);
}

}

This is a simple example but demonstrates the concepts explained above.

Part 3

You can clone my repository on public Github https://github.com/lukaskris/flutter_state_management/

Happy Learning

--

--

Lukas Kristianto

Senior Software Engineer Android and Artificial Intelligence