Architecture Flutter App the Bloc_Redux Way

limboy
5 min readJan 5, 2019

--

here’s the github link: https://github.com/lzyy/bloc_redux

After using flutter for some time, it seems there are no the go-to way to handle state and manage data flow. well, there are, one is flutter_redux, and the other is flutter_bloc. Let’s talk flutter_redux first.

It’s kind of the official implementation of redux. mainly combined with two parts: StoreProvider and StoreConnector. the former used to hold Store, the latter used to react to new state.

// Every time the button is tapped, an action is dispatched and
// run through the reducer. After the reducer updates the state,
// the Widget will be automatically rebuilt with the latest
// count. No need to manually manage subscriptions or Streams!
new StoreConnector<int, String>(
converter: (store) => store.state.toString(),
builder: (context, count) {
return new Text(
count,
style: Theme.of(context).textTheme.display1,
);
},
)

the problem of StoreConnector is whenever state updated, it will be triggered. even the state contains 10 properties, and only 1 bool property is updated.

// Creates the base [NextDispatcher].
//
// The base NextDispatcher will be called after all other middleware provided
// by the user have been run. Its job is simple: Run the current state through
// the reducer, save the result, and notify any subscribers.
NextDispatcher _createReduceAndNotify(bool distinct) {
return (dynamic action) {
final state = reducer(_state, action);
if (distinct && state == _state) return;_state = state;
_changeController.add(state);
};
}

that’s the redux notify mechanism. as long as the state are not equal, _changeController is notified. in react-redux, there’s some optimization, using connect's mapStateToProps , the component will be rerender only when the props it care updated. but flutter_redux can’t do this.

now let’s dive into flutter_bloc, before talk about it, let’s talk about bloc, which is promoted by google fellows.

it’s more of an idea rather than spec or protocol. flutter_bloc is an implementation of bloc. after looking into the code, it seems not quite different than redux, but added some concepts like BlocSupervisor BlocDelegate Transaction , make it more complex. state handling is no different than redux, even worse, without == check.

void _bindStateSubject() {
Event currentEvent;
(transform(_eventSubject) as Observable<Event>).concatMap((Event event) {
currentEvent = event;
return mapEventToState(_stateSubject.value, event);
}).forEach(
(State nextState) {
final transition = Transition(
currentState: _stateSubject.value,
event: currentEvent,
nextState: nextState,
);
BlocSupervisor().delegate?.onTransition(transition);
onTransition(transition);
_stateSubject.add(nextState);
},
);
}

nextState is directly add to _stateSubject without checking equal. meanwhile author seems to encourage using multi blocs which is not handy to use. (which bloc should i dispatch to?)

return BlocBuilder<LoginEvent, LoginState>(
bloc: widget.loginBloc,
builder: (
BuildContext context,
LoginState loginState,
) {
if (_loginSucceeded(loginState)) {
widget.authBloc.dispatch(Login(token: loginState.token));
widget.loginBloc.dispatch(LoggedIn());
}
}
);

So, these two libraries are not suited for my needs, let’s make another wheel!

Bloc_Redux

the problem with flutter_redux is its state handling. reducer return a new state, and it’s hard to separate modified properties, even it can, there should be a map to hold these relationships, too complicated.

Flutter already provides StreamBuilder , let widgets listen the streams they like, when an action dispatched, change these streams content. that’s it.

the reducer’s role is replaced by bloc.

/// 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 needs to be disposed.
/// they will be called within 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
///
/// Combine these two into one.
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
///
/// widget use `store.dispatch` to send action
/// store will iterate all blocs to handle this action
///
/// if this is an async action, blocs can dispatch another action
/// after data has received from remote.
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));
}
dispose() {
state.input.dispose();
}
}

that’s the core code. State is separated into StateInput and StateOutput , StateInput is handled by bloc, StateOutput is consumed by widgets to receive latest data.

Store has a dispose method, because it will be added to StoreProvider, when it disposed, the streams can be closed safely. Let’s see a demo

/// Actions
class ColorActionSelect extends BRAction<Color> {}
/// State
class ColorStateInput extends BRStateInput {
final BehaviorSubject<Color> selectedColor = BehaviorSubject();
final BehaviorSubject<List<ColorModel>> colors = BehaviorSubject();
dispose() {
selectedColor.close();
colors.close();
}
}
class ColorStateOutput extends BRStateOutput {
StreamWithInitialData<Color> selectedColor;
StreamWithInitialData<List<ColorModel>> colors;
ColorStateOutput(ColorStateInput input) {
selectedColor = StreamWithInitialData(
input.selectedColor.stream, input.selectedColor.value);
colors = StreamWithInitialData(input.colors.stream, input.colors.value);
}
}
class ColorState extends BRState<ColorStateInput, ColorStateOutput> {
ColorState() {
input = ColorStateInput();
output = ColorStateOutput(input);
}
}
/// Blocs
Bloc<ColorStateInput> colorSelectHandler = (action, input) {
if (action is ColorActionSelect) {
input.selectedColor.add(action.payload);
var colors = input.colors.value
.map((colorModel) => colorModel
..isSelected = colorModel.color.value == action.payload.value)
.toList();
input.colors.add(colors);
}
};
/// Store
class ColorStore extends BRStore<ColorStateInput, ColorState> {
ColorStore() {
state = ColorState();
blocs = [colorSelectHandler];
// init
var _colors = List<ColorModel>.generate(
30, (int index) => ColorModel(RandomColor(index).randomColor()));
_colors[0].isSelected = true;
state.input.colors.add(_colors);
state.input.selectedColor.add(_colors[0].color);
}
}

Store is like brain, it receives actions and make blocs handle these actions to change streams accordingly. widgets will receive streams’ latest data.

class ColorsWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final store = StoreProvider.of<ColorStore>(context);
final colors = store.state.output.colors;
return StreamBuilder<List<ColorModel>>(
stream: colors.stream,
initialData: colors.initialData,
builder: (context, snapshot) {
final colors = snapshot.data;
return SliverGrid.count(
crossAxisCount: 6,
children: colors.map((colorModel) {
return GestureDetector(
onTap: () {
store.dispatch(
ColorActionSelect()..payload = colorModel.color);
},
child: Container(
decoration: BoxDecoration(
color: colorModel.color,
border: Border.all(width: colorModel.isSelected ? 4 : 0)),
),
);
}).toList());
},
);
}
}

StreamBuilder is used to receive StateOutput stream’s value, and use store.dispatch to send action(one page one store). It’s that simple.

Source: https://github.com/lzyy/bloc_redux

--

--

limboy

as a creator, i make tools; as an explorer, i do research. Twitter: @lzyy