Flutter BLoC_Redux Pattern

Welcome back, At the second part, we learn state management using 2 methods (bloc and redux). Now in the third part, we learn how to combine bloc andRedux. Before you continue, you should read the second part first.

BLoC_Redux

Bloc_Redux is a combination of Bloc and Redux. OK, get it, but why?

Redux is an elegant pattern, which simplify the state management, and makes an unidirectional data flow. but it needs reducers return another state. by default it will make any widget using this state rebuild no matter if it has been modified. optimization can be made like diff to see the real changed properties, but not easy to do in flutter without reflection.

BLoC is a more general idea, UI trigger events using bloc’s sink method, bloc handle it then change stream, UI receive latest stream value to reflect the change. it’s vague on how to combine multi blocs or should there be only one per page? so it’s more an idea than a spec.

so to get the most of both, here comes Bloc_Redux.

Workflow

  • actions are the only way to change state.
  • blocs handle actions just like reducers but don’t produce new states.
  • widgets’ data comes from state’s stream, binded.

User trigger a button, it produces an action send to blocs, blocs which are interested in this action do some bussiness logic then change state by adding something new to stream, since streams are bind to widgets, UI are automatically updated.

First we need create base class for Bloc_Redux (Action, State, and Store)

/*
* Action
* Every action should extends this class
*/
abstract class BRAction<T>{
T payload;
}
/* State
*
* Input are used to change state.
* Usually filled with StreamController / BehaviorSubject
* handled by blocs.
*
* Implements disposable because stream controllers need to be disposed
* they will be called with in store's dispose method
*/
abstract class BRStateInput implements Disposable{}
/* Output are streams.
* followed by input. like someController.stream
* UI will use it as data source
*/
abstract class BRStateOutput{}
// State
abstract class BRState<T extends BRStateInput, U extends BRStateOutput>{
T input;
U output;
}
/* Bloc
* Like reducers in redux, but don't return a new state
* when they found something needs to change, just update state's input
* then state's output will change accordingly
*/
typedef Bloc<T extends BRStateInput> = void Function(BRAction action, T input);
// Store
abstract class BRStore<T extends BRStateInput, U extends BRState> implements Disposable{
List<Bloc<T>> blocs;
U state;
void dispatch(BRAction action){
blocs.forEach((f) => f(action, state.input));
}
@override
void dispose() {
state.input.dispose();
}
}

And then we need modify StreamBuilder for handling initial data.

// Useful when combined with StreamBuilder
class StreamWithInitialData<T> {
final Stream<T> stream;
final T initialData;
StreamWithInitialData(this.stream, this.initialData);
}

All base class is done, we create our StoreProvider

import 'package:flutter/material.dart';
import 'package:learn_redux/5_bloc_redux/core/blox_redux.dart';
Type _typeOf<T>() => T;class StoreProvider<T extends BRStore> extends StatefulWidget {
StoreProvider({
Key key,
@required this.child,
@required this.store,
}) : super(key: key);
final Widget child;
final T store;
@override
_StoreProviderState<T> createState() => _StoreProviderState<T>();
static T of<T extends BRStore>(BuildContext context) {
final type = _typeOf<_StoreProviderInherited<T>>();
_StoreProviderInherited<T> provider =
context.ancestorInheritedElementForWidgetOfExactType(type)?.widget;
return provider?.store;
}
}
class _StoreProviderState<T extends BRStore> extends State<StoreProvider<T>> {
@override
void dispose() {
widget.store?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return new _StoreProviderInherited<T>(
store: widget.store,
child: widget.child,
);
}
}
class _StoreProviderInherited<T> extends InheritedWidget {
_StoreProviderInherited({
Key key,
@required Widget child,
@required this.store,
}) : super(key: key, child: child);
final T store; @override
bool updateShouldNotify(_StoreProviderInherited oldWidget) => false;
}

All base preparation is done, we move to create our first Bloc_Redux. We create file that will handling Action, State, Bloc and Store.

import 'package:learn_redux/5_bloc_redux/core/blox_redux.dart';
import 'package:rxdart/rxdart.dart';
class CounterActionIncrement extends BRAction<int>{}class CounterStateInput extends BRStateInput{
final BehaviorSubject<int> counter = BehaviorSubject();
@override
void dispose() {
counter.close();
}
}
class CounterStateOutput extends BRStateOutput{
StreamWithInitialData<int> counter;
CounterStateOutput(CounterStateInput input){
counter = StreamWithInitialData(input.counter.stream, input.counter.value);
}
}
class CounterState extends BRState<CounterStateInput, CounterStateOutput>{
CounterState(){
input = CounterStateInput();
input.counter.add(0);
output = CounterStateOutput(input);
}
}
// Blocs
Bloc<CounterStateInput> counterIncrementHandler = (action, input){
if(action is CounterActionIncrement){
input.counter.add(input.counter.value + 1);
}
};
// Store
class CounterStore extends BRStore<CounterStateInput, CounterState>{
CounterStore(){
state = CounterState();
blocs = [counterIncrementHandler];
}
}

State is separated into input and output, input is used by blocs, if bloc find it's necessary to change state, it can add something new to stream. widgets will receive this change immediately by listening output.

And done, let’s us place our Store to Widget.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:learn_redux/5_bloc_redux/core/store_provider.dart';
import 'package:learn_redux/5_bloc_redux/store.dart';
class BlocReduxExample extends StatelessWidget{
@override
Widget build(BuildContext context) {
return StoreProvider(
store: CounterStore(),
child: new Scaffold(
appBar: new AppBar(
title: new Text('Example Bloc Redux'),
),
body: CounterWidget(),
floatingActionButton: AddButton()
),
);
}
}class CounterWidget extends StatelessWidget{
@override
Widget build(BuildContext context) {
final store = StoreProvider.of<CounterStore>(context);
final counter = store.state.output.counter;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
StreamBuilder<int>(
stream: counter.stream,
builder: (context, snapshot) {
return Text(
'${snapshot.data}',
style: Theme.of(context).textTheme.display1,
);
}
),
],
),
);
}
}
class AddButton extends StatelessWidget{
@override
Widget build(BuildContext context) {
final store = StoreProvider.of<CounterStore>(context);
return FloatingActionButton(
onPressed: (){
store.dispatch(CounterActionIncrement());
},
tooltip: 'Increment',
child: Icon(Icons.add),
);
}
}

Don’t forget to wrap our main widget with StoreProvider, and then at child widget can using Store class with StoreProvider. That concept like Bloc.

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

Happy Learning

Software Engineer Android