I have already written a post about bottom navigation bar architecture in Flutter. Since then I have changed my toolbox and updated this architecture. These days I mostly use flutter_bloc package for state management (instead of Provider) and auto_route for navigation.
auto_route
is a code generator for Flutter. You list all routes in a single
place and it will then generate all useful things like route generator and
argument classes. You can then use named routes without all the hassle.
This post will guide you how to setup your own bottom navigation with
auto_route
and flutter_bloc
. I will omit descriptions of things that have
not changed since the last post. Refer there
for more details.
You can check the example app for this post on my GitHub repository.
Navigation BLoC
I used BLoC instead of a ChangeNotifier
with Provider. To simplify this
example both state and event is a plain int
type that is the current index.
class NavigationBloc extends Bloc<int, int> {
NavigationBloc() : super(NavigationTabs.first);
@override
Stream<int> mapEventToState(int event) async* {
yield event;
}
final tabs = const <NavigationTab>[
NavigationTab(
name: 'First',
icon: Icon(Icons.home),
initialRoute: Routes.firstScreen,
),
NavigationTab(
name: 'Second',
icon: Icon(Icons.account_circle_rounded),
initialRoute: Routes.secondScreen,
),
NavigationTab(
name: 'Third',
icon: Icon(Icons.settings),
initialRoute: Routes.thirdScreen,
),
];
Future<bool> onWillPop() async {
// ...
}
}
Instead of using top-level constants for each tab index I use static class. This
is similar to the Icons
class in Flutter and should be more familiar.
class NavigationTabs {
/// Default constructor is private because this class will be only used for
/// static fields and you should not instantiate it.
NavigationTabs._();
static const first = 0;
static const second = 1;
static const third = 3;
}
Navigation tab model
Each tab is declared as a NavigationTab
class. Since my last post this class
is stripped down for simplicity. Important is the name
field which is used to
name each nested ExtendedNavigator
.
@immutable
class NavigationTab {
const NavigationTab({
this.name,
this.icon,
this.initialRoute,
});
final String name;
final Widget icon;
final String initialRoute;
}
Routers
auto_route
example repo
for parallel navigation suggests creating router class for each tab. From my
experience this complicates things and brings redundancy. I use a single router
and then reuse it while using a different initialRoute
.
@MaterialAutoRouter(
routes: [
MaterialRoute<void>(page: Root),
MaterialRoute<void>(page: FirstScreen),
MaterialRoute<void>(page: SecondScreen),
MaterialRoute<void>(page: ThirdScreen),
MaterialRoute<void>(page: PushedScreen),
],
)
class $AppRouter {}
Don't forget to run build_runner
to generate all files. You can use my package
build_runner_helper if you don't
want to type this command every time.1
flutter packages pub run build_runner build --delete-conflicting-outputs
Declare the root ExtendedNavigator
in your MaterialApp
. Don't forget to fill
the initialRoute
.
class ExampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => NavigationBloc(),
child: MaterialApp(
// ...
builder: ExtendedNavigator.builder(
router: AppRouter(),
initialRoute: Routes.root,
),
),
);
}
}
Root Widget
Root
Widget is now wrapped inside BlocBuilder
and uses ExtendedNavigator
instead of plain Navigator
.
class Root extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bloc = context.bloc<NavigationBloc>();
return BlocBuilder<NavigationBloc, int>(
cubit: bloc,
builder: (context, state) {
return WillPopScope(
onWillPop: bloc.onWillPop,
child: Scaffold(
body: IndexedStack(
index: state,
children: List.generate(bloc.tabs.length, (index) {
final tab = bloc.tabs[index];
return TickerMode(
// ...
child: Offstage(
// ...
child: ExtendedNavigator(
initialRoute: tab.initialRoute,
name: tab.name,
router: AppRouter(),
),
),
);
}),
),
bottomNavigationBar: BottomNavigationBar(
// ...
),
),
);
},
);
}
}
Footnotes
-
You obviously can put an alias into your
.zshrc
file if you have a UNIX-like system. I tend to frequently switch between Mac and Windows and managing each platform tends to get annoying. ↩