Create a nested navigation flow
                  Apps accumulate dozens and then hundreds of routes over time.
                  Some of your routes make sense as top-level (global) routes.
                  For example, "/", "profile", "contact", "social_feed" are all
                  possible top-level routes within your app.
                  But, imagine that you defined every possible route in your
                  top-level Navigator widget. The list would be very long,
                  and many of these routes would
                  be better handled nested within another widget.
                
Consider an Internet of Things (IoT) setup flow for a wireless light bulb that you control with your app. This setup flow consists of four pages:
find_devicespage: Find nearby bulbs.- 
                    
select_devicepage: Select the bulb that you want to add. connectingpage: Add the bulb.finishedpage: Complete the setup.
                  You could orchestrate this behavior from your top-level
                  Navigator widget. However, it makes more sense to define a second,
                  nested Navigator widget within your SetupFlow widget,
                  and let the nested Navigator take ownership over the four pages
                  in the setup flow. This delegation of navigation facilitates
                  greater local control, which is
                  generally preferable when developing software.
                
The following animation shows the app's behavior:
                  
                
                  In this recipe, you implement a four-page IoT setup
                  flow that maintains its own navigation nested beneath
                  the top-level Navigator widget.
                
Prepare for navigation
#This IoT app has two top-level screens, along with the setup flow. Define these route names as constants so that they can be referenced within code.
const routeHome = '/';
const routeSettings = '/settings';
const routePrefixDeviceSetup = '/setup/';
const routeDeviceSetupStart = '/setup/$routeDeviceSetupStartPage';
const routeDeviceSetupStartPage = 'find_devices';
const routeDeviceSetupSelectDevicePage = 'select_device';
const routeDeviceSetupConnectingPage = 'connecting';
const routeDeviceSetupFinishedPage = 'finished';
                    
                    
                    
                  
                  The home and settings screens are referenced with
                  static names. The setup flow pages, however,
                  use two paths to create their route names:
                  a /setup/ prefix followed by the name of the specific page.
                  By combining the two paths, your Navigator can determine
                  that a route name is intended for the setup flow without
                  recognizing all the individual pages associated with
                  the setup flow.
                
                  The top-level Navigator isn't responsible for identifying
                  individual setup flow pages. Therefore, your top-level
                  Navigator needs to parse the incoming route name to
                  identify the setup flow prefix. Needing to parse the route name
                  means that you can't use the routes property of your top-level
                  Navigator. Instead, you must provide a function for the
                  onGenerateRoute property.
                
                  Implement onGenerateRoute to return the appropriate widget
                  for each of the three top-level paths.
                
onGenerateRoute: (settings) {
  final Widget page;
  if (settings.name == routeHome) {
    page = const HomeScreen();
  } else if (settings.name == routeSettings) {
    page = const SettingsScreen();
  } else if (settings.name!.startsWith(routePrefixDeviceSetup)) {
    final subRoute = settings.name!.substring(
      routePrefixDeviceSetup.length,
    );
    page = SetupFlow(setupPageRoute: subRoute);
  } else {
    throw Exception('Unknown route: ${settings.name}');
  }
  return MaterialPageRoute<void>(
    builder: (context) {
      return page;
    },
    settings: settings,
  );
},
                    
                    
                    
                  
                  Notice that the home and settings routes are matched with exact
                  route names. However, the setup flow route condition only
                  checks for a prefix. If the route name contains the setup
                  flow prefix, then the rest of the route name is ignored
                  and passed on to the SetupFlow widget to process.
                  This splitting of the route name is what allows the top-level
                  Navigator to be agnostic toward the various subroutes
                  within the setup flow.
                
                  Create a stateful widget called SetupFlow that
                  accepts a route name.
                
class SetupFlow extends StatefulWidget {
  const SetupFlow({super.key, required this.setupPageRoute});
  final String setupPageRoute;
  @override
  State<SetupFlow> createState() => SetupFlowState();
}
class SetupFlowState extends State<SetupFlow> {
  //...
}
                    
                    
                    
                  Display an app bar for the setup flow
#The setup flow displays a persistent app bar that appears across all pages.
                  Return a Scaffold widget from your SetupFlow
                  widget's build() method,
                  and include the desired AppBar widget.
                
@override
Widget build(BuildContext context) {
  return Scaffold(appBar: _buildFlowAppBar(), body: const SizedBox());
}
PreferredSizeWidget _buildFlowAppBar() {
  return AppBar(title: const Text('Bulb Setup'));
}
                    
                    
                    
                  The app bar displays a back arrow and exits the setup flow when the back arrow is pressed. However, exiting the flow causes the user to lose all progress. Therefore, the user is prompted to confirm whether they want to exit the setup flow.
Prompt the user to confirm exiting the setup flow, and ensure that the prompt appears when the user presses the hardware back button on their device.
Future<void> _onExitPressed() async {
  final isConfirmed = await _isExitDesired();
  if (isConfirmed && mounted) {
    _exitSetup();
  }
}
Future<bool> _isExitDesired() async {
  return await showDialog<bool>(
        context: context,
        builder: (context) {
          return AlertDialog(
            title: const Text('Are you sure?'),
            content: const Text(
              'If you exit device setup, your progress will be lost.',
            ),
            actions: [
              TextButton(
                onPressed: () {
                  Navigator.of(context).pop(true);
                },
                child: const Text('Leave'),
              ),
              TextButton(
                onPressed: () {
                  Navigator.of(context).pop(false);
                },
                child: const Text('Stay'),
              ),
            ],
          );
        },
      ) ??
      false;
}
void _exitSetup() {
  Navigator.of(context).pop();
}
@override
Widget build(BuildContext context) {
  return PopScope(
    canPop: false,
    onPopInvokedWithResult: (didPop, _) async {
      if (didPop) return;
      if (await _isExitDesired() && context.mounted) {
        _exitSetup();
      }
    },
    child: Scaffold(appBar: _buildFlowAppBar(), body: const SizedBox()),
  );
}
PreferredSizeWidget _buildFlowAppBar() {
  return AppBar(
    leading: IconButton(
      onPressed: _onExitPressed,
      icon: const Icon(Icons.chevron_left),
    ),
    title: const Text('Bulb Setup'),
  );
}
                    
                    
                    
                  When the user taps the back arrow in the app bar, or presses the back button on their device, an alert dialog pops up to confirm that the user wants to leave the setup flow. If the user presses Leave, then the setup flow pops itself from the top-level navigation stack. If the user presses Stay, then the action is ignored.
                  You might notice that the Navigator.pop()
                  is invoked by both the Leave and
                  Stay buttons. To be clear,
                  this pop() action pops the alert dialog off
                  the navigation stack, not the setup flow.
                
Generate nested routes
#The setup flow's job is to display the appropriate page within the flow.
                  Add a Navigator widget to SetupFlow,
                  and implement the onGenerateRoute property.
                
final _navigatorKey = GlobalKey<NavigatorState>();
void _onDiscoveryComplete() {
  _navigatorKey.currentState!.pushNamed(routeDeviceSetupSelectDevicePage);
}
void _onDeviceSelected(String deviceId) {
  _navigatorKey.currentState!.pushNamed(routeDeviceSetupConnectingPage);
}
void _onConnectionEstablished() {
  _navigatorKey.currentState!.pushNamed(routeDeviceSetupFinishedPage);
}
@override
Widget build(BuildContext context) {
  return PopScope(
    canPop: false,
    onPopInvokedWithResult: (didPop, _) async {
      if (didPop) return;
      if (await _isExitDesired() && context.mounted) {
        _exitSetup();
      }
    },
    child: Scaffold(
      appBar: _buildFlowAppBar(),
      body: Navigator(
        key: _navigatorKey,
        initialRoute: widget.setupPageRoute,
        onGenerateRoute: _onGenerateRoute,
      ),
    ),
  );
}
Route<Widget> _onGenerateRoute(RouteSettings settings) {
  final page = switch (settings.name) {
    routeDeviceSetupStartPage => WaitingPage(
      message: 'Searching for nearby bulb...',
      onWaitComplete: _onDiscoveryComplete,
    ),
    routeDeviceSetupSelectDevicePage => SelectDevicePage(
      onDeviceSelected: _onDeviceSelected,
    ),
    routeDeviceSetupConnectingPage => WaitingPage(
      message: 'Connecting...',
      onWaitComplete: _onConnectionEstablished,
    ),
    routeDeviceSetupFinishedPage => FinishedPage(onFinishPressed: _exitSetup),
    _ => throw StateError('Unexpected route name: ${settings.name}!'),
  };
  return MaterialPageRoute(
    builder: (context) {
      return page;
    },
    settings: settings,
  );
}
                    
                    
                    
                  
                  The _onGenerateRoute function works the same as
                  for a top-level Navigator. A RouteSettings
                  object is passed into the function,
                  which includes the route's name.
                  Based on that route name,
                  one of four flow pages is returned.
                
                  The first page, called find_devices,
                  waits a few seconds to simulate network scanning.
                  After the wait period, the page invokes its callback.
                  In this case, that callback is _onDiscoveryComplete.
                  The setup flow recognizes that, when device discovery
                  is complete, the device selection page should be shown.
                  Therefore, in _onDiscoveryComplete, the _navigatorKey
                  instructs the nested Navigator to navigate to the
                  select_device page.
                
                  The select_device page asks the user to select a
                  device from a list of available devices. In this recipe,
                  only one device is presented to the user.
                  When the user taps a device, the onDeviceSelected
                  callback is invoked. The setup flow recognizes that,
                  when a device is selected, the connecting page
                  should be shown. Therefore, in _onDeviceSelected,
                  the _navigatorKey instructs the nested Navigator
                  to navigate to the "connecting" page.
                
                  The connecting page works the same way as the
                  find_devices page. The connecting page waits
                  for a few seconds and then invokes its callback.
                  In this case, the callback is _onConnectionEstablished.
                  The setup flow recognizes that, when a connection is established,
                  the final page should be shown. Therefore,
                  in _onConnectionEstablished, the _navigatorKey
                  instructs the nested Navigator to navigate to the
                  finished page.
                
                  The finished page provides the user with a Finish
                  button. When the user taps Finish,
                  the _exitSetup callback is invoked, which pops the entire
                  setup flow off the top-level Navigator stack,
                  taking the user back to the home screen.
                
Congratulations! You implemented nested navigation with four subroutes.
Interactive example
#Run the app:
- On the Add your first bulb screen, click the FAB, shown with a plus sign, +. This brings you to the Select a nearby device screen. A single bulb is listed.
 - Click the listed bulb. A Finished! screen appears.
 - Click the Finished button to return to the first screen.
 
import 'package:flutter/material.dart';
const routeHome = '/';
const routeSettings = '/settings';
const routePrefixDeviceSetup = '/setup/';
const routeDeviceSetupStart = '/setup/$routeDeviceSetupStartPage';
const routeDeviceSetupStartPage = 'find_devices';
const routeDeviceSetupSelectDevicePage = 'select_device';
const routeDeviceSetupConnectingPage = 'connecting';
const routeDeviceSetupFinishedPage = 'finished';
void main() {
  runApp(
    MaterialApp(
      theme: ThemeData(
        brightness: Brightness.dark,
        appBarTheme: const AppBarTheme(backgroundColor: Colors.blue),
        floatingActionButtonTheme: const FloatingActionButtonThemeData(
          backgroundColor: Colors.blue,
        ),
      ),
      onGenerateRoute: (settings) {
        final Widget page;
        if (settings.name == routeHome) {
          page = const HomeScreen();
        } else if (settings.name == routeSettings) {
          page = const SettingsScreen();
        } else if (settings.name!.startsWith(routePrefixDeviceSetup)) {
          final subRoute = settings.name!.substring(
            routePrefixDeviceSetup.length,
          );
          page = SetupFlow(setupPageRoute: subRoute);
        } else {
          throw Exception('Unknown route: ${settings.name}');
        }
        return MaterialPageRoute<void>(
          builder: (context) {
            return page;
          },
          settings: settings,
        );
      },
      debugShowCheckedModeBanner: false,
    ),
  );
}
@immutable
class SetupFlow extends StatefulWidget {
  static SetupFlowState of(BuildContext context) {
    return context.findAncestorStateOfType<SetupFlowState>()!;
  }
  const SetupFlow({super.key, required this.setupPageRoute});
  final String setupPageRoute;
  @override
  SetupFlowState createState() => SetupFlowState();
}
class SetupFlowState extends State<SetupFlow> {
  final _navigatorKey = GlobalKey<NavigatorState>();
  @override
  void initState() {
    super.initState();
  }
  void _onDiscoveryComplete() {
    _navigatorKey.currentState!.pushNamed(routeDeviceSetupSelectDevicePage);
  }
  void _onDeviceSelected(String deviceId) {
    _navigatorKey.currentState!.pushNamed(routeDeviceSetupConnectingPage);
  }
  void _onConnectionEstablished() {
    _navigatorKey.currentState!.pushNamed(routeDeviceSetupFinishedPage);
  }
  Future<void> _onExitPressed() async {
    final isConfirmed = await _isExitDesired();
    if (isConfirmed && mounted) {
      _exitSetup();
    }
  }
  Future<bool> _isExitDesired() async {
    return await showDialog<bool>(
          context: context,
          builder: (context) {
            return AlertDialog(
              title: const Text('Are you sure?'),
              content: const Text(
                'If you exit device setup, your progress will be lost.',
              ),
              actions: [
                TextButton(
                  onPressed: () {
                    Navigator.of(context).pop(true);
                  },
                  child: const Text('Leave'),
                ),
                TextButton(
                  onPressed: () {
                    Navigator.of(context).pop(false);
                  },
                  child: const Text('Stay'),
                ),
              ],
            );
          },
        ) ??
        false;
  }
  void _exitSetup() {
    Navigator.of(context).pop();
  }
  @override
  Widget build(BuildContext context) {
    return PopScope(
      canPop: false,
      onPopInvokedWithResult: (didPop, _) async {
        if (didPop) return;
        if (await _isExitDesired() && context.mounted) {
          _exitSetup();
        }
      },
      child: Scaffold(
        appBar: _buildFlowAppBar(),
        body: Navigator(
          key: _navigatorKey,
          initialRoute: widget.setupPageRoute,
          onGenerateRoute: _onGenerateRoute,
        ),
      ),
    );
  }
  Route<Widget> _onGenerateRoute(RouteSettings settings) {
    final page = switch (settings.name) {
      routeDeviceSetupStartPage => WaitingPage(
        message: 'Searching for nearby bulb...',
        onWaitComplete: _onDiscoveryComplete,
      ),
      routeDeviceSetupSelectDevicePage => SelectDevicePage(
        onDeviceSelected: _onDeviceSelected,
      ),
      routeDeviceSetupConnectingPage => WaitingPage(
        message: 'Connecting...',
        onWaitComplete: _onConnectionEstablished,
      ),
      routeDeviceSetupFinishedPage => FinishedPage(onFinishPressed: _exitSetup),
      _ => throw StateError('Unexpected route name: ${settings.name}!'),
    };
    return MaterialPageRoute(
      builder: (context) {
        return page;
      },
      settings: settings,
    );
  }
  PreferredSizeWidget _buildFlowAppBar() {
    return AppBar(
      leading: IconButton(
        onPressed: _onExitPressed,
        icon: const Icon(Icons.chevron_left),
      ),
      title: const Text('Bulb Setup'),
    );
  }
}
class SelectDevicePage extends StatelessWidget {
  const SelectDevicePage({super.key, required this.onDeviceSelected});
  final void Function(String deviceId) onDeviceSelected;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 24),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text(
                'Select a nearby device:',
                style: Theme.of(context).textTheme.titleLarge,
              ),
              const SizedBox(height: 24),
              SizedBox(
                width: double.infinity,
                height: 54,
                child: ElevatedButton(
                  style: ButtonStyle(
                    backgroundColor: WidgetStateColor.resolveWith((states) {
                      return const Color(0xFF222222);
                    }),
                  ),
                  onPressed: () {
                    onDeviceSelected('22n483nk5834');
                  },
                  child: const Text(
                    'Bulb 22n483nk5834',
                    style: TextStyle(fontSize: 24),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
class WaitingPage extends StatefulWidget {
  const WaitingPage({
    super.key,
    required this.message,
    required this.onWaitComplete,
  });
  final String message;
  final VoidCallback onWaitComplete;
  @override
  State<WaitingPage> createState() => _WaitingPageState();
}
class _WaitingPageState extends State<WaitingPage> {
  @override
  void initState() {
    super.initState();
    _startWaiting();
  }
  Future<void> _startWaiting() async {
    await Future<dynamic>.delayed(const Duration(seconds: 3));
    if (mounted) {
      widget.onWaitComplete();
    }
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 24),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              const CircularProgressIndicator(),
              const SizedBox(height: 32),
              Text(widget.message),
            ],
          ),
        ),
      ),
    );
  }
}
class FinishedPage extends StatelessWidget {
  const FinishedPage({super.key, required this.onFinishPressed});
  final VoidCallback onFinishPressed;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 24),
          child: SingleChildScrollView(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Container(
                  width: 200,
                  height: 200,
                  decoration: const BoxDecoration(
                    shape: BoxShape.circle,
                    color: Color(0xFF222222),
                  ),
                  child: const Center(
                    child: Icon(
                      Icons.lightbulb,
                      size: 140,
                      color: Colors.white,
                    ),
                  ),
                ),
                const SizedBox(height: 32),
                const Text(
                  'Bulb added!',
                  textAlign: TextAlign.center,
                  style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 32),
                ElevatedButton(
                  style: ButtonStyle(
                    padding: WidgetStateProperty.resolveWith((states) {
                      return const EdgeInsets.symmetric(
                        horizontal: 24,
                        vertical: 12,
                      );
                    }),
                    backgroundColor: WidgetStateColor.resolveWith((states) {
                      return const Color(0xFF222222);
                    }),
                    shape: WidgetStateProperty.resolveWith((states) {
                      return const StadiumBorder();
                    }),
                  ),
                  onPressed: onFinishPressed,
                  child: const Text('Finish', style: TextStyle(fontSize: 24)),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
@immutable
class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: _buildAppBar(context),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 24),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Container(
                width: 200,
                height: 200,
                decoration: const BoxDecoration(
                  shape: BoxShape.circle,
                  color: Color(0xFF222222),
                ),
                child: Center(
                  child: Icon(
                    Icons.lightbulb,
                    size: 140,
                    color: Theme.of(context).scaffoldBackgroundColor,
                  ),
                ),
              ),
              const SizedBox(height: 32),
              const Text(
                'Add your first bulb',
                textAlign: TextAlign.center,
                style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
              ),
            ],
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.of(context).pushNamed(routeDeviceSetupStart);
        },
        child: const Icon(Icons.add),
      ),
    );
  }
  PreferredSizeWidget _buildAppBar(BuildContext context) {
    return AppBar(
      title: const Text('Welcome'),
      actions: [
        IconButton(
          icon: const Icon(Icons.settings),
          onPressed: () {
            Navigator.pushNamed(context, routeSettings);
          },
        ),
      ],
    );
  }
}
class SettingsScreen extends StatelessWidget {
  const SettingsScreen({super.key});
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: _buildAppBar(),
      body: SingleChildScrollView(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: List.generate(8, (index) {
            return Container(
              width: double.infinity,
              height: 54,
              margin: const EdgeInsets.only(left: 16, right: 16, top: 16),
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(8),
                color: const Color(0xFF222222),
              ),
            );
          }),
        ),
      ),
    );
  }
  PreferredSizeWidget _buildAppBar() {
    return AppBar(title: const Text('Settings'));
  }
}
                
              Unless stated otherwise, the documentation on this site reflects Flutter 3.35.5. Page last updated on 2025-10-30. View source or report an issue.