Flutter with BLoC and Redux
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 componentStream<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 Store
which holds a State
object that represents the state of the whole application. Every application event (either from the user or external) is represented as an Action
that gets dispatched to a Reducer
function. This Reducer
updates the Store
with a new State
depending on what Action
it receives. And whenever a new State
is pushed through the Store
the View
is recreated to reflect the changes.
With Redux most components are decoupled, making UI changes very easy to make. In addition, the only business logic sits in the Reducer
functions. A Reducer
is a function that takes an Action
and the current application State
and it returns a new State
object, therefore it is straightforward to test because we can write a unit test that sets up an initial State
and checks that the Reducer
returns 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 theredux
library which are useful for implementing Redux in Flutter, such as:StoreProvider
(the baseWidget
for the app that will be used to provide theStore
to all theWidget
s that need it),StoreBuilder
(aWidget
that receives theStore
from theStoreProvider
) andStoreConnector
(a very usefulWidget
that can be used instead of theStoreBuilder
as you can convert theStore
into aViewModel
to build theWidget
tree and whenever theState
in theStore
is modified, theStoreConnector
will 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 ViewModel
class that contains a view-specific representation of the data we need to display, as well as the actions the user can do. This ViewModel
gets 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 ViewModel
class to display the to-do list. Notice that we wrap our Widget
s inside a StoreConnector
which allows us to create the ViewModel
from the Store
and 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