Widgets en Flutter
Aprende a crear interfaces con widgets reutilizables en Flutter
¿Qué son los Widgets en Flutter?
Definición
En Flutter, todo es un widget. Los widgets son los componentes básicos de construcción de la interfaz de usuario, desde elementos simples como texto e iconos hasta estructuras complejas como listas y navegación.
Ventajas de usar Widgets
- Reutilización: Crea widgets una vez y úsalos en toda tu aplicación
- Composición: Construye interfaces complejas combinando widgets simples
- Mantenimiento: Fácil de modificar y actualizar
- Reactividad: Se actualizan automáticamente cuando cambian los datos
Jerarquía de Widgets
Flutter organiza los widgets en un árbol donde cada widget puede contener otros widgets. Esta estructura jerárquica permite crear interfaces complejas de manera organizada.
📝 Ejemplo Práctico - Widgets Personalizados
Este ejemplo muestra cómo crear y usar widgets personalizados en Flutter.
Sistema de Ejemplos Flutter
🔍 Análisis del Código: main.dart
Estructura Principal de la Aplicación
import 'package:flutter/material.dart';
import 'package:flutter_interfaz/widgets/tarjetapokemon.dart';
import '../widgets/tarjeta.dart';
void main() {
runApp(MainApp());
}
class MainApp extends StatelessWidget {
MainApp({super.key});
final List<TarjetaWidget> tarjetas = [
// Lista de widgets personalizados
];
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Column(
children: [
// Barra superior con iconos de usuarios
Wrap(
alignment: WrapAlignment.center,
spacing: 40,
children: [
// Widgets de perfil...
],
),
// Encabezado de noticias
Padding(
padding: const EdgeInsets.all(20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Discover News:", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
IconButton(onPressed: null, icon: Icon(Icons.add)),
],
),
),
// Lista de tarjetas
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
...tarjetas, // Operador spread para incluir todas las tarjetas
PokemonScreen(nombre: "Blastoise"), // Widget de Pokémon
],
),
),
),
],
),
// Barra de navegación inferior
bottomNavigationBar: BottomNavigationBar(
// Configuración de la barra...
),
),
);
}
}
Explicación de Conceptos Clave
- MaterialApp: Widget raíz que proporciona el tema Material Design
- StatelessWidget: Widget que no mantiene estado interno
- Scaffold: Proporciona la estructura básica de la pantalla
- Column: Organiza widgets verticalmente
- Wrap: Organiza widgets en fila con ajuste automático
- SingleChildScrollView: Permite desplazamiento del contenido
🎯 Widget Personalizado: TarjetaWidget
Implementación del Widget de Tarjeta
class TarjetaWidget extends StatelessWidget {
final String avatarUrl;
final String title;
final String subtitle;
final String body;
final String imageUrl;
const TarjetaWidget({
super.key,
required this.avatarUrl,
required this.title,
this.subtitle = 'Subtítulo',
this.body = 'Texto descriptivo de la tarjeta.',
required this.imageUrl,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 700,
child: Card(
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Encabezado con avatar y título
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 28,
backgroundImage: NetworkImage(avatarUrl),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
Text(subtitle),
],
),
),
IconButton(
onPressed: () {},
icon: const Icon(Icons.more_horiz),
),
],
),
// Cuerpo de la tarjeta
const SizedBox(height: 12),
Text(body),
const SizedBox(height: 12),
// Imagen principal
ClipRRect(
child: Image.network(
imageUrl,
height: 180,
fit: BoxFit.cover,
),
),
// Pie de tarjeta con estadísticas
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Botones de interacción...
],
),
],
),
),
),
);
}
}
Características del Widget Personalizado
- Parámetros requeridos: avatarUrl, title, imageUrl
- Parámetros opcionales: subtitle, body con valores por defecto
- Composición: Combina múltiples widgets básicos
- Responsive: Usa Expanded para adaptarse al espacio disponible
- Reutilizable: Se puede instanciar múltiples veces con diferentes datos
🌐 Widget con Consumo de API: TarjetaPokemon
Modelo de Datos y Consumo de API
class TarjetaPokemon {
final String nombre;
final String imagenUrl;
final String tipo;
final int hp;
final int ataque;
final int defensa;
TarjetaPokemon({
required this.nombre,
required this.imagenUrl,
required this.tipo,
required this.hp,
required this.ataque,
required this.defensa,
});
// Constructor desde JSON
factory TarjetaPokemon.fromJson(Map<String, dynamic> json) {
return TarjetaPokemon(
nombre: json['name'],
imagenUrl: json['sprites']['front_default'],
tipo: json['types'][0]['type']['name'],
hp: json['stats'][0]['base_stat'],
ataque: json['stats'][1]['base_stat'],
defensa: json['stats'][2]['base_stat'],
);
}
}
// Función para obtener datos de la API
Future<TarjetaPokemon> fetchPokemon(String nombre) async {
final response = await http.get(Uri.parse('https://pokeapi.co/api/v2/pokemon/$nombre'));
if (response.statusCode == 200) {
return TarjetaPokemon.fromJson(json.decode(response.body));
} else {
throw Exception('Error al cargar Pokémon');
}
}
Widget FutureBuilder para Manejo Asíncrono
class PokemonScreen extends StatelessWidget {
final String nombre;
const PokemonScreen({super.key, required this.nombre});
@override
Widget build(BuildContext context) {
return FutureBuilder<TarjetaPokemon>(
future: fetchPokemon(nombre),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (snapshot.hasData) {
return PokemonCard(pokemon: snapshot.data!);
} else {
return const Center(child: Text('No se encontró el Pokémon'));
}
},
);
}
}
Conceptos Avanzados Implementados
- Future: Representa un valor que estará disponible en el futuro
- FutureBuilder: Widget que construye su contenido basado en un Future
- Async/Await: Sintaxis para trabajar con operaciones asíncronas
- Modelado de datos: Clase TarjetaPokemon para representar la estructura de datos
- Manejo de estados: Diferentes estados de carga, error y éxito
🏗️ Patrones y Mejores Prácticas
Separación de Responsabilidades
El código está bien organizado en archivos separados:
- main.dart: Punto de entrada y estructura principal
- tarjeta.dart: Widget reutilizable para tarjetas
- tarjetapokemon.dart: Lógica de API y widget especializado
Widgets Stateless vs Stateful
En este ejemplo todos los widgets son Stateless porque:
- No necesitan mantener estado interno
- Reciben todos los datos a través de parámetros
- Son más eficientes y fáciles de probar
💡 Ejercicios para Practicar
Ejercicio 1: Crear un Nuevo Widget
Crea un widget "TarjetaNoticia" similar a TarjetaWidget pero con:
- Fecha de publicación
- Autor
- Categoría
- Botón de "Me gusta" funcional
Ejercicio 2: Mejorar la API
Modifica el widget Pokémon para:
- Mostrar más estadísticas
- Implementar búsqueda por nombre
- Añadir animaciones al cargar
Ejercicio 3: Estado Compartido
Convierte TarjetaWidget a StatefulWidget para:
- Contador de likes local
- Funcionalidad de expandir/contraer
- Cambio de tema claro/oscuro