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.
The Simpsons API
Utilizaremos https://thesimpsonsapi.com/ - una API pública que proporciona información sobre personajes de Los Simpsons.
Endpoints Principales:
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
constconstructores cuando sea posible - Implementa paginación para listas largas
- Cachea respuestas con packages como dio
- Usa
ListView.builderpara 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),
),
);