Navegación Horizontal con BottomNavigationBar
En el capítulo anterior viste que Navigator maneja una pila: cada pantalla nueva se apila encima y Atrás la desapila (navegación jerárquica, drilling down). Pero una app también tiene secciones principales (Cursos, Guardados, Perfil) entre las que el usuario salta con un toque, sin ir "más adentro". Eso es la navegación horizontal por pestañas, y no usa la pila.
En este capítulo aprenderás:
- Por qué cambiar de pestaña y abrir un detalle son dos movimientos distintos y cuál usar en cada caso.
- El patrón de la
Screenanfitriona: un único widget con estado que guarda un índice y decide qué página mostrar. - Cómo
IndexedStackconserva lo que el usuario escribió en cada pestaña. - En qué archivos vive cada parte del código.
1. Dos movimientos: horizontal y jerárquico
La regla de oro de Flutter cabe en una frase:
Cambiar de pestaña es
setState. Abrir un detalle espush.
Un error muy común al empezar es usar Navigator.pushNamed para cambiar entre las pestañas del menú inferior, o al revés, usar setState para mostrar el detalle de un producto. Prueba ambos movimientos en el teléfono y mira qué pasa con la pila de rutas de la derecha:
| Criterio | Cambiar de pestaña (setState) | Abrir un detalle (Navigator.push) |
|---|---|---|
| Dirección | Horizontal: entre secciones hermanas. | Jerárquica: de un resumen a un elemento concreto. |
| Pila de rutas | No cambia: sigue con una ruta (MainScreen). | Crece: se apila una ruta nueva. |
| Barra inferior | Siempre visible. | Cubierta por la nueva pantalla. |
| Botón Atrás | No existe: no hay relación padre-hijo. | Automático: el AppBar dibuja la flecha (pop). |
Para decidir en un caso nuevo, hazte estas preguntas en orden:
¿Es una sección principal a la que el usuario volverá a menudo?
├── SÍ ──> Pestaña (NavigationBar + setState).
└── NO ──> ¿Nace de tocar un elemento para verlo o editarlo?
├── SÍ ──> Navigator.push / pushNamed (cubre la pantalla).
└── NO ──> ¿Es una acción rápida o una confirmación?
└── showModalBottomSheet o showDialog.
2. La Screen anfitriona: un índice que decide qué se ve
Para que las pestañas funcionen hacen falta dos roles distintos:
Screenanfitriona (MainScreen): unStatefulWidgetcon elScaffoldraíz, la barra inferior y una sola variable de estado:int _selectedIndex.Pagede contenido (CursosPage,GuardadosPage,PerfilPage): widgets que dibujan solo el cuerpo de cada sección. No declaran su propioScaffoldni otra barra: devuelven columnas, listas o contenedores.
Al tocar una pestaña, el callback de la barra (onDestinationSelected) llama a setState con el índice nuevo; build() se ejecuta otra vez y el body muestra la página que corresponde.
En Flutter moderno se recomienda NavigationBar con hijos NavigationDestination. Es el sucesor de BottomNavigationBar (indicador en forma de píldora, mejor contraste), y ambos comparten el mismo modelo mental: un índice y setState.
Conservar el estado: IndexedStack frente a la lista directa
Hay dos formas de escribir el body, y se comportan distinto:
_paginas[_selectedIndex]: solo existe la página elegida. Al cambiar de pestaña la anterior se destruye y, al volver, nace de nuevo: se pierde lo que el usuario había escrito o su posición de scroll.IndexedStack(index: _selectedIndex, children: _paginas): todas las páginas permanecen montadas y solo se muestra una. Lo escrito y el scroll se conservan.
Escribe algo en «Buscar», cambia de pestaña y vuelve; luego alterna entre las dos formas y repite:
3. El código, archivo por archivo
El proyecto separa cada rol en su carpeta:
lib/
├── main.dart # arranca la app: MaterialApp(home: MainScreen())
├── screens/ # pantallas completas (tienen Scaffold)
│ ├── main_screen.dart # la anfitriona: _selectedIndex + NavigationBar
│ └── detalle_curso_screen.dart # el detalle, que se abre con push
└── pages/ # cuerpos de las pestañas (sin Scaffold)
├── cursos_page.dart
├── guardados_page.dart
└── perfil_page.dart
La diferencia entre las dos carpetas es la misma de la sección anterior: una screen es una pantalla completa con su Scaffold; una page es solo el contenido que la anfitriona coloca en su body.
Toca una pestaña o un curso en el teléfono y mira qué archivo se abre y qué línea corre; o pulsa ▶ en el código y observa cómo cambian el teléfono y la pila. Con el botón cambiar a lista directa (en main_screen.dart) puedes comprobar qué se pierde sin IndexedStack.
4. Buenas prácticas y errores habituales
- No anides
Scaffoldinnecesariamente: si la anfitriona ya tiene elScaffoldcon su barra inferior, lasPageno necesitan otro, salvo que una pestaña requiera su propioFloatingActionButtono fondo. - Usa
IndexedStackcuando una pestaña tenga estado que importe: formularios a medias, listas con scroll o paginación. Cuesta más memoria, pero evita que el usuario pierda su lugar. - El detalle siempre tapa la barra inferior: si el usuario abre un producto, la barra no debe competir por atención; la cubre el
Scaffolddel detalle gracias aNavigator.push. - Una pestaña no se abre con
pushNamed: apilaría una copia de la anfitriona en cada toque y Atrás tendría que deshacerlas una por una.