Consumo de APIs en Flutter

Learn to connect your Flutter app with REST APIs using The Simpsons API

Download Complete Project

What 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.

App Flutter
Solicitud HTTP
API The Simpsons
Datos JSON
App Flutter

The Simpsons API

We'll use https://thesimpsonsapi.com/ - a public API that provides information about characters from The Simpsons.

Main Endpoints:

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

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 const constructors when possible
  • Implement pagination for long lists
  • Cache responses with packages like dio
  • Use ListView.builder for 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),
  ),
);