Crear un flujo de navegación anidado
Cómo implementar un flujo con navegación anidada.
Las apps acumulan docenas y luego cientos de rutas con el tiempo.
Algunas de tus rutas tienen sentido como rutas de nivel superior (globales).
Por ejemplo, "/", "profile", "contact", "social_feed" son todas
posibles rutas de nivel superior dentro de tu app.
Pero imagina que definieras cada ruta posible en tu
widget Navigator de nivel superior. La lista sería muy larga,
y muchas de estas rutas estarían
mejor gestionadas si estuvieran anidadas dentro de otro widget.
Considera un flujo de configuración de Internet de las Cosas (IoT) para una bombilla inalámbrica que controlas con tu app. Este flujo de configuración consta de cuatro páginas:
- Página
find_devices: Buscar bombillas cercanas. - Página
select_device: Seleccionar la bombilla que deseas añadir. - Página
connecting: Añadir la bombilla. - Página
finished: Completar la configuración.
Podrías orquestar este comportamiento desde tu widget
Navigator de nivel superior. Sin embargo, tiene más sentido definir un segundo widget
Navigator anidado dentro de tu widget SetupFlow,
y dejar que el Navigator anidado asuma la propiedad de las cuatro páginas
en el flujo de configuración. Esta delegación de la navegación facilita
un mayor control local, lo cual es
generalmente preferible al desarrollar software.
La siguiente animación muestra el comportamiento de la app:
En esta receta, implementarás un flujo de configuración IoT de cuatro páginas
que mantiene su propia navegación anidada debajo
del widget Navigator de nivel superior.
Prepararse para la navegación
#Esta app IoT tiene dos pantallas de nivel superior, junto con el flujo de configuración. Define estos nombres de rutas como constantes para que puedan ser referenciados dentro del código.
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';
Las pantallas de inicio y configuración (settings) están referenciadas con
nombres estáticos. Las páginas del flujo de configuración, sin embargo,
usan dos rutas para crear sus nombres de ruta:
un prefijo /setup/ seguido del nombre de la página específica.
Al combinar las dos rutas, tu Navigator puede determinar
que un nombre de ruta está destinado al flujo de configuración sin
reconocer todas las páginas individuales asociadas con
el flujo de configuración.
El Navigator de nivel superior no es responsable de identificar
las páginas individuales del flujo de configuración. Por lo tanto, tu Navigator
de nivel superior necesita analizar el nombre de la ruta entrante para
identificar el prefijo del flujo de configuración. Necesitar analizar el nombre de la ruta
significa que no puedes usar la propiedad routes de tu Navigator
de nivel superior. En su lugar, debes proporcionar una función para la
propiedad onGenerateRoute.
Implementa onGenerateRoute para retornar el widget apropiado
para cada una de las tres rutas de nivel superior.
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,
);
},
Ten en cuenta que las rutas de inicio (home) y configuración (settings) se comparan con nombres de ruta
exactos. Sin embargo, la condición de la ruta del flujo de configuración solo
busca un prefijo. Si el nombre de la ruta contiene el prefijo del flujo de
configuración, entonces el resto del nombre de la ruta se ignora
y se pasa al widget SetupFlow para que lo procese.
Esta división del nombre de la ruta es lo que permite al Navigator
de nivel superior ser agnóstico respecto a las diversas subrutas
dentro del flujo de configuración.
Crea un widget stateful llamado SetupFlow que
acepte un nombre de ruta.
class SetupFlow extends StatefulWidget {
const SetupFlow({super.key, required this.setupPageRoute});
final String setupPageRoute;
@override
State<SetupFlow> createState() => SetupFlowState();
}
class SetupFlowState extends State<SetupFlow> {
//...
}
Mostrar una app bar para el flujo de configuración
#El flujo de configuración muestra una app bar persistente que aparece en todas las páginas.
Retorna un widget Scaffold desde el método build()
de tu widget SetupFlow,
e incluye el widget AppBar deseado.
@override
Widget build(BuildContext context) {
return Scaffold(appBar: _buildFlowAppBar(), body: const SizedBox());
}
PreferredSizeWidget _buildFlowAppBar() {
return AppBar(title: const Text('Bulb Setup'));
}
La app bar muestra una flecha de retroceso y sale del flujo de configuración cuando se presiona la flecha. Sin embargo, salir del flujo hace que el usuario pierda todo el progreso. Por lo tanto, se le solicita al usuario confirmar si desea salir del flujo de configuración.
Pídele al usuario que confirme si desea salir del flujo de configuración, y asegúrate de que el aviso aparezca cuando el usuario presione el botón físico de retroceso en su dispositivo.
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'),
);
}
Cuando el usuario toca la flecha de retroceso en la app bar, o presiona el botón de retroceso en su dispositivo, aparece un diálogo de alerta para confirmar que el usuario desea salir del flujo de configuración. Si el usuario presiona Leave, entonces el flujo de configuración se retira (pops) de la pila de navegación de nivel superior. Si el usuario presiona Stay, entonces la acción se ignora.
Es posible que notes que Navigator.pop()
es invocado por ambos botones, Leave y
Stay. Para ser claros,
esta acción pop() retira el diálogo de alerta de
la pila de navegación, no el flujo de configuración.
Generar rutas anidadas
#El trabajo del flujo de configuración es mostrar la página adecuada dentro del flujo.
Añade un widget Navigator a SetupFlow,
e implementa la propiedad onGenerateRoute.
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,
);
}
La función _onGenerateRoute funciona igual que
para un Navigator de nivel superior. Se pasa un objeto
RouteSettings a la función,
el cual incluye el name de la ruta.
Basándose en ese nombre de ruta,
se retorna una de las cuatro páginas del flujo.
La primera página, llamada find_devices,
espera unos segundos para simular el escaneo de red.
Después del período de espera, la página invoca su callback.
En este caso, ese callback es _onDiscoveryComplete.
El flujo de configuración reconoce que, cuando la búsqueda de dispositivos
se ha completado, se debe mostrar la página de selección de dispositivos.
Por lo tanto, en _onDiscoveryComplete, la _navigatorKey
indica al Navigator anidado que navegue a la
página select_device.
La página select_device le pide al usuario que seleccione un
dispositivo de una lista de dispositivos disponibles. En esta receta,
solo se presenta un dispositivo al usuario.
Cuando el usuario toca un dispositivo, se invoca el callback
onDeviceSelected. El flujo de configuración reconoce que,
cuando se selecciona un dispositivo, se debe mostrar la página de conexión.
Por lo tanto, en _onDeviceSelected, la
_navigatorKey indica al Navigator anidado
que navegue a la página "connecting".
La página connecting funciona de la misma manera que la
página find_devices. La página connecting espera
unos segundos y luego invoca su callback.
En este caso, el callback es _onConnectionEstablished.
El flujo de configuración reconoce que, cuando se establece una conexión,
se debe mostrar la página final. Por lo tanto,
en _onConnectionEstablished, la _navigatorKey
indica al Navigator anidado que navegue a la
página finished.
La página finished le proporciona al usuario un botón Finish.
Cuando el usuario toca Finish,
se invoca el callback _exitSetup, el cual retira (pops) todo el
flujo de configuración de la pila del Navigator de nivel superior,
llevando al usuario de regreso a la pantalla de inicio.
¡Felicitaciones! Implementaste una navegación anidada con cuatro subrutas.
Ejemplo interactivo
#Ejecutar la app:
- En la pantalla Add your first bulb, haz clic en el FAB, que se muestra con un signo más, +. Esto te lleva a la pantalla Select a nearby device. Se muestra una sola bombilla.
- Haz clic en la bombilla de la lista. Aparece una pantalla de Finished!.
- Haz clic en el botón Finished para regresar a la primera pantalla.
A menos que se indique lo contrario, la documentación de este sitio refleja Flutter 3.44.0. Página actualizada por última vez el 2026-05-05. Ver código fuente oreportar un problema.