Nested Navigation In Flutter

Chris Elliot
5 min readDec 6, 2022
Nested Navigation in Flutter

Today we explore how we can use the Navigator class to transition between screens inside a screen, bottom drawer or modal. Another benefit of nested navigation is the ability to provide blocs “globally” to a specific set of screens and not to the whole app.

Introduction

The Navigator class manages a stack of Route objects and provides us with the ability to push and pop to different. We won’t dive too deep into the technicalities of the api as most flutter devs have been using the navigator class since their very first day of developing. Many flutter devs would like to utilise this alongside the modal_bottom_sheet package so that is what we will walk through.

How To Create A Nested Navigation Flow

We will assume you have already got a project set up and you have your main Navigator high up the widget tree.

1. First we need to open our bottom sheet.

void showMyBottomSheet(BuildContext context, CubitA cubitA) {
showMaterialModalBottomSheet(
context: context,
builder: (context) => MyBottomSheet(cubitA: cubitA),
);
}

2. Now lets set up our MyBottomSheet class.

Here we will pass the blocs or cubits we want to provide to every subsequent screen in our nested navigator.

class MyBottomSheet extends StatelessWidget {
static const String id = '/MyBottomSheet';
const MyBottomSheet({Key? key, required this.cubitA}) : super(key: key);

final CubitA cubitA;

@override
Widget build(BuildContext context) {
return MultiBlocProvider(providers: [
BlocProvider.value(value: cubitA),
BlocProvider(create: (context) => CubitB()),
], child: const _MyBottomSheetBody());
}
}

In the example we have provided cubitA, which we created higher up the widget tree, via BlocProvider.value and created a new CubitB.

3. Creating our Navigator

Now it is time to create our new Navigator. First we will create our routes class. We create a switch and use our ids we define in each of our screens as the case. We won’t create our screens until step 5 but feel free to skip ahead and come back if you prefer to create them now.

class BottomSheetRoutes {
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case FirstScreen.id:
return MaterialPageRoute(builder: (context) => const FirstScreen());
case SecondScreen.id:
return MaterialPageRoute(builder: (_) => const SecondScreen());

default:
return _errorRoute();
}
}

static Route<dynamic> _errorRoute() {
return MaterialPageRoute(builder: (_) => const Scaffold(body: Center(child: Text('Error}'))));
}
}

Next, we will create our Navigator. You may be familiar with the look of this from your Material App class.

final nestedNavigatorKey = GlobalKey<NavigatorState>();

class NestedNavigator extends StatelessWidget {
const NestedNavigator({Key? key, required this.child}) : super(key: key);

final Widget child;

@override
Widget build(BuildContext context) {
return Navigator(
key: nestedNavigatorKey,
onGenerateRoute: BottomSheetRoutes.generateRoute,
onGenerateInitialRoutes: (navigator, initialRoute) => [
MaterialPageRoute(builder: (context) => child),
],
);
}
}

4. Now let’s add some ui to our bottom sheet.

For example, we may want to show a bar at the top of it to hint to users they can swipe down to close our bottom sheet. We want this to be static and not animate when we go to our next screen so we need to put it above our nested navigator.

Here is our simple indicator:

class DragDownIndicator extends StatelessWidget {
const DragDownIndicator({
Key? key,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
margin: const EdgeInsets.only(top: 8, bottom: 32),
width: 45,
height: 6,
decoration: BoxDecoration(
color: Colors.grey[500],
borderRadius: BorderRadius.circular(10),
),
),
],
);
}
}

And here is our BottomSheetUI:

class _MyBottomSheetBody extends StatelessWidget {
const _MyBottomSheetBody({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height * 0.9,
width: double.infinity,
padding: EdgeInsets.only(
// We use viewInsets to calculate the height of the keyboard. You don't need
// this if you don't have a text field in your bottom sheet.
bottom: MediaQuery.of(context).viewInsets.bottom > 0
? MediaQuery.of(context).viewInsets.bottom + 45
: MediaQuery.of(context).viewInsets.bottom),
child:
Column(mainAxisSize: MainAxisSize.min, children: [
const DragDownIndicator(),
Expanded(
child: CustomScrollView(
physics: const ClampingScrollPhysics(),
shrinkWrap: true,
controller: ModalScrollController.of(context),
slivers: const <Widget>[
SliverFillRemaining(hasScrollBody: false, child: SizedBox())
]))
]));
}
}

We also need a function to open our bottom sheet so we add this:

void openBottomSheet(BuildContext context, AuthCubit authCubit) {
showMaterialModalBottomSheet(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
context: context,
builder: (context) => MyBottomSheet(authCubit: authCubit));
}

Now if we call this from a button it should look like this:

5. Create our nested navigation screens.

For the purposes of this tutorial we will create two very simple screens

Screen One:

class FirstScreen extends StatelessWidget {
const FirstScreen({Key? key}) : super(key: key);

static const String id = '/first_screen';

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Screen One'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pushNamed(SecondScreen.id);
},
child: const Text('Go to Screen Two'),
),
),
);
}
}

Screen Two:

class SecondScreen extends StatelessWidget {
static const String id = '/second_screen';
const SecondScreen({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: const Center(
child: Text("Second Screen"),
),
);
}
}

6. Time to put it together. Head back to your _MyBottomSheetBody class and replace the empty sized box with your Nested Navigator and First Screen as its child.

class _MyBottomSheetBody extends StatelessWidget {
const _MyBottomSheetBody({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return CustomBottomSheet(
child: Column(mainAxisSize: MainAxisSize.min, children: [
const DragDownIndicator(),
Expanded(
child: CustomScrollView(
physics: const ClampingScrollPhysics(),
shrinkWrap: true,
controller: ModalScrollController.of(context),
slivers: const <Widget>[
SliverFillRemaining(
hasScrollBody: false,
child: BottomSheetNestedNavigator(child: FirstScreen()),
)
]))
]));
}
}

Now your bottom sheet should look something like this:

And when you press the go to screen two button you will get a nice animation inside the sheet to your next screen.

Bonus

As we provided our blocs/cubits above a navigator you can access the state of your bloc on any screen without having to pass the bloc as a route argument and use BlocProvider.value.

For example we can do this on our second screen:

class SecondScreen extends StatelessWidget {
static const String id = '/second_screen';
const SecondScreen({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: BlocBuilder<CubitB, CubitB>(builder: (context, state) => Text(state.someData)),
),
);
}
}

And there we have it, beautiful nested navigation. Let me know in the comments if this tutorial helped you or if you think it can be improved!

--

--