I have to admit it. My first experience with Flutter was not great. It was very unstable when I started working with it, and what put me off was the lack of architecture patterns. It was hard for me to easily structure my app, and I had to create custom logic for good communication between components. So I abandoned my Flutter projects and waited for what the time will bring.
Recently I had to create a multiplatform application. So it was either Flutter or React Native. Since my skills in web development ended on writing "Hello World" in HTML I have decided to give Flutter another chance. I saw that a lot of things have changed, and a lot of new architecture patterns came into play. I have tested some of them in simple projects, and the one I immediately fell in love with was BLoC.
BLoC introduction
BLoC stands for Business Logic Controller. It was created by Google and introduced at DartConf 2018. It is created based on Streams and Reactive Programming.
If you want to start creating apps with BLoC architecture I would strongly recommend two libraries that make working with it much easier: bloc and flutter_bloc. I would also recommend the official documentation of these libraries. It is well-written, with tons of examples that could be applied to most of the use-cases. I will describe briefly all of the BLoC components, but if you want to dive deeper, documentation is the best place to go.
In BLoC pattern we can distinguish four main layers of application:
UI - it holds all of the application's components, that are visible to the user and could be interacted with. Since in Flutter all parts of User Interface are Widgets, we can say that all of them belong in this layer.
BLoC - these are classes that act as a layer between data and UI components. It listens to events passed from it, and after receiving a response it emits an appropriate state.
Repository - it is responsible for fetching pieces of information from single or multiple data sources and processing it for the UI classes.
Data sources - these are classes that provide data for the application, from all of the data sources including database, network, shared preferences, etc.
So now, when we know all of the basic structures, we should understand how these layers communicate with each other. BLoC pattern relays on two main components for it:
Events that are passed from UI, that contains information about a specific action that has to be handled by the bloc.
States that show how the UI should react to change of data. Every BLoC has its initial state, which is defined when it is created.
For example, if we want to implement a login screen, we would have to pass LoginEvent with login details, when user clicks on the appropriate button. After receiving response BLoC should show the SuccessState - when login will be completed successfully, or ErrorState - when the user has entered the wrong credentials, or some other error has occurred.
App specification
Let's tackle BLoC by example. I have created a simple app for fetching song lyrics. It should enable a user to search for lyrics from the Genius API. I have also decided to allow a user to create, update and delete their lyrics to test how the BLoC pattern will work with multiple data sources. Project source code can be found here. And since I will only describe some of the BLoC specific components, you can see how I have implemented the Data Source and Repository layer there.
Lyrics fetched from the Genius are displayed on the Webview, and those added by a user are shown on the custom screen with the possibility to edit them. Removing items is done by swiping them off the list.
Getting started
To start working with Flutter BLoC library I had to add two dependencies to pubspec.yaml file.
bloc: ^2.0.0
flutter_bloc: ^2.0.1
My first approach to creating this app was following the TODOs example. It looked very similar to my application and had similar functionalities. Following this example, I have created one BLoC class for all operations on my lyrics data and three screens. My project structure looked like this:
It turned out not to be the best solution. It was hard to properly handle state changes in screens, and update them depending on states of other screens.
I found out that the best solution for structuring a BLoC app is to create one BLoC for one screen. It will help you to always know in which state UI component is currently in, just from the BLoC that is assigned to it.
Keeping this in mind I have refactored my project, and afterward, its structure looked like this:
You can see that not all of the screens have BLoC assigned to it. Take for example the song details screen. It will only display information about the song, which will be passed to it. So it is unnecessary to track this screen state information.
While working with BLoC it is up to you to decide, whenever each screen should have its BLoC component. Some complicated screens could even have multiple BLoCs that will communicate with each other.
Events and states
Now, when I knew how the project will be structured I could define states which every screen could be in, and which events will it sent.
I will demonstrate the process of implementing BLoC architecture by Search Screen with the possibility to search for song lyrics. Firstly, I have to define the event that this screen will send:
TextChanged - shows that input in the search field has changed, and new songs list should be fetched
Now I have to define states that this screen could be in:
StateEmpty - it should be active when there is no user input in the search bar, it would be BLoC initial state.
StateError - the state should be passed with an error message when something goes wrong.
StateLyricsLoaded - this state will be passed, with a list of songs, after successful fetching songs from repository.
StateLoading - defines that the repository is waiting for the response from the server, or is processing data.
Putting it all together
I now know how the main screen of the application will behave, and I can start implementing app functionalities. Let's start with the main app functionality - searching for lyrics.
When working with BLoC pattern you should always start from the bottom layer and then move to the upper ones based on the flow of data. So after I have implemented data sources and repository, the next step was to create BLoC.
To hold states for the main screen of the application I have to create file song_search_state. It defines all of the states that the search screen could be in.
abstract class SongsSearchState extends Equatable {
SongsSearchState([List props = const []]) : super(props);
}
As you can see, this class extends the Equatable. It will help to check if new state differs from the current one. It simply allows us to compare objects by the list of props, that are passed in the constructor.
But why is it needed to check if the new state differs from the current? If a passed object is equal to the last one, we don't want to rebuild our screen, and this solution does that for us. So for example, if StateLoading is passed two times, one after another, UI widget that listens to it, will only receive it once.
class SearchStateEmpty extends SongsSearchState {
@override
String toString() => 'SearchStateEmpty';
}
class SearchStateLoading extends SongsSearchState {
@override
String toString() => 'SearchStateLoading';
}
class SearchStateSuccess extends SongsSearchState {
final List<SongBase> songs;
final String query;
SearchStateSuccess(this.songs, this.query) : super([songs]);
@override
String toString() => 'SearchStateSuccess { songs: ${songs.length} }';
}
class SearchStateError extends SongsSearchState {
final String error;
SearchStateError(this.error) : super([error]);
@override
String toString() => 'SearchStateError { error: $error }';
}
As you can see, I have created states that was described before. Every state could also hold and pass different objects. Like for example, ErrorState holds an error message and SuccessState holds a list with the fetched songs.
It is also good practice to override the toString method. It will well describe the state and will be printed after a transition from one state to another. More on that later.
abstract class SongAddEditEvent extends Equatable{
SongAddEditEvent([List props = const []]) : super(props);
}
Event class also extends Equatable. It is not necessary since by default BLoC library doesn't make use of this. But knowing whenever passed event will be different from current one allows to manipulate the stream of events in BLoC, which makes it possible to implement functionalities like debounce, distinct, etc.
class TextChanged extends SongSearchEvent {
final String query;
TextChanged({this.query}) : super([query]);
@override
String toString() => "SongSearchTextChanged { query: $query }";
}
Now I could create an event, that will inform BLoC that the user has changed the search query. Event classes look very similar to state classes, and similar rules apply to them.
When states and events were created, I could finally start implementing song_search_bloc.
class SongsSearchBloc extends Bloc<SongSearchEvent, SongsSearchState> {
final LyricsRepository lyricsRepository;
SongsSearchBloc({
@required this.lyricsRepository,
@required this.songAddEditBloc})
@override
SongsSearchState get initialState => SearchStateEmpty();
@override
void onTransition(Transition<SongSearchEvent, SongsSearchState> transition) {
print(transition);
} @override
Stream mapEventToState(SongSearchEvent event) async* {
if (event is TextChanged) {
yield* _mapSongSearchTextChangedToState(event);
}
}
}
Song search BLoC holds an instance of LyricsRepository, which is responsible for combining network and local data source, and doing all operations on the fetched data.
As stated before, I had to override getter for field initialState, to show in which state will BLoC be after its creation.
Remember when I have advised to override the toString method in every state and event? Place where it will be useful is the onTransition. It is called whenever BLoC will change its state. And thanks to the toString method of every state, after each transition terminal would print nice output.
I/flutter (22988): Transition { currentState: SearchStateEmpty, event: SongSearchTextChanged { query: never gonna give }, nextState: SearchStateLoading }
I/flutter (22988): Transition { currentState: SearchStateLoading, event: SongSearchTextChanged { query: never gonna give }, nextState: SearchStateSuccess { songs: 10 } }
I/flutter (22988): Transition { currentState: SearchStateSuccess { songs: 10 }, event: SongSearchTextChanged { query: }, nextState: SearchStateEmpty }
Next thing to override in BLoC is mapEventToState method. It will be called every time a new event is added to the BLoC, and it should do what its name suggests - react to a particular event with a specific state.
Every event should have their corresponding method. So as stated before TextChanged should produce state depending on the result of search.
Stream _mapSongSearchTextChangedToState(TextChanged event) async* {
final String searchQuery = event.query;
if (searchQuery.isEmpty) {
yield SearchStateEmpty();
} else {
yield SearchStateLoading();
try {
final result = await lyricsRepository.searchSongs(searchQuery);
yield SearchStateSuccess(result, searchQuery);
} catch (error) {
yield error is SearchResultError
? SearchStateError(error.message)
: SearchStateError("Default error");
}
}
}
One new thing for me was the yield keyword. It adds value to the stream that was called by the yield*. You can think about it, that it acts as a return, but it doesn't stop execution of code that is afterward, thanks to which state could be changed to multiple values in one method.
One last thing that should be done is to provide created BLoC, so it could be accessed by UI widgets. To do this main MaterialApp file should be modified.
@override
Widget build(BuildContext context) {
return BlocProvider<SongAddEditBloc>(
builder: (context)
SongAddEditBloc(lyricsRepository: lyricsRepository),
child: MaterialApp(
//main app code
));
}
Working with BLoC from UI
After BLoC is implemented, the next thing to do is to make use of it in UI. Firstly I have to show appropriate widget depending on the state:
@override
Widget build(BuildContext context) {
return BlocBuilder<SongsSearchBloc, SongsSearchState>
bloc: BlocProvider.of(context),
builder: (BuildContext context, SongsSearchState state) {
if (state is SearchStateLoading) {
return CircularProgressIndicator();
}
if (state is SearchStateError) {
return Text(state.error);
}
if (state is SearchStateSuccess) {
return state.songs.isEmpty
? Text(AppLocalizations.of(context).tr(S.EMPTY_LIST))
: Expanded(
child: _SongsSearchResults(
songsList: state.songs,
),
);
} else {
return Text(AppLocalizations.of(context).tr(S.ENTER_SONG_TITLE));
}
},
);
}
Then in TextField, where user types song search query I have to send new event to created BLoC. We can achieve this by calling method add(Event).
TextField(
onChanged: (text) {
_songSearchBloc.add(TextChanged(query: text));
}
)
If you take a look at the project source code you will see, that these functions are placed in separate files. And this is where the power in BLoC lies in. You can get the same instance of single BLoC in any Widget.
Transforming events
Now the search is working as it should be. But there is one thing that could be improved. Right now every time user changes text input, a new request is sent. So when user types name of the song very quickly there would be as many requests as many letters this title contains. Good practice in this situation is to wait for a small amount of time and cancel the previous request when new is send. This method is called debounce, you can find more information about it in ReactiveX documentation.
@override
Stream<SongsSearchState> transformEvents(Stream<SongSearchEvent> events,
Stream<SongsSearchState> Function(SongSearchEvent event) next) {
return super.transformEvents(
(events as Observable<SongSearchEvent>).debounceTime(
Duration(milliseconds: DEFAULT_SEARCH_DEBOUNCE),
),
next,
);
}
As I have mentioned before, we can extend Equatable in events class of BLoC, to know whenever the state changed to new. This gives us the possibility to override function transformEvents in BLoC and manipulate the incoming stream
Communication between BLoCs
Let's skip in time for a while. I have implemented second BLoC with a possibility to add and edit a song. Allegorically as I shown before I added events states and assigned it to screen.
Everything worked great until I had added a song and went back to my search screen. When there was inserted search query that newly added song should contain, it would not appear on the list.
My first solution was to update the list after adding and removing the song. But it generated unnecessary API calls. I found a better solution, which is listening to my second BLoC state changes in the first one.
First I had to add new events to SongSearchBloc - SongAdded and SongUpdated - which will pass the instance of added or changed song. Then create something called StreamSubscription, which is responsible for listening changes from other BLoC.
final SongAddEditBloc songAddEditBloc;
StreamSubscription addEditBlocSubscription;
SongsSearchBloc(
{@required this.lyricsRepository, @required this.songAddEditBloc}) {
songAddEditBloc.listen((songAddEditState) {
if (state is SearchStateSuccess) {
if (songAddEditState is EditSongStateSuccess) {
add(SongUpdated(song: songAddEditState.song));
} else if (songAddEditState is AddSongStateSuccess) {
add(SongAdded(song: songAddEditState.song));
}
}
});
}
It is also very important to remember to cancel subscription after it won't be needed anymore. Every BLoC can override method close, which is called after BLoC will no longer be used.
@override
Future<void> close() {
addEditBlocSubscription.cancel(); return super.close();
}
Since I have created second BLoC, and since the first one depended on it, main file should be modified once again.
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<SongAddEditBloc>(
builder: (context) =>
SongAddEditBloc(lyricsRepository: lyricsRepository),
),
BlocProvider<SongsSearchBloc>(
builder: (context) => SongsSearchBloc(
lyricsRepository: lyricsRepository,
songAddEditBloc: BlocProvider.of<SongAddEditBloc>(context)),
),
],
child: MaterialApp(
//main app code
),
);
}
Testing with BLoC
As I have stated before BLoC helps you to easily create tests. Since this topic is really broad and could be subject for another article I will show a simple example, and if you want to see more of them you can always go to the source code, where I have prepared few tests.
Firstly there should be prepared mocked classes, that will be used in tests.
class MockLyricsRepository extends Mock implements LyricsRepository {}
class MockSongBase extends Mock implements SongBase {}
Afterwards, we can start implementing the main function of our tests. We should remember to initialize BLoC in setUp method and close it in tearDown.
void main() {
SongsSearchBloc songsSearchBloc;
MockLyricsRepository lyricsRepository;
String query = "query.test";
List<SongBase> songsList = List();
setUp(() {
lyricsRepository = MockLyricsRepository();
songsSearchBloc = SongsSearchBloc(lyricsRepository: lyricsRepository);
});
tearDown(() {
songsSearchBloc?.close();
});}
Then we can write simple tests that will check whenever BLoC initial state is correct and it not emit any state after being closed.
test('after initialization bloc state is correct', () {
expect(SearchStateEmpty(), songsSearchBloc.initialState);
});
test('after closing bloc does not emit any states', () {
expectLater(songsSearchBloc, emitsInOrder([SearchStateEmpty(), emitsDone]));
songsSearchBloc.close();
});
When writing tests for BLoC you have to know in which states should the BLoC be after specific events. Let's tackle search functionality as an example, after inserting text by user state from empty should go to loading, and after fetching songs list - to success. This states should be defined in order in the array and passed as an argument of function expectsLater, which checks if BLoCs states have changed accordingly.
test('emits success state after insering lyrics search query', () {
List<SongBase> songsList = List();
songsList.add(MockSongBase());
final expectedResponse = [
SearchStateEmpty(),
SearchStateLoading(),
SearchStateSuccess(songsList, query)
];
expectLater(songsSearchBloc, emitsInOrder(expectedResponse));
when(lyricsRepository.searchSongs(query))
.thenAnswer((_) => Future.value(songsList));
songsSearchBloc.add(TextChanged(query: query));
});
Then we just have to tell the instance of mocked LyricsRepository to return the list with mocked songs, so when our BLoC will call this function it will work as expected.
The last thing is to add an event to our BLoC, that will produce the desired state, and that's it. Now we have a working test for implemented functionality.
Summary
I think that BLoC is a great pattern, that could be used in every type of app. It helps to improve the quality of your code and makes working with it a real pleasure.
Since it uses advanced techniques like Streams and Reactive Programming, I think that it would be hard for the beginners to try it. But after understanding fundamentals, it is really simple to create a simple app using this architecture.
Photo by David Pisnoy on Unsplash
Top comments (2)
BloC was presented at DartConf 2018 by Paolo Soares.
Hi, yeah you are right. Thank you, I have corrected my statement.