Posted on February 18, 2021 | Guille
Using Google’s Flutter SDK with BLoC’s cubits is a great way to quickly and easily build a reactive application that can run natively on a variety of platforms including Android, iOS, web, and desktop.
What is Flutter?
Flutter is an open-source declarative UI software development kit created by Google for the Dart programming language. Declarative UI means that instead of describing the UI and then modifying it with code (known as imperative programming), you describe the UI in terms of its state, so that it changes automatically when the state changes. This is now becoming the preferred way of working with UI in many frameworks such as React (web), SwiftUI (iOS), and Jetpack Compose (Android). Dart is an object-oriented and strongly typed programming language which compiles to native code and JavaScript. When creating a Flutter application it is hard to discern where Dart ends and Flutter begins, so you end up learning the combination (just as people learn jQuery or React without distinguishing between the framework and the underlying JavaScript language).
What is a Cubit?
To make use of the declarative UI you need a way of storing and modifying the state. Google’s answer to this is the Business Logic Component (BLoC) pattern, based on events (known as reactive programming). A BLoC receives inputs (user interactions, API responses, etc.) and sends events to change the state and update the UI accordingly. Working with BLoC events and streams is very powerful, but introduces complexity that is unnecessary in many cases. For this reason a subset of BLoC known as Cubit was created, where new states can be set by simply calling the emit() method. All widgets that are subscribed to the Cubit (through BlocProvider) will receive the new state and can then rebuild themselves (with BlocBuilder) or do something (with BlocListener).
Putting it all together
After installing the Flutter SDK on your computer, you can create an empty app by executing the command flutter create <my_app>
in the command-line or by using the integrations available for Android Studio and Visual Studio Code. Next you have to add the flutter_bloc library as a dependency to your app, by adding the corresponding line to the pubspec.yaml
file in the root directory of your app:
dependencies:
flutter_bloc: ^6.1.2
Another useful library that is part of BLoC is hydrated_bloc, which persists the cubit on the device so the app can return to the exact same state when it is reopened.
It helps to organize your code in files that correspond with the pieces of the BLoC pattern. In general you will have one these files for each entity in your app:
- entity_model.dart: contains the description of the object you will be working with
- entity_repository.dart: contains the data collection code (API calls, file reading, etc.)
- entity_state.dart: contains all possible states and the data they require
- entity_cubit.dart: contains the cubit logic (methods to emit state changes)
- entity_page.dart: contains the widget to draw the screen UI
We will now look at the content of each of these files in more detail.
Model
This is just the typical OO description of the entity your app will be dealing with. It will have properties associated with the entity, and methods to modify it. Apart from the default constructor, it is fairly common to have a factory method that will create the entity from a data source (for example, an API’s JSON response). Obviously this depends a lot on the entity you are working with, but here is a very basic example:
class Entity {
final int id;
final String name;
Entity(this.id, this.name);
factory Entity.fromJson(Map<String, dynamic> json) {
return Entity(json['id'], json['name']);
}
}
Repository
This class will deal with whichever external data source must be accessed to keep the app state aligned with the real world. For the common case of an API being the data source it would look something like this:
class EntityRepository {
Future<Entity> getEntity() async {
try {
final Response response = await http.get('https://example.com/api/entity');
return Entity.fromJson(response.data);
} catch (e) {
throw e;
}
}
}
State
Here we will define an abstract EntityState class which must extend Equatable to allow the states to be compared so the UI knows if it has to rebuild. Each state will extend this abstract state and may contain data associated with the state. This is what it looks like:
abstract class EntityState extends Equatable {}
class EntityInitial extends EntityState {
@override
List<Object> get props => [];
}
class EntityLoading extends EntityState {
@override
List<Object> get props => [];
}
class EntityLoaded extends EntityState {
final Entity entity;
EntityLoaded(this.entity);
@override
List<Object> get props => [entity];
}
class EntityError extends EntityState {
final String message;
EntityError(this.message);
@override
List<Object> get props => [message];
}
In this case the loaded state contains the entity as its data, while the error state has an error message as its data.
Cubit
This file describes a class EntityCubit that extends Cubit (or HydratedCubit). It will have a reference to the repository and include methods such as getEntity
, updateEntity
, addEntity
, etc. Each of these methods will do something with the repository and then emit a new state with the result. When the repository is asynchronous (for example an API request) it can be useful to first emit a loading state (so the UI reflects that it is doing something in the background), and at the end emit the new loaded state. It will look something like this:
class EntityCubit extends Cubit<EntityState> {
final EntityRepository repository;
EntityCubit(this.repository) : super(EntityInitial());
getEntity() async {
emit(EntityLoading());
try {
final Entity entity = await repository.getEntity();
emit(EntityLoaded(entity));
} catch (e) {
emit(EntityError('Could not retrieve entity');
}
}
}
Here we see that the getEntity method will first set the loading state, then ask the repository for the entity and, if successful, set the loaded state with the entity as its data. If the repository throws an exception, the error state is set with a message to indicate what happened.
Page
A tree of Flutter Widgets to describe the UI in terms of the state. It must contain the BlocProvider widget to receive the Cubit, and the BlocBuilder widget to specify the UI dependence on the state. In this simple example it could look like this:
class EntityPage extends StatefulWidget {
@override
_EntityPageState createState() => _EntityPageState();
}
class _EntityPageState extends State<EntityPage> {
@override
void initState() {
super.initState();
EntityCubit().getEntity();
}
@override
Widget build(BuildContext context) {
return BlocProvider<EntityCubit>(
create: (context) => EntityCubit(),
child: BlocBuilder<EntityCubit, EntityState>(
builder: (context, state) {
if (state is EntityLoading) {
return Center(
child: CircularProgressIndicator()
);
} else if (state is EntityError) {
return Center(
child: Text(state.message)
);
} else if (state is EntityLoaded) {
return Column(
children: [
Text('ID: ${state.entity}'),
Text('Name: ${state.name}')
]
);
} else {
return Container();
}
}
)
)
}
}
As you can see, it will show an empty Container for the EntityInitial state, then when the getEntity method is called and the state is changed to EntityLoading it will display a CircularProgressIndicator. If the API call fails the state will become EntityError and the corresponding message will be displayed. If the API call is successful it will transition to the EntityLoaded state, where the information of the entity (its ID and Name) is shown to the user.
Conclusion
Hopefully this has served as an introduction to Flutter and the BLoC pattern. Obviously this example is very basic and doesn’t show everything that is possible, but you can see more complex cases in two of our apps: Workout Time! and WD Notes.