Widgets de UI y Navegación en Flutter

  Widgets de UI y Navegación

Los widgets de UI y navegación proporcionan la estructura y funcionalidad básica de las aplicaciones móviles. Incluyen elementos como barras de navegación, listas, menús y sistemas de retroalimentación.

Scaffold: La Base de tu App

¿Qué es Scaffold?

Scaffold proporciona la estructura básica para la mayoría de las aplicaciones móviles, incluyendo elementos como barras de navegación, cajones de navegación (drawers) y barras de estado.

dart
Scaffold(
appBar: AppBar(
  title: Text('Mi Aplicación'),
  backgroundColor: Colors.blue,
  actions: [
    IconButton(
      icon: Icon(Icons.search),
      onPressed: () {},
    ),
    IconButton(
      icon: Icon(Icons.more_vert),
      onPressed: () {},
    ),
  ],
),
body: Center(
  child: Text('Contenido Principal'),
),
floatingActionButton: FloatingActionButton(
  onPressed: () {},
  child: Icon(Icons.add),
),
drawer: Drawer(
  child: ListView(
    children: [
      DrawerHeader(
        decoration: BoxDecoration(color: Colors.blue),
        child: Text(
          'Menú Principal',
          style: TextStyle(color: Colors.white, fontSize: 24),
        ),
      ),
      ListTile(
        leading: Icon(Icons.home),
        title: Text('Inicio'),
        onTap: () {},
      ),
    ],
  ),
),
)

Propiedades Principales del Scaffold

A continuación, se presentan las propiedades principales del widget Scaffold:

dart
Scaffold(
// Barra superior de la app
appBar: AppBar(
  title: Text('Mi App'),
  backgroundColor: Colors.blue,
  elevation: 4.0, // Sombra
),

// Contenido principal
body: Center(
  child: Text('Hola Mundo!'),
),

// Botón flotante
floatingActionButton: FloatingActionButton(
  onPressed: () {},
  child: Icon(Icons.add),
  backgroundColor: Colors.orange,
),

// Posición del botón flotante
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,

// Menú lateral
drawer: Drawer(
  child: ListView(
    children: [
      DrawerHeader(
        child: Text('Encabezado del Drawer'),
        decoration: BoxDecoration(color: Colors.blue),
      ),
      ListTile(
        title: Text('Opción 1'),
        leading: Icon(Icons.star),
        onTap: () {},
      ),
    ],
  ),
),

// Barra de navegación inferior
bottomNavigationBar: BottomNavigationBar(
  items: [
    BottomNavigationBarItem(
      icon: Icon(Icons.home),
      label: 'Inicio',
    ),
    BottomNavigationBarItem(
      icon: Icon(Icons.person),
      label: 'Perfil',
    ),
  ],
  currentIndex: 0,
  onTap: (index) {
    print('Seleccionado: $index');
  },
),

// Color de fondo
backgroundColor: Colors.grey[100],

// Botones persistentes en la parte inferior
persistentFooterButtons: [
  ElevatedButton(
    onPressed: () {},
    child: Text('Botón 1'),
  ),
  ElevatedButton(
    onPressed: () {},
    child: Text('Botón 2'),
  ),
],
)

ListView: Listas Desplazables

Este widget es esencial para mostrar grandes cantidades de información de manera organizada y eficiente.

ListView Básico

Un ListView básico muestra una lista de elementos verticalmente.

dart
ListView(
children: [
  ListTile(
    leading: Icon(Icons.map),
    title: Text('Mapa'),
    subtitle: Text('Ver ubicación'),
    trailing: Icon(Icons.arrow_forward),
    onTap: () {
      print('Mapa seleccionado');
    },
  ),
  ListTile(
    leading: Icon(Icons.photo),
    title: Text('Álbum'),
    subtitle: Text('Ver fotos'),
    trailing: Icon(Icons.arrow_forward),
    onTap: () {
      print('Álbum seleccionado');
    },
  ),
  ListTile(
    leading: Icon(Icons.phone),
    title: Text('Teléfono'),
    subtitle: Text('Hacer llamada'),
    trailing: Icon(Icons.arrow_forward),
    onTap: () {
      print('Teléfono seleccionado');
    },
  ),
],
)

ListView.builder (Recomendado para listas largas)

Para listas muy grandes, ListView.builder es más eficiente que ListView, ya que solo construye los elementos visibles en la pantalla.

dart
class ListaEficiente extends StatelessWidget {
final List<String> items = List.generate(1000, (index) => 'Item $index');


Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: items.length,
    itemBuilder: (context, index) {
      return ListTile(
        leading: CircleAvatar(
          child: Text('${index + 1}'),
        ),
        title: Text(items[index]),
        subtitle: Text('Descripción del ${items[index]}'),
        onTap: () {
          print('Seleccionado: ${items[index]}');
        },
      );
    },
  );
}
}

ListView.separated

Útil cuando necesitas separadores entre elementos.

dart
ListView.separated(
itemCount: 20,
separatorBuilder: (context, index) => Divider(
  color: Colors.grey,
  thickness: 1,
),
itemBuilder: (context, index) {
  return ListTile(
    title: Text('Elemento $index'),
    leading: Icon(Icons.star),
    onTap: () {},
  );
},
)

ListTile: Elementos de Lista


ListTile Completo

Un ListTile es una fila de lista que normalmente contiene un ícono, un título y un subtítulo.

dart
ListTile(
// Icono o widget al inicio
leading: CircleAvatar(
  backgroundColor: Colors.blue,
  child: Icon(Icons.person, color: Colors.white),
),

// Título principal
title: Text(
  'Juan Pérez',
  style: TextStyle(
    fontWeight: FontWeight.bold,
    fontSize: 16,
  ),
),

// Subtítulo
subtitle: Text(
  'Desarrollador Flutter',
  style: TextStyle(color: Colors.grey[600]),
),

// Widget al final
trailing: Row(
  mainAxisSize: MainAxisSize.min,
  children: [
    IconButton(
      icon: Icon(Icons.call),
      onPressed: () {},
    ),
    IconButton(
      icon: Icon(Icons.message),
      onPressed: () {},
    ),
  ],
),

// Acción al tocar
onTap: () {
  print('Perfil de Juan Pérez');
},

// Acción al mantener presionado
onLongPress: () {
  print('Opciones para Juan Pérez');
},

// Hace que el ListTile sea más denso
dense: true,

// Añade padding interno
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
)

SnackBar: Notificaciones Rápidas


SnackBar Básico

Un SnackBar es una notificación breve que se muestra en la parte inferior de la pantalla.

dart
// Mostrar SnackBar básico
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
  content: Text('Operación exitosa!'),
  duration: Duration(seconds: 2),
),
);

SnackBar con Acción

Un SnackBar puede incluir una acción que se activa cuando el usuario toca el botón.

dart
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
  content: Text('Item eliminado'),
  action: SnackBarAction(
    label: 'DESHACER',
    textColor: Colors.yellow,
    onPressed: () {
      // Código para deshacer la acción
      print('Acción deshecha');
    },
  ),
  duration: Duration(seconds: 4),
),
);

SnackBar Personalizado

dart
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
  content: Row(
    children: [
      Icon(Icons.check_circle, color: Colors.white),
      SizedBox(width: 12),
      Expanded(
        child: Text(
          'Guardado correctamente',
          style: TextStyle(fontSize: 16),
        ),
      ),
    ],
  ),
  backgroundColor: Colors.green,
  behavior: SnackBarBehavior.floating,
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(10),
  ),
  margin: EdgeInsets.all(16),
  duration: Duration(seconds: 3),
),
);

SafeArea: Zona Segura


¿Por qué usar SafeArea?

SafeArea evita que el contenido se superponga con áreas no visibles de la pantalla, como el notch o las barras de estado.

dart
Scaffold(
body: SafeArea(
  child: Column(
    children: [
      Container(
        color: Colors.blue,
        height: 100,
        child: Center(
          child: Text(
            'Contenido Seguro',
            style: TextStyle(color: Colors.white, fontSize: 18),
          ),
        ),
      ),
      Expanded(
        child: Container(
          color: Colors.red,
          child: Center(
            child: Text(
              'Más Contenido',
              style: TextStyle(color: Colors.white, fontSize: 16),
            ),
          ),
        ),
      ),
    ],
  ),
),
)

SafeArea Selectivo

SafeArea puede proteger solo ciertas áreas de la pantalla, como la parte superior, inferior, izquierda o derecha.

dart
SafeArea(
top: true,    // Protege la parte superior
bottom: false, // No protege la parte inferior
left: true,   // Protege el lado izquierdo
right: true,  // Protege el lado derecho
child: YourWidget(),
)

GestureDetector: Detección de Gestos


Gestos Básicos

Un GestureDetector es un widget que detecta gestos del usuario, como toques, arrastres y desplazamientos.

dart
GestureDetector(
onTap: () {
  print('Tap simple');
},
onDoubleTap: () {
  print('Doble tap');
},
onLongPress: () {
  print('Presión larga');
},
onPanUpdate: (details) {
  print('Arrastrando: ${details.localPosition}');
},
child: Container(
  width: 200,
  height: 200,
  decoration: BoxDecoration(
    color: Colors.blue,
    borderRadius: BorderRadius.circular(12),
  ),
  child: Center(
    child: Text(
      'Toca aquí',
      style: TextStyle(
        color: Colors.white,
        fontSize: 18,
        fontWeight: FontWeight.bold,
      ),
    ),
  ),
),
)

GestureDetector Avanzado

Un GestureDetector puede detectar múltiples gestos y actualizar el estado de la interfaz de usuario.

dart
class GestureExample extends StatefulWidget {

_GestureExampleState createState() => _GestureExampleState();
}

class _GestureExampleState extends State<GestureExample> {
String gestureText = 'Realiza un gesto';
Color containerColor = Colors.blue;


Widget build(BuildContext context) {
  return GestureDetector(
    onTap: () {
      setState(() {
        gestureText = 'Tap detectado';
        containerColor = Colors.green;
      });
    },
    onDoubleTap: () {
      setState(() {
        gestureText = 'Doble tap detectado';
        containerColor = Colors.orange;
      });
    },
    onLongPress: () {
      setState(() {
        gestureText = 'Presión larga detectada';
        containerColor = Colors.red;
      });
    },
    child: AnimatedContainer(
      duration: Duration(milliseconds: 300),
      width: 250,
      height: 150,
      decoration: BoxDecoration(
        color: containerColor,
        borderRadius: BorderRadius.circular(15),
      ),
      child: Center(
        child: Text(
          gestureText,
          style: TextStyle(
            color: Colors.white,
            fontSize: 16,
            fontWeight: FontWeight.bold,
          ),
          textAlign: TextAlign.center,
        ),
      ),
    ),
  );
}
}

Ejemplo Práctico: App Completa


dart
class AppCompleta extends StatefulWidget {

_AppCompletaState createState() => _AppCompletaState();
}

class _AppCompletaState extends State<AppCompleta> {
int _selectedIndex = 0;
final List<String> _items = List.generate(50, (index) => 'Item ${index + 1}');

void _onItemTapped(int index) {
  setState(() {
    _selectedIndex = index;
  });
}

void _showSnackBar(String message) {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text(message),
      action: SnackBarAction(
        label: 'OK',
        onPressed: () {},
      ),
    ),
  );
}


Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('App Completa'),
      backgroundColor: Colors.blue,
      actions: [
        IconButton(
          icon: Icon(Icons.search),
          onPressed: () => _showSnackBar('Búsqueda activada'),
        ),
      ],
    ),
    
    drawer: Drawer(
      child: ListView(
        padding: EdgeInsets.zero,
        children: [
          DrawerHeader(
            decoration: BoxDecoration(
              gradient: LinearGradient(
                colors: [Colors.blue, Colors.blueAccent],
              ),
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                CircleAvatar(
                  radius: 30,
                  backgroundColor: Colors.white,
                  child: Icon(Icons.person, size: 40, color: Colors.blue),
                ),
                SizedBox(height: 10),
                Text(
                  'Usuario Demo',
                  style: TextStyle(color: Colors.white, fontSize: 18),
                ),
              ],
            ),
          ),
          ListTile(
            leading: Icon(Icons.home),
            title: Text('Inicio'),
            onTap: () {
              Navigator.pop(context);
              _showSnackBar('Inicio seleccionado');
            },
          ),
          ListTile(
            leading: Icon(Icons.settings),
            title: Text('Configuración'),
            onTap: () {
              Navigator.pop(context);
              _showSnackBar('Configuración seleccionada');
            },
          ),
        ],
      ),
    ),
    
    body: SafeArea(
      child: _selectedIndex == 0
          ? ListView.separated(
              itemCount: _items.length,
              separatorBuilder: (context, index) => Divider(),
              itemBuilder: (context, index) {
                return GestureDetector(
                  onLongPress: () => _showSnackBar('Presión larga en ${_items[index]}'),
                  child: ListTile(
                    leading: CircleAvatar(
                      child: Text('${index + 1}'),
                    ),
                    title: Text(_items[index]),
                    subtitle: Text('Descripción del item ${index + 1}'),
                    trailing: Icon(Icons.arrow_forward_ios),
                    onTap: () => _showSnackBar('${_items[index]} seleccionado'),
                  ),
                );
              },
            )
          : Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(
                    Icons.person,
                    size: 100,
                    color: Colors.grey,
                  ),
                  SizedBox(height: 20),
                  Text(
                    'Perfil de Usuario',
                    style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                  ),
                ],
              ),
            ),
    ),
    
    floatingActionButton: FloatingActionButton(
      onPressed: () => _showSnackBar('Botón flotante presionado'),
      child: Icon(Icons.add),
      backgroundColor: Colors.orange,
    ),
    
    bottomNavigationBar: BottomNavigationBar(
      items: [
        BottomNavigationBarItem(
          icon: Icon(Icons.list),
          label: 'Lista',
        ),
        BottomNavigationBarItem(
          icon: Icon(Icons.person),
          label: 'Perfil',
        ),
      ],
      currentIndex: _selectedIndex,
      selectedItemColor: Colors.blue,
      onTap: _onItemTapped,
    ),
  );
}
}
  Nota

Tip de UX: Usa SafeArea siempre que tu contenido pueda verse afectado por el notch o barras del sistema. Combina GestureDetector con SnackBar para crear interacciones intuitivas.

  Nota

Rendimiento: Para listas largas, siempre usa ListView.builder en lugar de ListView con children. Es mucho más eficiente en memoria.

Buenas Prácticas


Gestión de Estado en Listas

dart
class ListaConEstado extends StatefulWidget {

_ListaConEstadoState createState() => _ListaConEstadoState();
}

class _ListaConEstadoState extends State<ListaConEstado> {
List<bool> _selectedItems = List.generate(20, (index) => false);


Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: _selectedItems.length,
    itemBuilder: (context, index) {
      return ListTile(
        leading: Checkbox(
          value: _selectedItems[index],
          onChanged: (bool? value) {
            setState(() {
              _selectedItems[index] = value ?? false;
            });
          },
        ),
        title: Text('Item ${index + 1}'),
        selected: _selectedItems[index],
        onTap: () {
          setState(() {
            _selectedItems[index] = !_selectedItems[index];
          });
        },
      );
    },
  );
}
}

Navegación Responsiva

La navegación responsiva se refiere a la adaptación de la interfaz de usuario a diferentes tamaños de pantalla, como teléfonos móviles, tablets y computadoras.

dart
class ResponsiveScaffold extends StatelessWidget {

Widget build(BuildContext context) {
  return LayoutBuilder(
    builder: (context, constraints) {
      if (constraints.maxWidth > 600) {
        // Tablet/Desktop layout
        return Scaffold(
          body: Row(
            children: [
              NavigationRail(
                destinations: [
                  NavigationRailDestination(
                    icon: Icon(Icons.home),
                    label: Text('Inicio'),
                  ),
                  NavigationRailDestination(
                    icon: Icon(Icons.person),
                    label: Text('Perfil'),
                  ),
                ],
                selectedIndex: 0,
                onDestinationSelected: (index) {},
              ),
              Expanded(child: MainContent()),
            ],
          ),
        );
      } else {
        // Mobile layout
        return Scaffold(
          appBar: AppBar(title: Text('Mobile Layout')),
          body: MainContent(),
          bottomNavigationBar: BottomNavigationBar(
            items: [
              BottomNavigationBarItem(
                icon: Icon(Icons.home),
                label: 'Inicio',
              ),
              BottomNavigationBarItem(
                icon: Icon(Icons.person),
                label: 'Perfil',
              ),
            ],
          ),
        );
      }
    },
  );
}
}

class MainContent extends StatelessWidget {

Widget build(BuildContext context) {
  return Center(child: Text('Contenido Principal'));
}
}