Flutter BottomNavigationBar with BLoC pattern

Sandro Lovnički
7 min readAug 11, 2019

--

In this article, we will be building a simple example application with Flutter demonstrating the usage of BottomNavigationBar widget with BLoC design pattern.

Full source of this app can be found here at GitHub.

If you are unfamiliar with BLoC pattern, you can read more at http://flutterdevs.com/blog/bloc-pattern-in-flutter-part-1/. The basic idea of BLoC pattern is based on Streams and Sinks which are used to provide and consume a continuous flow of data. Dealing with Streams and Sinks directly can be cumbersome, so we will be using some great packages; bloc and flutter_bloc.

Our app should look something like this when finished:

Let’s get started

First things first, let’s add our packages to pubspec.yaml under ‘dependencies’. Notice that we are also adding an equatable and meta packages. Equatable is very useful in reducing the amount of boilerplate code for determining whether the two objects are equal regarding their properties, and this package is from the same developer as bloc and flutter_bloc. Meta is used for annotating parts of code.

dependencies:
flutter:
sdk: flutter

bloc: ^0.14.0 # https://pub.dev/packages/bloc
flutter_bloc: ^0.19.0 # https://pub.dev/packages/flutter_bloc
equatable: ^0.3.0 # https://pub.dev/packages/equatable
meta: ^1.1.6 # https://pub.dev/packages/meta

Directory structure

Although it is not necessary to have so much separation for this small example app, I think this structure is a very good practice as projects get bigger. Our lib/ will look something like this when we finish making the app.

lib/
| repositories/
| api/
| models/
| repositories.dart
| first_page_repository.dart
| second_page_repository.dart
| blocs/
| bottom_navigation/
| bottom_navigation.dart
| bottom_navigation_event.dart
| bottom_navigation_state.dart
| bottom_navigation_bloc.dart
| ui/
| pages/
| pages.dart
| first_page.dart
| second_page.dart
| app_screen.dart
| main.dart

repositories/ should be for fetching and storing data, blocs/ for different blocs that manipulate objects between repositories and UI and ui/ for UI components.

We will have nothing inside api/ and models/, but they can be used by the *_repository.dart files for fetching and storing data in appropriate format. Our page data will be just String and int that we will generate locally within repository files.

Repositories

Our FirstPageRepository class will be responsible for providing data contained in the first page and SecondPageRepository will provide data for second page. Simple enough. Let’s implement those classes:

class FirstPageRepository {
String _data;

Future<void> fetchData() async {
// simulate real data fetching
await Future.delayed(Duration(milliseconds: 600));
// store dummy data
_data = 'First Page';
}

String get data => _data;
}

And the second…

import 'dart:math';

class SecondPageRepository {
int _data;

Future<void> fetchData() async {
// simulate real data fetching
await Future.delayed(Duration(milliseconds: 600));
// store dummy data
_data = Random().nextInt(1000);
}

int get data => _data;
}

These classes and their methods will be used by our BottomNavigationBloc which we will see soon enough.

Bottom Navigation BLoC

The main things to implement here are Events, States and a method mapEventToState to transform Events to States.

Events

bloc package is designed to convert Events into States. Event is considered to be some user interaction with UI (e.g. button press) and State is a product of this interaction that gets sent back to the UI for its rebuild based on data the State is carrying (explicitly or implicitly).

Our only* Event will be PageTapped which will also carry information which page was tapped; first or second. Well, we will also add an Event AppStarted for the sake of diversity, but this Event should usually be a part of some other BLoC.

import 'package:meta/meta.dart';
import 'package:equatable/equatable.dart';

abstract class BottomNavigationEvent extends Equatable {
BottomNavigationEvent([List props = const []]) : super(props);
}

class AppStarted extends BottomNavigationEvent {
@override
String toString() => 'AppStarted';
}

class PageTapped extends BottomNavigationEvent {
final int index;

PageTapped({@required this.index}) : super([index]);

@override
String toString() => 'PageTapped: $index';
}

We have our base class BottomNavigationEvent that extends Equatable (for reasons mentioned at the beginning of this article) and our two Events extend this further. We are overriding toString methods because we will see later how to use BlocDelegate to print our Event-State transitions as they are happening.

States

Now we need to think in what States will our app be regarding these Events, i.e. what changes should happen in the UI.

As our app has the BottomNavigationBar with highlighted current tab, we need to let the UI know when this has been changed. For this, we will have CurrentIndexChanged state which will carry the currentIndex to the UI. Also, when we switch the tab, some data should be loaded and drawn. For the loading State, we have PageLoading and for the loaded States, we have FirstPageLoaded and SecondPageLoaded which will carry the page’s data.

import 'package:meta/meta.dart';
import 'package:equatable/equatable.dart';

@immutable
abstract class BottomNavigationState extends Equatable {
BottomNavigationState([List props = const []]) : super(props);
}

class CurrentIndexChanged extends BottomNavigationState {
final int currentIndex;

CurrentIndexChanged({@required this.currentIndex}) : super([currentIndex]);

@override
String toString() => 'CurrentIndexChanged to $currentIndex';
}

class PageLoading extends BottomNavigationState {
@override
String toString() => 'PageLoading';
}

class FirstPageLoaded extends BottomNavigationState {
final String text;

FirstPageLoaded({@required this.text}) : super([text]);

@override
String toString() => 'FirstPageLoaded with text: $text';
}

class SecondPageLoaded extends BottomNavigationState {
final int number;

SecondPageLoaded({@required this.number}) : super([number]);

@override
String toString() => 'SecondPageLoaded with number: $number';
}

The implementation of BottomNavigationState is very similar in approach as was for BottomNavigationEvent .

BLoC

The last thing to do is to implement the logic for converting Events to States. This is done by extending a Bloc class provided by bloc package and overriding initialState and mapEventToState .

import 'dart:async';

import 'package:bloc/bloc.dart';

import 'bottom_navigation_event.dart';
import 'bottom_navigation_state.dart';
import 'package:bottom_navigation_bloc/repositories/first_page_repository.dart';
import 'package:bottom_navigation_bloc/repositories/second_page_respository.dart';

class BottomNavigationBloc extends Bloc<BottomNavigationEvent, BottomNavigationState> {
final FirstPageRepository firstPageRepository;
final SecondPageRepository secondPageRepository;
int currentIndex = 0;

BottomNavigationBloc({
this.firstPageRepository,
this.secondPageRepository
}) : assert(firstPageRepository != null),
assert(secondPageRepository != null);

@override
BottomNavigationState get initialState => PageLoading();

@override
Stream<BottomNavigationState> mapEventToState(BottomNavigationEvent event) async* {
if (event is AppStarted) {
this.dispatch(PageTapped(index: this.currentIndex));
}
if (event is PageTapped) {
this.currentIndex = event.index;
yield CurrentIndexChanged(currentIndex: this.currentIndex);
yield PageLoading();

if (this.currentIndex == 0) {
String data = await _getFirstPageData();
yield FirstPageLoaded(text: data);
}
if (this.currentIndex == 1) {
int data = await _getSecondPageData();
yield SecondPageLoaded(number: data);
}
}
}

Future<String> _getFirstPageData() async {
String data = firstPageRepository.data;
if (data == null) {
await firstPageRepository.fetchData();
data = firstPageRepository.data;
}
return data;
}

Future<int> _getSecondPageData() async {
int data = secondPageRepository.data;
if (data == null) {
await secondPageRepository.fetchData();
data = secondPageRepository.data;
}
return data;
}
}

Notice that we have here our repositories as members so BLoC doesn’t have to deal with how the data is being fetched, via network or locally and what not. BLoC’s responsibility is to get the data, decide what to do with it and send appropriate states to the UI. This is done inside mapEventToState method.

UI

The last thing to do is to write the UI components for this app.

Pages

We start of with pages. They will be StatelessWidget that just render themselves with data they are constructed with.

import 'package:flutter/material.dart';

class FirstPage extends StatelessWidget {
final String text;

FirstPage({this.text}) : super();

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('My text is: $text'),
),
);
}
}

And the second…

import 'package:flutter/material.dart';

class SecondPage extends StatelessWidget {
final int number;

SecondPage({this.number}) : super();

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('My number is: $number'),
),
);
}
}

AppScreen

This StatelessWidget redraws its body and bottomNavigationBar depending on the state yielded from BottomNavigationBloc , by using the BlocBuilder from flutter_bloc package.

Also, bottomNavigationBar dispatches the PageTapped Events to the BottomNavigationBloc .

import 'package:flutter/material.dart';

import 'package:flutter_bloc/flutter_bloc.dart';

import 'package:bottom_navigation_bloc/blocs/bottom_navigation/bottom_navigation.dart';
import 'package:bottom_navigation_bloc/ui/pages/pages.dart';

class AppScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final BottomNavigationBloc bottomNavigationBloc = BlocProvider.of<BottomNavigationBloc>(context);

return Scaffold(
appBar: AppBar(
title: Text('Bottom Navigation with BLoC'),
),
body: BlocBuilder<BottomNavigationEvent, BottomNavigationState>(
bloc: bottomNavigationBloc,
builder: (BuildContext context, BottomNavigationState state) {
if (state is PageLoading) {
return Center(child: CircularProgressIndicator());
}
if (state is FirstPageLoaded) {
return FirstPage(text: state.text);
}
if (state is SecondPageLoaded) {
return SecondPage(number: state.number);
}
return Container();
},
),
bottomNavigationBar: BlocBuilder<BottomNavigationEvent, BottomNavigationState>(
bloc: bottomNavigationBloc,
builder: (BuildContext context, BottomNavigationState state) {
return BottomNavigationBar(
currentIndex: bottomNavigationBloc.currentIndex,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home, color: Colors.black),
title: Text('First'),
),
BottomNavigationBarItem(
icon: Icon(Icons.all_inclusive, color: Colors.black),
title: Text('Second'),
),
],
onTap: (index) => bottomNavigationBloc.dispatch(PageTapped(index: index)),
);
}
),
);
}
}

main.dart

The last thing we will go through is the main.dart which initializes and runs our MaterialApp. Here, we also initialized an instance of BlocDelegate I mention earlier, which is responsible to print every Event-State transition that is happening in our app.

For the home of our MaterialApp we used BlocProvider to provide the BottomNavigationBloc instance to its child (with necessary instances of FirstPageRepository and SecondPageRepository). As far as the UI is concerned, the home of MaterialApp is AppScreen.

import 'package:flutter/material.dart';

import 'package:bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import 'package:bottom_navigation_bloc/repositories/repositories.dart';
import 'package:bottom_navigation_bloc/blocs/bottom_navigation/bottom_navigation.dart';
import 'package:bottom_navigation_bloc/ui/app_screen.dart';

class SimpleBlocDelegate extends BlocDelegate {
@override
void onTransition(Bloc bloc, Transition transition) {
super.onTransition(bloc, transition);
print(transition);
}
}

void main() {
BlocSupervisor.delegate = SimpleBlocDelegate();
runApp(App());
}

class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider<BottomNavigationBloc>(
builder: (context) => BottomNavigationBloc(
firstPageRepository: FirstPageRepository(),
secondPageRepository: SecondPageRepository(),
)
..dispatch(AppStarted()),
child: AppScreen(),
)
);
}
}

Conclusion

The usage of BLoC pattern leads to a very clean application structure and really good responsibility distribution among objects. Packages like bloc and flutter_bloc make the implementation of BLoC pattern a breeze.

--

--

Sandro Lovnički

Fluttering @FFireEsports 🔥 Maintaining Beamer 💻 Hosting Flutter dArtists 📢 Organizing @FlutterCroatia , @gdgzagreb 🇭🇷 Loving Petra and our cat ❤️