Bloc Orchestration in Flutter
Not so long ago I stumbled upon a problem that requires me to orchestrate a few blocs at once in a flutter. To my surprise, there isn’t any article about one! So I had to bootstrap ideas from bloc documentation and hack my way through it for my blocs to be able to talk to each other. This article is a hack that I found during my study.
In this article, I will assume that you understand how to create and use blocs. That’s why I won’t explain that. The complete solution is at the end of the article.
The Scenario
Let’s imagine we’re tasked to build an application mimicking a dam. The dam has one state: its waterLevel. The only interaction is we can increment its waterLevel. The app will tell us if the water is chill, beware, danger, or flooding.
To create such app, we can create a bloc for dam, then make 4 states that represents chill, beware, danger, or flood. BUT for the sake of this article, we’re creating two bloc: DamBloc and WarningBloc which depends on each other. The app will do two things:
- When the water level rises (DamBloc), do some calculations to determine the warningLevel (WarningBloc)
- When in FloodState (WarningBloc), after some seconds, reset the water level of the dam (DamBloc)
Now, let’s create some bloc orchestration!
Setup
Create a new flutter project named bloc_orchestration
and add bloc
and flutter_bloc
to the dependency.
After that, create a /lib/blocs folder then fill it with 3 files:
// lib/blocs/dam.bloc.dart
import 'package:bloc/bloc.dart';
class DamBloc extends Bloc<DamEvent, DamState> {
DamBloc() : super(DamState(0)) {
on<IncrementWaterLevel>((event, emit) {
final waterLevel = state.waterLevel + 1;
emit(DamState(waterLevel));
});
on<ResetWaterLevel>((event, emit) {
emit(DamState(0));
});
}
}
abstract class DamEvent {}
class IncrementWaterLevel extends DamEvent {}
class ResetWaterLevel extends DamEvent {}
class DamState {
int waterLevel;
DamState(this.waterLevel);
}
// lib/blocs/warning.bloc.dart
import 'package:bloc/bloc.dart';
import 'package:bloc_orchestration/blocs/constant.dart';
class WarningBloc extends Bloc<WarningEvent, WarningState> {
WarningBloc() : super(ChillState()) {
on<UpdateWarning>((event, emit) {
if (event.warningLevel < chillThresshold) {
emit(ChillState());
} else if (event.warningLevel < bewareThreshold) {
emit(BewareState());
} else if (event.warningLevel < dangerousThreshold) {
emit(DangerousState());
} else {
emit(FloodState());
}
});
}
}
abstract class WarningEvent {}
class UpdateWarning extends WarningEvent {
int warningLevel;
UpdateWarning(this.warningLevel);
}
abstract class WarningState {}
class ChillState extends WarningState {}
class BewareState extends WarningState {}
class DangerousState extends WarningState {}
class FloodState extends WarningState {}
// lib/blocs/constant.dart
const chillThresshold = 4;
const bewareThreshold = 6;
const dangerousThreshold = 8;
And then, let’s flood the UI to use the dam.bloc.dart:
# lib/main.dart
import 'package:bloc_orchestration/blocs/dam.bloc.dart';
import 'package:bloc_orchestration/blocs/warning.bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'dam.screen.dart';
void main() {
runApp(const DamApp());
}
class DamApp extends StatelessWidget {
const DamApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MultiBlocProvider(
providers: [
BlocProvider(create: (_) => DamBloc()),
BlocProvider(create: (_) => WarningBloc()),
],
child: const DamScreen(),
),
);
}
}
# lib/dam.screen.dart
import 'package:bloc_orchestration/blocs/dam.bloc.dart';
import 'package:bloc_orchestration/blocs/warning.bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class DamScreen extends StatelessWidget {
const DamScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
title: BlocBuilder<WarningBloc, WarningState>(
builder: (context, state) {
String text = "";
Color color = Colors.white;
if (state is ChillState) {
text = "We cool";
color = Colors.green;
}
if (state is BewareState) {
text = "Warning: possible flood";
color = Colors.yellow;
}
if (state is DangerousState) {
text = "Danger: high possibility of flood";
color = Colors.red;
}
if (state is FloodState) {
text = "It's flooding, pray to your Gods";
color = Colors.orange;
}
return Text(text, style: TextStyle(color: color));
},
),
),
body: Center(
child: BlocBuilder<DamBloc, DamState>(
builder: (context, state) => Text("${state.waterLevel}"),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
BlocProvider.of<DamBloc>(context).add(IncrementWaterLevel());
},
),
);
}
}
Now try running the application. This should be what you see:
That is pretty cool. Now, let’s make it so that the warning bloc changes when waterLevel changes!
MultiBlocListener
Create a new file called dam.notifier.dart
in lib/blocs directory and add those codes:
import 'dart:async';
import 'package:bloc_orchestration/blocs/dam.bloc.dart';
import 'package:bloc_orchestration/blocs/warning.bloc.dart';
import 'package:flutter/src/widgets/container.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class DamNotifier extends StatelessWidget { // 1
const DamNotifier({super.key, required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return MultiBlocListener( // 2
listeners: [
BlocListener<DamBloc, DamState>(
listener: (_, state) {
BlocProvider.of<WarningBloc>(context).add(
UpdateWarning(
(state.waterLevel / 2).floor(), // 3
),
);
},
),
BlocListener<WarningBloc, WarningState>(
listener: (_, state) {
// if it is flooding, some secs later, water level chills down
if (state is FloodState) {
Future.delayed(const Duration(seconds: 3), () {
BlocProvider.of<DamBloc>(context).add(ResetWaterLevel()); // 4
});
}
},
),
],
child: child,
);
}
}
Now I’ll explain some important concepts above:
- The orchestrator, or the DamNotifier itself is a stateless widget. It has to be a widget so that we can wrap our screen with the orchestrator.
- MultiBlocListener allows us to listen to multiple blocs at once, and play with its current state.
- We pretend the calculation of waterLevel to warningLevel to be waterLevel / 2. The advantage of this approach is that when we want to change calculation, we will only change the orchestrator, not the bloc itself.
- Let’s pretend that 3 seconds is enough to reset the dam back to zero (I know nothing about civil engineering ¯\_(ツ)_/¯).
After that, in your main.dart, change the child in MultiBlocProvider
to DamNotifier:
MultiBlocProvider(
// ...
child: const DamNotifier(child: const DamScreen()),
),
Now, restart the app, and the orchestration should work!
For the complete solution: https://github.com/margunwa123/flutter-multi-bloc-orchestration .