Consumo de APIs en Flutter
Learn to connect your Flutter app with REST APIs using The Simpsons API
Download Complete ProjectWhat is a REST API?
A API REST (Representational State Transfer) is an interface that lets different applications communicate with each other using web protocols like HTTP.
The Simpsons API
We'll use https://thesimpsonsapi.com/ - a public API that provides information about characters from The Simpsons.
Main Endpoints:
JSON Response Example:
[
{
"id": 1,
"name": "Homer Simpson",
"description": "Padre de familia",
"image": "homer.jpg"
},
{
"id": 2,
"name": "Marge Simpson",
"description": "Madre de familia",
"image": "marge.jpg"
}
]
Initial Setup
1. Add Dependencies
Add http and provider to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
http: ^1.1.0
provider: ^6.1.1
2. Internet Permissions (Android)
Add the permission in android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
Creating the Data Model
personaje.dart - Character Model
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'] ?? '',
);
}
}
Model Explanation:
- fromJson: Constructor that converts JSON to a Dart object
- required: Ensures that all fields are provided
- ??: Null-safety operator for default values
- final: Immutability for better performance
Service for Consuming the API
api_service.dart - HTTP Service
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');
}
}
}
State Management with Provider
personajes_provider.dart - State Provider
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();
}
}
}
Main Screen
personajes_screen.dart - List Screen
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('Retry'),
),
],
),
);
}
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),
),
);
}
}
Custom Widget for Characters
personaje_card.dart - Character Card
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,
),
],
),
),
],
),
),
);
}
}
API Call Simulator
Press "Simulate API Call" to see the result
// La respuesta JSON aparecerá aquí
Error Handling
🔄 Connection Error
When there's no internet or the server doesn't respond
try {
response = await http.get(url);
} on SocketException {
// Manejar error de conexión}
📊 Error HTTP
When the server responds with an error code (404, 500, etc.)
if (response.statusCode == 200) {
// Procesar datos} else {
throw Exception('HTTP ${response.statusCode}');
}
📝 Parsing Error
When the JSON doesn't have the expected format
try {
var data = json.decode(response.body);
} on FormatException {
// Manejar error de JSON}
Best Practices
✅ Separation of Concerns
- Model: Only data and JSON conversion
- Service: HTTP call logic
- Provider: State management
- Widgets: Only user interface
⚡ Optimizations
- Use
constconstructors when possible - Implement pagination for long lists
- Cache responses with packages like dio
- Use
ListView.builderfor lists
🔒 Security
- Always validate server responses
- Use HTTPS for public APIs
- Don't expose API keys in the code
- Handle authentication tokens securely
Advanced Tip
For more complex APIs, consider using the dio package, which offers interceptors, request cancellation, and better error handling than the basic http client.
Exercises to Practice
Exercise 1: Real-Time Search
Implement a SearchBar that filters characters as the user types.
Suggested solution:
TextField(
onChanged: (query) {
provider.filtrarPersonajes(query);
},
decoration: InputDecoration(
hintText: 'Buscar personaje...',
prefixIcon: Icon(Icons.search),
),
)
Exercise 2: Infinite Pagination
Implement infinite scroll that loads more characters when reaching the end.
Suggested solution:
ListView.builder(
controller: _scrollController,
itemCount: provider.personajes.length + 1,
itemBuilder: (context, index) {
if (index == provider.personajes.length) {
provider.cargarMasPersonajes();
return CircularProgressIndicator();
}
return PersonajeCard(...);
},
)
Exercise 3: Character Details
Create a details screen that shows complete information when tapped.
Suggested solution:
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetallesScreen(personaje: personaje),
),
);