Better BLOC flutter state management
Hi everyone, I am Filippo Zazzeroni. I have been a software engineer since three years, focusing mainly on mobile development, in Flutter and Native, both iOS and Android. I write this article because I think Flutter has great potential and need to be understood in order to fully exploit its abilities. What I think about this technology is that it is very powerful and can lead to really great results in a short time.
My motto, and I think which is shared among community of developers, is that if the wheel already exists, it shouldn’t be invented again. But with Flutter, everything changes. This motto is no longer applicable and is replaced by strong software designing skills and dedication.
My approach with Flutter has continuously changed over time, because new knowledge was being added as days went on. As the team leader of MasterBiga, a startup, with the aim to create technology for non-industrial bakery and pizzeria, I decided to apply this development cycle to our flutter products. We divided our main application into microservices that could live independently of each other. This was developed and maintained by the internal team. All the plugins were developed with the scope of reducing external dependencies to already existing flutter packages, like BLOC.
BLOC library was redesigned from scratch to achieve better results and to not adapt to already existing flutter bloc plugin.
BLOC plugin redesign
The redesign led to some important results. The first result was with the BlocProvider class that originally uses a context in order to provide the specified bloc to the class. Now the bloc provider doesn’t use context anymore to retrieve blocs, instead it uses a Singleton manager class that simply stores blocs in a list and it filters them to retrieve the selected one.
Here, we can see that project structure of bloc redesign:
-- bloc:
- bloc_provider.dart
- bloc_builder.dart
- bloc.dart
- bloc_coordinator.dart
- bloc_state.dart
- state_manageable.dart
- provider_manager.dart
- bloc_initialiser.dart
Bloc
import 'dart:async';
import 'package:payments/models/interfaces/bloc/provider_manager.dart';
import 'bloc_coordinator.dart';
import 'bloc_state.dart';
abstract class Bloc<T extends BlocState, C extends BlocCoordinator> {
abstract final BlocCoordinator coordinator;
Stream<C> get stream;
T get state;
void setState(T state) {
coordinator.setState(state);
}
static T ofType<T extends Bloc>() {
return ProviderManager.instance.read<T>();
}
}
Here is the Bloc class: it defines the foundation of a bloc. Bloc holds a coordinator who coordinates the states of the bloc. The bloc provide a class method to read the required bloc from the provider manager. The bloc is able to set its state and get a stream of its coordinator object.
Bloc Coordinator
import 'dart:async';
import 'bloc_state.dart';
abstract class BlocCoordinator<State extends BlocState> {
BlocCoordinator() {
onControllerFirstListenCallback();
}
State getState();
void setState(State state);
abstract final State state;
abstract final StreamController<BlocCoordinator> controller;
void onControllerFirstListenCallback();
}
The coordinator object holds the bloc state and a stream of itself. It is able to set the state of the bloc.
Provider Manager
import 'package:payments/models/interfaces/bloc/bloc.dart';
class ProviderManager {
ProviderManager._internal();
static final instance = ProviderManager._internal();
final _providers = [];
void save<T extends Bloc>(T provider) {
_providers.add(provider);
}
T read<T extends Bloc>() {
return _providers.whereType<T>().first;
}
}
This manager implements Singleton pattern, and is able to save and read a Bloc from the providers lists.
Bloc Provider
import 'package:payments/models/interfaces/bloc/provider_manager.dart';
import 'bloc.dart';
class BlocProvider<T extends Bloc> {
void create(T bloc) {
ProviderManager.instance.save(bloc);
}
}
The bloc provider is very simple, and delete any trace of the context. It can only create a bloc and save it in the provider list.
State Manageable
mixin StateManageable {
Widget blockBuilder<T extends Bloc, V extends BlocCoordinator>(Widget Function(BuildContext,AsyncSnapshot<BlocCoordinator>) builder) {
return BlocBuilder<T ,V>(builder: builder);
}
}
This mixin helps use this plugin in a flexible way like the example below:
class TestView extends StatelessWidget with StateManageable {
const TestView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<DatabaseUpdateNotifier,DatabaseUpdateStateCoordinator>(
builder: (context, data) {
return const Text("test");
},);
}
}
Bloc Initialiser
mixin BlocInizitaliserable {
void initialiser({required Bloc bloc}) {
BlocProvider().create(bloc);
}
}
Mixins are very flexible way to integrate features in class and for this reason I use mixin to expose the functionality of the package.
Here is an example:
class TestView extends StatefulWidget {
const TestView({Key? key}) : super(key: key);
@override
State<TestView> createState() => _TestViewState();
}
class _TestViewState extends State<TestView> with StateManageable, BlocInitialiser { @override
void initState() { initialiser(bloc: DatabaseUpdateNotifier()) } @override
Widget build(BuildContext context) {
return BlocBuilder<DatabaseUpdateNotifier, DatabaseUpdateStateCoordinator>(
builder: (context, data) {
return const Text("test");
},);
}
}
Application to Database Update Notifier
This bloc listens to update on the database and when new data are added, it notifies the bloc builders that listen to this bloc.
class DatabaseUpdateNotifier extends Bloc<DatabaseUpdateState, DatabaseUpdateStateCoordinator> {
final _coordinator = DatabaseUpdateStateCoordinator();
void updateState(DatabaseUpdateState newState) {
if (newState != coordinator.getState()) {
coordinator.controller.sink.add(coordinator);
}
}
@override
BlocCoordinator<BlocState> get coordinator => _coordinator;
@override
DatabaseUpdateState get state => _coordinator.getState();
@override
Stream<DatabaseUpdateStateCoordinator> get stream => _coordinator.controller.stream as Stream<DatabaseUpdateStateCoordinator>;
}
class DatabaseUpdateStateCoordinator<T extends DatabaseUpdateState> implements BlocCoordinator<DatabaseUpdateState> {
@override
DatabaseUpdateState getState() {
state = DatabaseUpdateState(DatabaseUpdateState.noUpdates);
controller.add(this);
return state;
}
@override
StreamController<DatabaseUpdateStateCoordinator> controller = StreamController<DatabaseUpdateStateCoordinator>.broadcast();
@override
void onControllerFirstListenCallback() {
controller.onListen = () {
controller.add(this);
};
}
@override
DatabaseUpdateState state = DatabaseUpdateState(DatabaseUpdateState.noUpdates);
@override
void setState(DatabaseUpdateState state) {
this.state = state;
controller.add(this);
}
}
class DatabaseUpdateState extends BlocState {
DatabaseUpdateState(this.rawValue);
static const noUpdates = "noUpdates";
static const updatesInQueue = "updatesInQueue";
@override
final String rawValue;
}
In this article, I have tried to show an example of the Database Update Notifier with the support of the Bloc package.
Thank you for reading. This, for me, is first of many articles to come. I can’t wait to share my knowledge and publish new resources.