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