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 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.
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: () {},
),
],
),
),
) A continuación, se presentan las propiedades principales del widget Scaffold:
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'),
),
],
) Este widget es esencial para mostrar grandes cantidades de información de manera organizada y eficiente.
Un ListView básico muestra una lista de elementos verticalmente.
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');
},
),
],
) Para listas muy grandes, ListView.builder es más eficiente que ListView, ya que solo construye los elementos visibles en la pantalla.
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]}');
},
);
},
);
}
} Útil cuando necesitas separadores entre elementos.
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: () {},
);
},
) Un ListTile es una fila de lista que normalmente contiene un ícono, un título y un subtítulo.
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),
) Un SnackBar es una notificación breve que se muestra en la parte inferior de la pantalla.
// Mostrar SnackBar básico
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Operación exitosa!'),
duration: Duration(seconds: 2),
),
); Un SnackBar puede incluir una acción que se activa cuando el usuario toca el botón.
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),
),
); 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 evita que el contenido se superponga con áreas no visibles de la pantalla, como el notch o las barras de estado.
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 puede proteger solo ciertas áreas de la pantalla, como la parte superior, inferior, izquierda o derecha.
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(),
) Un GestureDetector es un widget que detecta gestos del usuario, como toques, arrastres y desplazamientos.
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,
),
),
),
),
) Un GestureDetector puede detectar múltiples gestos y actualizar el estado de la interfaz de usuario.
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,
),
),
),
);
}
} 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,
),
);
}
} 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.
Rendimiento: Para listas largas, siempre usa ListView.builder en lugar de ListView con children. Es mucho más eficiente en memoria.
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];
});
},
);
},
);
}
} 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.
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'));
}
}