Consumo de APIs en Flutter

Aprende a conectar tu app Flutter con APIs REST usando The Simpsons API

Descargar Proyecto Completo

¿Qué es una API REST?

Una API REST (Representational State Transfer) es una interfaz que permite a diferentes aplicaciones comunicarse entre sí mediante protocolos web como HTTP.

App Flutter
Solicitud HTTP
API The Simpsons
Datos JSON
App Flutter

The Simpsons API

Utilizaremos https://thesimpsonsapi.com/ - una API pública que proporciona información sobre personajes de Los Simpsons.

Endpoints Principales:

GET
/api/characters
Todos los personajes
GET
/api/characters?count=10
Límite de personajes
GET
/api/characters/{id}
Personaje específico

Ejemplo de Respuesta JSON:

[
  {
    "id": 1,
    "name": "Homer Simpson",
    "description": "Padre de familia",
    "image": "homer.jpg"
  },
  {
    "id": 2,
    "name": "Marge Simpson", 
    "description": "Madre de familia",
    "image": "marge.jpg"
  }
]

Configuración Inicial

1. Añadir Dependencias

Agrega http y provider a tu pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0
  provider: ^6.1.1

2. Permisos de Internet (Android)

Añade el permiso en android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />

Creando el Modelo de Datos

personaje.dart - Modelo del Personaje

class Personaje {
  final int id;
  final String nombre;
  final String descripcion;
  final String imagen;

  Personaje({
    required this.id,
    required this.nombre,
    required this.descripcion,
    required this.imagen,
  });

  factory Personaje.fromJson(Map<String, dynamic> json) {
    return Personaje(
      id: json['id'] ?? 0,
      nombre: json['name'] ?? '',
      descripcion: json['description'] ?? '',
      imagen: json['image'] ?? '',
    );
  }
}

Explicación del Modelo:

  • fromJson: Constructor que convierte JSON a objeto Dart
  • required: Garantiza que todos los campos sean proporcionados
  • ??: Operador de null-safety para valores por defecto
  • final: Inmutabilidad para mejor rendimiento

Servicio para Consumir la API

api_service.dart - Servicio HTTP

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'personaje.dart';

class ApiService {
  static const String _baseUrl = 'https://thesimpsonsapi.com/api';
  
  static Future<List<Personaje>> obtenerPersonajes() async {
    try {
      final response = await http.get(Uri.parse('$_baseUrl/characters?count=20'));
      
      if (response.statusCode == 200) {
        final List<dynamic> jsonData = json.decode(response.body);
        return jsonData.map((json) => Personaje.fromJson(json)).toList();
      } else {
        throw Exception('Error al cargar personajes: ${response.statusCode}');
      }
    } catch (e) {
      throw Exception('Error de conexión: $e');
    }
  }
  
  static Future<Personaje> obtenerPersonajePorId(int id) async {
    final response = await http.get(Uri.parse('$_baseUrl/characters/$id'));
    
    if (response.statusCode == 200) {
      return Personaje.fromJson(json.decode(response.body));
    } else {
      throw Exception('Error al cargar personaje');
    }
  }
}

Gestión de Estado con Provider

personajes_provider.dart - Provider del Estado

import 'package:flutter/material.dart';
import 'personaje.dart';
import 'api_service.dart';

class PersonajesProvider with ChangeNotifier {
  List<Personaje> _personajes = [];
  bool _cargando = false;
  String _error = '';

  List<Personaje> get personajes => _personajes;
  bool get cargando => _cargando;
  String get error => _error;

  Future<void> cargarPersonajes() async {
    _cargando = true;
    _error = '';
    notifyListeners();

    try {
      _personajes = await ApiService.obtenerPersonajes();
    } catch (e) {
      _error = e.toString();
    } finally {
      _cargando = false;
      notifyListeners();
    }
  }
}

Pantalla Principal

personajes_screen.dart - Pantalla de Lista

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

class PersonajesScreen extends StatelessWidget {
  const PersonajesScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Personajes de Los Simpsons'),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () {
              context.read<PersonajesProvider>().cargarPersonajes();
            },
          ),
        ],
      ),
      body: Consumer<PersonajesProvider>(
        builder: (context, provider, child) {
          if (provider.cargando) {
            return const Center(child: CircularProgressIndicator());
          }
          
          if (provider.error.isNotEmpty) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('Error: ${provider.error}'),
                  ElevatedButton(
                    onPressed: () => provider.cargarPersonajes(),
                    child: const Text('Reintentar'),
                  ),
                ],
              ),
            );
          }
          
          return ListView.builder(
            itemCount: provider.personajes.length,
            itemBuilder: (context, index) {
              return PersonajeCard(personaje: provider.personajes[index]);
            },
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => context.read<PersonajesProvider>().cargarPersonajes(),
        child: const Icon(Icons.download),
      ),
    );
  }
}

Widget Personalizado para Personajes

personaje_card.dart - Tarjeta de Personaje

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

class PersonajeCard extends StatelessWidget {
  final Personaje personaje;

  const PersonajeCard({super.key, required this.personaje});

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.all(8),
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Row(
          children: [
            ClipRRect(
              borderRadius: BorderRadius.circular(8),
              child: Image.network(
                'https://thesimpsonsapi.com/${personaje.imagen}',
                width: 60,
                height: 60,
                fit: BoxFit.cover,
                errorBuilder: (context, error, stackTrace) {
                  return Container(
                    width: 60,
                    height: 60,
                    color: Colors.grey[300],
                    child: const Icon(Icons.person, color: Colors.grey),
                  );
                },
              ),
            ),
            const SizedBox(width: 16),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    personaje.nombre,
                    style: const TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    personaje.descripcion,
                    style: TextStyle(
                      fontSize: 14,
                      color: Colors.grey[600],
                    ),
                    maxLines: 2,
                    overflow: TextOverflow.ellipsis,
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Simulador de Llamadas API

Presiona "Simular Llamada API" para ver el resultado

// La respuesta JSON aparecerá aquí

Manejo de Errores

🔄 Error de Conexión

Cuando no hay internet o el servidor no responde

try { response = await http.get(url); } on SocketException { // Manejar error de conexión }

📊 Error HTTP

Cuando el servidor responde con código de error (404, 500, etc.)

if (response.statusCode == 200) { // Procesar datos } else { throw Exception('HTTP ${response.statusCode}'); }

📝 Error de Parseo

Cuando el JSON no tiene el formato esperado

try { var data = json.decode(response.body); } on FormatException { // Manejar error de JSON }

Mejores Prácticas

✅ Separación de Responsabilidades

  • Modelo: Solo datos y conversión JSON
  • Servicio: Lógica de llamadas HTTP
  • Provider: Gestión del estado
  • Widgets: Solo interfaz de usuario

⚡ Optimizaciones

  • Usa const constructores cuando sea posible
  • Implementa paginación para listas largas
  • Cachea respuestas con packages como dio
  • Usa ListView.builder para listas

🔒 Seguridad

  • Valida siempre las respuestas del servidor
  • Usa HTTPS para APIs públicas
  • No expongas keys de API en el código
  • Maneja tokens de autenticación de forma segura

Tip Avanzado

Para APIs más complejas, considera usar el package dio que ofrece interceptores, cancelación de requests, y mejor manejo de errores que el cliente http básico.

Ejercicios para Practicar

Ejercicio 1: Búsqueda en Tiempo Real

Implementa un SearchBar que filtre personajes mientras el usuario escribe.

Solución sugerida:
TextField(
  onChanged: (query) {
    provider.filtrarPersonajes(query);
  },
  decoration: InputDecoration(
    hintText: 'Buscar personaje...',
    prefixIcon: Icon(Icons.search),
  ),
)

Ejercicio 2: Paginación Infinita

Implementa scroll infinito que cargue más personajes al llegar al final.

Solución sugerida:
ListView.builder(
  controller: _scrollController,
  itemCount: provider.personajes.length + 1,
  itemBuilder: (context, index) {
    if (index == provider.personajes.length) {
      provider.cargarMasPersonajes();
      return CircularProgressIndicator();
    }
    return PersonajeCard(...);
  },
)

Ejercicio 3: Detalles del Personaje

Crea una pantalla de detalles que muestre información completa al hacer tap.

Solución sugerida:
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => DetallesScreen(personaje: personaje),
  ),
);