La estructura principal de una app es la organización de sus pantallas base.
Ya sabemos empujar pantallas (push) y volver atrás (pop). Pero las aplicaciones modernas no son solo una pila infinita de pantallas.
Muchas tienen una estructura fija con un menú inferior o lateral que permite cambiar entre sus secciones principales (Inicio, Buscador o Perfil).
Hoy vamos a construir esa estructura usando el BottomNavigationBar.
La lógica del menú
A diferencia de Navigator.push, aquí no estamos apilando una ruta nueva. Cambiamos el contenido del body del Scaffold según el botón del menú que hayamos tocado.
Por tanto, necesitamos:
- Un
StatefulWidget(porque el índice de la pestaña seleccionada cambia). - Una variable
intpara saber qué pestaña está activa (0, 1, 2…). - Una lista de
Widgets(las pantallas que queremos mostrar).
Implementación paso a paso
Vamos a crear una MainScreen que será el contenedor de nuestra App.
Preparar el estado
Definimos el índice seleccionado y la lista de pantallas.
class MainScreen extends StatefulWidget {
@override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
int _indiceSeleccionado = 0; // Empieza en la primera pestaña
// Lista de pantallas que queremos mostrar
final List<Widget> _pantallas = [
PantallaInicio(),
PantallaBuscar(),
PantallaPerfil(),
];
// Función para cambiar el índice al tocar el menú
void _cambiarPestaña(int index) {
setState(() {
_indiceSeleccionado = index;
});
}
Añadir Scaffold y BottomNavigationBar
Ahora montamos la estructura visual.
@override
Widget build(BuildContext context) {
return Scaffold(
// En el body mostramos la pantalla correspondiente al índice
body: _pantallas[_indiceSeleccionado],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _indiceSeleccionado, // Le decimos cuál está activa
onTap: _cambiarPestaña, // Qué hacer al tocar
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Inicio',
),
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: 'Buscar',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: 'Perfil',
),
],
),
);
}
}
¡Ya funciona! Si tocas los iconos, el cuerpo cambia. Pero… tenemos un problema grave.
Conservar el estado de las pestañas
Tal y como lo hemos hecho arriba (body: _pantallas[_indiceSeleccionado]), cada vez que cambias de pestaña, Flutter destruye la pantalla anterior y crea la nueva desde cero.
Esto significa que:
- Si hiciste scroll en “Inicio”, al volver estarás arriba del todo.
- Si escribiste algo en un formulario en “Buscar”, al volver se habrá borrado.
Para evitarlo y mantener las pantallas vivas en memoria, podemos usar el widget IndexedStack.
Usando IndexedStack
Este widget funciona como un Stack: apila todas las pantallas una encima de otra, pero solo muestra la que coincida con el índice. Las demás están ocultas, pero vivas.
@override
Widget build(BuildContext context) {
return Scaffold(
// ⚠️ CAMBIO IMPORTANTE:
body: IndexedStack(
index: _indiceSeleccionado,
children: _pantallas, // Le pasamos TODAS las pantallas
),
bottomNavigationBar: BottomNavigationBar(...)
);
}
Ahora, al cambiar de pestaña, el estado se conserva. El coste es que todas las pantallas permanecen montadas, por lo que en una aplicación grande puede convenir una estrategia de navegación más específica.
NavigationBar en Material 3
Flutter evoluciona rápido. Aunque BottomNavigationBar es el clásico, el nuevo estándar de Material 3 se llama simplemente NavigationBar.
Es más moderno, los botones son un poco más altos y tienen una animación de “píldora” al seleccionarse.
bottomNavigationBar: NavigationBar(
selectedIndex: _indiceSeleccionado,
onDestinationSelected: _cambiarPestaña,
destinations: [
NavigationDestination(icon: Icon(Icons.home), label: 'Inicio'),
NavigationDestination(icon: Icon(Icons.search), label: 'Buscar'),
NavigationDestination(icon: Icon(Icons.person), label: 'Perfil'),
],
),
La lógica es idéntica, solo cambia el diseño visual.
Menú lateral con Drawer
Si prefieres un menú de hamburguesa que sale del lateral, el Scaffold tiene la propiedad drawer.
La lógica es un poco distinta: el Drawer suele usarse para opciones secundarias (Configuración o Ayuda), no para la navegación principal, ya que está oculto y requiere dos acciones (abrir y seleccionar).
Scaffold(
appBar: AppBar(title: Text("Mi App")),
drawer: Drawer(
child: ListView(
children: [
DrawerHeader(child: Text("Menú")),
ListTile(
leading: Icon(Icons.settings),
title: Text("Configuración"),
onTap: () {
// Cerramos el drawer primero
Navigator.pop(context);
// Navegamos
Navigator.push(...);
},
)
],
),
),
body: ...
)