flutter-bottom-navigation-bar-estructura

Navegación inferior en Flutter con NavigationBar

  • 4 min

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:

  1. Un StatefulWidget (porque el índice de la pestaña seleccionada cambia).
  2. Una variable int para saber qué pestaña está activa (0, 1, 2…).
  3. 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;
    });
  }
Copied!

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',
          ),
        ],
      ),
    );
  }
}
Copied!

¡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(...)
  );
}
Copied!

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.

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'),
  ],
),
Copied!

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: ...
)
Copied!