How to architect a Flutter application is a question that has no easy answer. Dart, being a multi-paradigmatic language, lets you mix and match OOP and functional programming approaches. It is very easy to mess things up when using both these concepts, but I am going to make your life a little easier using reactive programming and BLoC (Business Logic Component) library.
What is BLoC?
BLoC stands for Business Logic Component, and is, essentially, a global state manager. It holds data that describes a particular state of your application (can be literally everything, from theme and locale to value in a text box). BLoC also provides an easy way to subscribe your Widgets to part of the state and implement logic to modify the state. Those coming from React background will immediately recognize it as a Redux sort of thing, and they do have a lot of similarities.
How does it work?
Here you can see a simplified flowchart of how BLoC works. A Bloc holds the state and provides streams of this state. A stream is something you can subscribe to and be notified every time the state changes. The Bloc also accepts events. An event is sent to the bloc, and bloc handles it by applying transitions. A transition is a change from one state to another in response to an event. After a transition is applied, streams are notified and UI reflects the changes to the state. Let it sink in for a minute.
Let me explain this architecture more clearly with an example. Suppose that you are building an app that has a dark theme and a light theme, and it is light theme by default. You create a bloc that holds the current theme, either light or dark. In your UI, there is a button to change the theme. This button will send an event when user presses it. The bloc will receive the event, and prepare the transition from old state (light theme) to new state (dark theme). After the transition is applied, streams notify of this change every UI Widget that cares for it. Widgets receive new state and re-render using a different theme.
The repository is something not included in the original Flux architecture, but very useful nonetheless. The bloc uses the repository to access the long-term state. Unlike regular state, long-term state is usually stored in a database, either locally or remotely. The repository provides access to it, by means of REST, GraphQL, SQL, or other methods. The nice thing about repositories is that you can have multiple repositories (for local and remote state, or for different SQL dialects) and you do not have to change anything in your business logic code while switching between them!
How do I use it?
I will walk you through using BLoC to build the simplest of apps, the counter. It is the same one that you see with every new project, but this time you will use BLoC to do it. Firstly, you need to include it in your dependencies. Add the following (maybe update the versions if the current year is not 2020) to your pubspec.yaml
:
bloc: ^4.0.0
flutter_bloc: ^4.0.0
Now, we are ready to start using BLoC! Create a new Flutter app (read this tutorial first if you do not know how to do it) and create a new file counter.dart
. Firstly, we will define our event types. Since need to add or remove 1 from counter, let us name them increase
and decrease
:
import 'package:bloc/bloc.dart';
enum CounterEvent {increment, decrement}
The enum
keyword defines a enumeration, which is perfect for our case. As your events become more complex, you might switch to using classes for them, but enum
will do for now. Then, we will define the bloc itself:
class CounterBloc extends Bloc<CounterEvent, int> {
@override
int get initialState => 0;
}
You can see that to make a bloc you need to inherit Bloc
, and provide types for (1) events and (2) state itself. Now, you must override the getter for initialState
and provide the initial state. In our case, it is just 0
.
Do not forget to import the
bloc
library with this line:import 'package:bloc/bloc.dart';
Lastly, we need to process the events. Add this method to class body:
@override
Stream<int> mapEventToState(CounterEvent event) async* {
switch (event) {
case CounterEvent.decrement:
yield state - 1;
break;
case CounterEvent.increment:
yield state + 1;
break;
}
}
Every bloc must also override the mapEventToState
function. This is a generator function, which accepts event
and returns a stream, that notifies of state change whatever is subscribed to it. In the Redux world, this is a reducer.
Let’s go back to main.dart
now. Define a stateless widget CounterPage
, it will render the UI for our app. Here is the code for it:
On line 4, you can notice how to access the bloc from within the Widget. It still has to be provided, I will show you later. Then, on line 8, we use BlocBuilder
to build the UI. You need to use BlocBuilder
so that it re-renders your Widgets if state is modified. On lines 16 and 21 you can see how events are sent to the bloc: with the function add
.
Additionally add these imports to top of main.dart
and change the code in MyApp
:
On line 12, we use BlocProvider
to provide the rest of the widgets with our bloc. This does not mean that everything in BlocProvider
re-renders on state change: Widgets have to manually consume the bloc with BlocProvider.of
. Apart from that, there are no other changes in the code. If you try running the app now, you will see the working counter.
Closing notes
Thank you for reading, I hope you liked this article. I will be writing more on advanced Flutter and BLoC concepts, and you can subscribe to my mailing list if you are interested.
You can try this library with generic approach for cooking graphql_flutter + bloc
https://pub.dev/packages/graphql_flutter_bloc