Crear un menú lateral o Drawer en flutter para la navegación en nuestra app

Video thumbnail

Por lo general, el navigation drawer se abre desde el lado izquierdo de la pantalla, pero también puede configurarlo para que se abra desde el lado derecho y ocupa un 70 por ciento de la pantalla, y para cerrarlo, simplemente puede deslizar o hacer clic fuera del drawer.

Quedamos desde la entrada anterior que aprendimos a usar el widget de tipo Dismissible en Flutter.

¿Qué es un menú lateral o Drawer y para qué sirve?

Un menú lateral es un elemento de la interfaz de usuario que se muestra en el costado de una aplicación y que muestra opciones de navegación o acciones secundarias muy usadado este tipo de elemento de interfaz en en el contexto de apps en Flutter o Material Design y Android en general. Se puede abrir deslizando el dedo o tocando un botón o icono, y desplaza el contenido principal de la aplicación para mostrar opciones adicionales. Los usuarios pueden interactuar con las opciones del menú para acceder a diferentes vistas o acciones; es muy usado para la navegación entre distintas páginas.

Un menú lateral o Navigation Drawer simplemente es un panel de navegación que se muestra desde el lado izquierdo o derecho generalmente al presionar un botón de tipo hamburguesa desde el AppBar o Toolbar y que por supuesto podemos implementar en nuestra aplicación en Flutter.

El objetivo de este menú es presentar un conjunto de opciones a nuestro usuario que consiste en funcionalidades que permite realizar la aplicación.

¿Cómo crear un menú lateral?

Empleando Dart específicamente el framework de Flutter lo podemos crear fácilmente en nuestras aplicaciones empleando un widget que nos permite crear dicho componente y lo podemos atar fácilmente a un Scaffold mediante una propiedad llamada drawer.

Creando el Drawer

Aquí tienes que comenzar con lo típico, crear un proyecto limpio en Flutter con Android Studio o Visual Studio Code, quitar el código inicial y creamos un Scaffold:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter NavBar',
      ),
      home: Scaffold(
        appBar: AppBar(title: Text("Menu Lateral"),),
        drawer: CustomDrawer.getDrawer(),
      )
    );
  }
}

Vamos a conocer como podemos crear un Drawer o menú lateral en Flutter al igual que hicimos en Android en una entrada anterior en la cual creamos un menú lateral en Android; vamos a dar toda la implementación del código y luego iremos analizando el código en detalle:

Recuerda que un menú lateral es solo un panel vertical en la cual podemos colocar una colección de elementos que en nuestro caso sería una colección de ítems uno apilado debajo de otro; por lo tanto el Column nos viene perfecto aquí para apilar multitud de widgets para nuestra cabecera y opciones:

 class DrawerItem {
  String title;
  IconData icon;
  DrawerItem(this.title, this.icon);
}
 
class CustomDrawer {
 
  static int selectedDrawerIndex = 1;
 
  static final _drawerItems = [
    DrawerItem("Perfil", Icons.person),
    DrawerItem("Ver eventos", Icons.access_alarm),
    DrawerItem("Crear eventos", Icons.add_alarm),
    DrawerItem("Ver consejos", Icons.web),
    DrawerItem("Aviso legal", Icons.info)
  ];
 
  static _onTapDrawer(int itemPos, BuildContext context){
    Navigator.pop(context); // cerramos el drawer
    selectedDrawerIndex = itemPos;
  }
 
  static Widget getDrawer(BuildContext context) {
    final prefs = new UserPreferences();
    List<Widget> drawerOptions = [];
    // armamos los items del menu
    for (var i = 0; i < _drawerItems.length; i++) {
      var d = _drawerItems[i];
      drawerOptions.add(new ListTile(
        leading: new Icon(d.icon),
        title: new Text(d.title),
        selected: i == selectedDrawerIndex,
        onTap: () => _onTapDrawer(i, context),
      ));
    }
 
    // menu lateral
    return Drawer(
      child: Column(
        children: <Widget>[
          UserAccountsDrawerHeader(
              accountName: Text(prefs.name), accountEmail: Text(prefs.email)),
          Column(children: drawerOptions)
        ],
      ),
    );
  }
}

En términos generales, antes de entrar al detalle de todo el código, la app fue creada para ser consumida mediante métodos estáticos; Es decir, esto es una clase aparte que puedes consumir desde tus páginas en Flutter mediante en método de getDrawer() tal cual hacemos en nuestro curso de:

Items para el menú lateral

Para facilitar el proceso de creación de los ítems o las opciones del menú lateral y tener todo mejor organizado, creamos una clase auxiliar llamada _DrawerItem con los elementos de los cuales van a constar nuestro ítem de menú; para nuestro ejemplo, tiene lo usual, un título o nombre para el ítem y un ícono asociado, pero tu podrías personalizarlo aún más y colocar otros elementos como el color; como puedes ver, esta clase es privada, la razón, es que solamente nos interesa emplearla en la clase que se encarga de definir el panel vertical o menú lateral.

Y desde el código anterior también puedes ver que tenemos un array con los ítems del menú que emplean la clase que definimos y explicamos anteriormente.

Pero no podemos procesar un array de clases como tal, recuerda que todo en Flutter es un widget y un menú lateral no es la excepción, así que esta lista que tenemos definida como base o hardcode en nuestra función de build la podrías traer por ejemplo de una Rest Api.

Y pasamos esta lista a una Lista de widgets de tipo ListTile que emplearemos a posterior.

Mapear la lista de objetos de opciones a una lista de widgets

Lo siguiente que tenemos en la lista es mapear nuestra lista, es decir, tenemos una lista de objetos, pero drawerItems pero nosotros necesitamos es una lista de widgets; existen muchas formas en las cuales podemos hacer esto, pero la más interesante es emplear el método map que nos permite mapear de un tipo de dato a otro; tal cual puedes ver en la implementación, variamos de una lista de objetos a una lista de widgets; el punto interesante aquí es que la función de map implementa una función anónima en la cual podemos hacer cualquier clase de operación para convertir nuestro ítem inicial a otro tipo de objetos que en este caso sería un widget que a la final vendrá siendo nuestra lista de widgets; ya que la función map la podemos asignar a otro objeto y luego llamamos al método toList() para pasar de un mapa a una lista.

Crear el menú lateral: Drawer en Flutter

Cabecera o header del menú lateral

Ahora, ya con la data básica definida y lista para usar, creamos nuestro menú lateral, para eso hacemos uso del Scaffold como elemento raíz de nuestra app y su propiedad llamada drawer en el cual le podemos pasar un widget que será nuestro menú lateral Drawer; este menú lateral recibe un child como viene siendo usual en este tipo de componentes y luego le definimos un Column, porque vamos a definir una lista de elementos o widgets; lo primero que colocamos será un UserAccountsDrawerHeader que es un widget para definir una cabecera, así de fácil la famosa cabecera para definir elementos como la imagen del perfil, nombre de usuario o cuenta, email entre otros aspectos que puedes revisar.

Evento tap y navegación

Luego de la cabecera del Drawer, le pasamos nuestro listado de opciones, así de simple, lo que hicimos en el bloque anterior todo lo tenemos en una función que se lo pasamos de una a nuestro menú lateral y listo; como puedes notar, cada ítem del menú puede implementar un evento tap el cual lo tenemos definido pasando el index del elemento tocado; y de aquí nos viene perfecto el widget ListTile que permite definir, textos, íconos y el evento tap.

Otro widget opcional en esta implementación sería el de ConstrainedBox que nos permite definir una caja con unas dimensiones máximas o mínimas, es una especie de contenedor responsive con extremos; puedes buscar más información de la misma en la documentación oficial.

Otro punto importante que todavía no hemos visto y vamos a tratar en otra entrada, es la navegación entre pantallas que es un tema primordial y que sin ella las opciones de nuestro menú lateral servirán para bastante poco.

Pero con esto tenemos nuestro menú lateral implementado y listo para hacer lo que desees hacer.

Crear un Scaffold Personalizado y variar el Drawer o Estático y Responsividad (OrientationBuilder)

Quiero hablarte de dos conceptos principales que implementé en mi aplicación de Academia:

  • La ventaja de crear un Scaffold Personalizado (CustomScaffold).
  • Cómo crear un menú lateral (Drawer) adaptable que cambia su comportamiento según el tamaño de la pantalla.

Observa que si reducimos la pantalla, el menú pasa al enfoque clásico móvil (un Drawer desplegable). Si la pantalla crece (enfoque para web, tableta, Mac o Windows), el menú se vuelve estático en el lateral. Esto nos permite aprovechar el espacio disponible para colocar más elementos, y te mostraré cómo lo hice.

️ Cómo Crear un CustomScaffold

En Flutter, el widget superior es el MaterialApp. Para crear una página, utilizamos el Scaffold. Por lo tanto, cada opción y sección de tu aplicación tendrá su propio Scaffold.

El Problema de Copiar y Pegar

Normalmente, si queremos personalizar el Scaffold (como agregar un Drawer), tendríamos que copiar y pegar esa configuración en cada una de las páginas de la aplicación.

El problema es obvio: si decides cambiar algo en el Scaffold (por ejemplo, el diseño del AppBar o el comportamiento del Drawer), tendrías que modificar todas las páginas individualmente.

La Solución: CustomScaffold

Por esta razón, yo decidí crear un widget llamado CustomScaffold. Este es el widget que finalmente utilizamos en cada página.

El CustomScaffold es básicamente un StatefulWidget que recibe los elementos esenciales del Scaffold, como el title y el body, y los widgets opcionales como endDrawer o floatingActionButton.

class CustomScaffold extends StatefulWidget {
 final String title;
 final Widget body;
 // ... otros parámetros como endDrawer, floatingActionButton
 // ...
}

Ventajas del CustomScaffold

El principal beneficio radica en la centralización. Si, por ejemplo, quise implementar la lógica para el botón flotante (como el que uso para el filtro en la página de posts), solo tuve que modificar este archivo central.

Otro ejemplo de su utilidad: en el método initState de este widget personalizado, puedo agregar un proceso adicional, como la verificación de la conexión a internet (checkisInternetConnection). Este cambio se aplica automáticamente a todas las páginas que utilicen el CustomScaffold. Esto es la belleza de la modularización.

Diseño Adaptable con OrientationBuilder

Ahora, veamos cómo logramos el comportamiento adaptable del menú lateral: fijo en pantallas grandes y desplegable (Drawer) en pantallas pequeñas.

En Flutter, todo es un widget, por lo que podemos manipular la disposición de los elementos fácilmente. Dentro de nuestro CustomScaffold, empleamos el widget OrientationBuilder.

Uso de OrientationBuilder

El OrientationBuilder es un listener que se ejecuta cada vez que el dispositivo o la ventana cambia de tamaño u orientación. Dentro de su constructor (builder), podemos aplicar lógica condicional:

return OrientationBuilder(
   builder: (BuildContext context, Orientation orientation) {
     if (getBreakpoint(MediaQuery.of(context).size.width) ==
             WindowsBreakpoint.sm ||
         getBreakpoint(MediaQuery.of(context).size.width) ==
             WindowsBreakpoint.md) {
       // ... Lógica para pantallas pequeñas (Móviles / Tablets)
       // ...
     } else {
       // window lg - Lógica para pantallas grandes (Web / Escritorio)
       // ...
     }
   });

Ejemplo completo:

OrientationBuilder(
       builder: (BuildContext context, Orientation orientation) {
     if (getBreakpoint(MediaQuery.of(context).size.width) ==
             WindowsBreakpoint.sm ||
         getBreakpoint(MediaQuery.of(context).size.width) ==
             WindowsBreakpoint.md) {
       return Scaffold(
           appBar: AppBar(
             title: Text(widget.title),
           ),
           drawer: DrawerComponent(true),
           ***
           } else {
        //window lg
        return Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          ***
          body: Row(
            children: <Widget>[
              SizedBox(
                width: 300,
                height: double.infinity,
                child: DrawerComponent(),
              ),
              Container(
                width: 1,
                height: double.infinity,
                color: Colors.grey,
              ),
              Expanded(
                  child: Padding(
                padding: const EdgeInsets.all(8.0),
                child: SafeArea(child: widget.body),
              ))
            ],

Crear Scaffold personalizado

Creamos un widget con el scaffold personalizado en el cual, usaremos más adelante; puedes personalizar cualquier aspecto con propiedades e implementación base:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'package:easy_localization/easy_localization.dart';

import 'package:desarrollolibre/generated/locale_keys.g.dart';
import 'package:desarrollolibre/components/drawer_component.dart';
import 'package:desarrollolibre/provider/app_model.dart';
import 'package:desarrollolibre/utils/windows_sizes.dart';
import 'package:desarrollolibre/utils/academy_helper.dart';
import 'package:desarrollolibre/utils/helpers.dart';

import 'package:connectivity_plus/connectivity_plus.dart';

class CustomScaffold extends StatefulWidget {
  final String title;
  final Widget body;
  final Widget? endDrawer;
  final FloatingActionButton? floatingActionButton;

  const CustomScaffold(this.title, this.body,
      [this.endDrawer, this.floatingActionButton]);

  @override
  State<CustomScaffold> createState() => _CustomScaffoldState();
}

class _CustomScaffoldState extends State<CustomScaffold> {
  late AppModel appModel;
  bool thereIsInternet = true;

  @override
  void initState() {
    appModel = Provider.of<AppModel>(context, listen: false);
    if (!appModel.userCredentialsCheck) {
      checkuserVerified(context);
    }

    init();

    super.initState();
  }

  Future<bool> checkisInternetConnection() async {
    var connectivityResult = await (Connectivity().checkConnectivity());
    return ConnectivityResult.none != connectivityResult;
  }

  init() async {
    thereIsInternet = await checkisInternetConnection();
    if (!thereIsInternet) {
      // no hay internet
      setState(() {});
    }
  }

  @override
  Widget build(BuildContext context) {
    if (!thereIsInternet) {
      // no hay conexion
      return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Text('No hay Internet'),
      );
    }

    return OrientationBuilder(
        builder: (BuildContext context, Orientation orientation) {
      if (getBreakpoint(MediaQuery.of(context).size.width) ==
              WindowsBreakpoint.sm ||
          getBreakpoint(MediaQuery.of(context).size.width) ==
              WindowsBreakpoint.md) {
        return Scaffold(
            appBar: AppBar(
              title: Text(widget.title),
            ),
            drawer: DrawerComponent(true),
            endDrawer: widget.endDrawer,
            floatingActionButton:
                appModel.userIsLogging && !appModel.userVerified
                    ? FloatingActionButton.extended(
                        onPressed: () {
                          AcademyHelper.verifyUserPost(appModel.userToken);
                          showToastMessage(context,
                              "${LocaleKeys.sentEmailTo.tr()} ${appModel.userEmail} ${LocaleKeys.verifyAccount.tr()}");
                        },
                        label: Text(LocaleKeys.verifyUser.tr()),
                      )
                    : widget.floatingActionButton,
            body: SafeArea(child: widget.body));
      } else {
        //window lg
        return Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          floatingActionButton: appModel.userIsLogging && !appModel.userVerified
              ? FloatingActionButton.extended(
                  onPressed: () {
                    AcademyHelper.verifyUserPost(appModel.userToken);
                    showToastMessage(context,
                        "${LocaleKeys.sentEmailTo.tr()} ${appModel.userEmail} ${LocaleKeys.verifyAccount.tr()}");
                  },
                  label: Text(LocaleKeys.verifyUser.tr()),
                )
              : widget.floatingActionButton,
          body: Row(
            children: <Widget>[
              SizedBox(
                width: 300,
                height: double.infinity,
                child: DrawerComponent(),
              ),
              Container(
                width: 1,
                height: double.infinity,
                color: Colors.grey,
              ),
              Expanded(
                  child: Padding(
                padding: const EdgeInsets.all(8.0),
                child: SafeArea(child: widget.body),
              ))
            ],
          ),
          endDrawer: widget.endDrawer,
        );
      }
    });
  }
}

1. Pantallas Pequeñas (Móvil / Tablet)

Si el breakpoint detecta que la pantalla es pequeña (sm o md):

  • Retornamos un Scaffold clásico.
  • Utilizamos la propiedad nativa drawer: del Scaffold y le pasamos nuestro widget DrawerComponent. Esto crea el menú desplegable que se abre desde el AppBar.

2. Pantallas Grandes (Escritorio / Web)

Si el breakpoint indica que la pantalla es grande (lg):

  • Retornamos un Scaffold sin la propiedad drawer.
  • El body del Scaffold se construye utilizando un widget Row.
  • Dentro del Row, colocamos nuestro menú lateral (DrawerComponent) con un ancho fijo (p. ej., SizedBox(width: 300, ...)), creando el menú estático.
  • El resto del contenido de la página (widget.body) se envuelve en un widget Expanded para que ocupe el espacio restante.

Esta es la clave del truco: al utilizar la lógica condicional dentro de un widget que reacciona a los cambios de tamaño, logramos un diseño completamente adaptable; el menú queda como:

return Drawer(
      child: SafeArea(
        child: Column(
          children: <Widget>[
            const SizedBox(
              height: 15,
            ),
            MediaQuery.of(context).size.height > 700
                ? SizedBox(
                    width: double.infinity,
                    height: 120,
                    child: CircleAvatar(
                      backgroundColor: Colors.purple,
                      child: Text(
                        appModel.userIsLogging
                            ? appModel.userEmail.substring(0, 1).toUpperCase() +
                                appModel.userEmail.substring(1, 2)
                            : 'DL',
                        style:
                            const TextStyle(fontSize: 40, color: Colors.white),
                      ),
                    ),
                  )
                : const SizedBox(),
            const SizedBox(
              height: 15,
            ),
            Text('${LocaleKeys.hello.tr()} ${appModel.userEmail}'),
            Expanded(child: _Items(activatePopInBack)),
            ListTile(
              leading: const Icon(Icons.circle, color: Colors.purple),
              title: const Text('Dark Mode'),
              trailing: Switch.adaptive(
                  value: userPreference.themeDarkMode,
                  activeColor: Colors.purple,
                  onChanged: (value) {
                    userPreference.themeDarkMode = value;
                    appTheme.darkTheme = value;
                    AcademyHelper.userExtraPost(appModel.userToken, value);
                  }),
            ),
          ],
        ),
      ),
    );
  }
}

Next step, create a table using the DataTable widget in Flutter.

Acepto recibir anuncios de interes sobre este Blog.

Vamos a conocer como podemos crear un menú lateral o Drawer con opciones, cabeceras e información del usuario en Flutter e implementar un Scaffold personalizado con el Drawer.

| 👤 Andrés Cruz

🇺🇸 In english