flutter-futurebuilder-manejo-errores

Manejo de errores y FutureBuilder en Flutter

  • 4 min

El manejo de errores es preparar la aplicación para responder bien cuando algo falla.

En los artículos anteriores hemos sido muy optimistas. Asumimos que el servidor siempre responde, que el usuario siempre tiene internet y que los datos siempre llegan perfectos.

Pero la realidad es cruel. Los servidores se caen, el WiFi se corta y las APIs cambian.

Si no gestionas esto, tu usuario verá una pantalla en blanco infinita o, peor aún, la aplicación se cerrará de golpe. Hoy vamos a aprender a gestionar el caos y a mostrarlo elegantemente usando el widget FutureBuilder.

Tratar errores en el servicio

La defensa empieza en la cocina (en nuestro UserService). Hasta ahora solo mirábamos si el código era 200. Pero, ¿y si no tengo internet? http.get lanzará una excepción y la App morirá.

Debemos envolver la llamada en un bloque try-catch.

class UserService {
  Future<List<User>> getUsers() async {
    final url = Uri.https('jsonplaceholder.typicode.com', '/users');

    try {
      final response = await http.get(url);

      if (response.statusCode == 200) {
        // ... lógica de parseo (vista en el artículo anterior) ...
        return usuarios;
      } else {
        // El servidor respondió, pero con error (Ej: 404 Not Found)
        throw Exception('Error ${response.statusCode}: No se encontraron usuarios');
      }
      
    } on http.ClientException catch (e) {
      // Error del cliente HTTP, por ejemplo al conectar
      throw Exception('No se pudo completar la conexión: $e');
    } on FormatException catch (e) {
      // La respuesta no tenía el formato esperado
      throw Exception('La respuesta del servidor no es válida: $e');
    }
  }
}
Copied!

Ahora nuestro servicio es robusto. Si algo falla, lanza una Exception controlada con un mensaje que podemos mostrar.

El problema del setState manual

Para mostrar estos datos en la pantalla, hasta ahora tendríamos que haber hecho esto en nuestro StatefulWidget:

  1. Crear variable bool isLoading = true.
  2. Crear variable String error = ''.
  3. Crear variable List<User> users = [].
  4. Llamar a la API en initState.
  5. Usar setState para apagar la carga, guardar los datos o guardar el error.

Es mucho código repetitivo (“Boilerplate”). Para simplificar esto, Flutter nos regala el FutureBuilder.

FutureBuilder para representar la espera

El FutureBuilder es un Widget que se construye a sí mismo basándose en el estado de un Future.

Escucha una “promesa” y se redibuja automáticamente cuando la promesa cambia de estado (de “Esperando” a “Completado” o “Error”).

Tiene dos propiedades clave:

  • future: La tarea asíncrona que estamos esperando (userService.getUsers()).
  • builder: Una función que nos da el context y un snapshot (una foto instantánea de cómo va la tarea).

Analizando el AsyncSnapshot

El objeto snapshot tiene la información que necesitamos:

  • snapshot.connectionState: ¿Estamos esperando (waiting) o ya terminó (done)?
  • snapshot.hasData: ¿Tenemos datos válidos?
  • snapshot.hasError: ¿Falló la tarea?
  • snapshot.data: Los datos en sí (nuestra lista de usuarios).
  • snapshot.error: El objeto de la excepción si falló.

Implementación completa

Vamos a ver cómo queda una pantalla profesional que gestiona los 3 estados (Carga, Error, Datos).

class UserListScreen extends StatefulWidget {
  @override
  _UserListScreenState createState() => _UserListScreenState();
}

class _UserListScreenState extends State<UserListScreen> {
  final UserService _userService = UserService();
  
  // Guardamos el Future en una variable para evitar recargas innecesarias
  late Future<List<User>> _futureUsers;

  @override
  void initState() {
    super.initState();
    _futureUsers = _userService.getUsers();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Usuarios con FutureBuilder")),
      body: FutureBuilder<List<User>>(
        future: _futureUsers, // 1. Qué esperamos
        builder: (context, snapshot) {
          
          // CASO 1: Todavía estamos cargando
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          }

          // CASO 2: Ha ocurrido un error
          if (snapshot.hasError) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.error_outline, color: Colors.red, size: 50),
                  Text('No se pudieron cargar los usuarios'),
                  ElevatedButton(
                    onPressed: () {
                      // Truco para reintentar: volver a asignar el future
                      setState(() {
                        _futureUsers = _userService.getUsers();
                      });
                    },
                    child: Text("Reintentar"),
                  )
                ],
              ),
            );
          }

          // CASO 3: Tenemos datos (Éxito)
          if (snapshot.hasData) {
            final usuarios = snapshot.data!; // El ! asegura que no es nulo
            
            return ListView.builder(
              itemCount: usuarios.length,
              itemBuilder: (ctx, i) => ListTile(
                title: Text(usuarios[i].name),
                subtitle: Text(usuarios[i].email),
                leading: CircleAvatar(child: Text(usuarios[i].name[0])),
              ),
            );
          }

          // Caso por defecto (raro que llegue aquí)
          return Text("Sin datos");
        },
      ),
    );
  }
}
Copied!

Fíjate que he guardado _futureUsers en el initState. Si pones future: _userService.getUsers() directamente dentro del build, cada vez que toques la pantalla o abras el teclado, Flutter volverá a lanzar la petición HTTP. Guardarlo en una variable evita llamadas duplicadas.