Saltar al contenido principal

Obtener datos de internet

Cómo obtener datos a través de Internet utilizando el paquete http.

Obtener datos de internet es necesario para la mayoría de las aplicaciones. Afortunadamente, Dart y Flutter proporcionan herramientas, como el paquete http, para este tipo de trabajo.

Esta receta utiliza los siguientes pasos:

  1. Agrega el paquete http.
  2. Realiza una solicitud de red utilizando el paquete http.
  3. Convierte la respuesta en un objeto Dart personalizado.
  4. Obtén y muestra los datos con Flutter.

1. Agregar el paquete http

#

El paquete http proporciona la forma más sencilla de obtener datos de internet.

Para agregar el paquete http como dependencia, ejecuta flutter pub add:

flutter pub add http

Importa el paquete http.

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

Si estás haciendo el despliegue en Android, edita tu archivo AndroidManifest.xml para agregar el permiso de Internet.

xml
<!-- Required to fetch data from the internet. -->
<uses-permission android:name="android.permission.INTERNET" />

Del mismo modo, si estás haciendo el despliegue en macOS, edita tus archivos macos/Runner/DebugProfile.entitlements y macos/Runner/Release.entitlements para incluir el entitlement del cliente de red.

xml
<!-- Required to fetch data from the internet. -->
<key>com.apple.security.network.client</key>
<true/>

2. Realizar una solicitud de red

#

Esta receta cubre cómo obtener un álbum de ejemplo desde JSONPlaceholder utilizando el método http.get().

dart
Future<http.Response> fetchAlbum() {
  return http.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));
}

El método http.get() devuelve un Future que contiene una Response.

  • Future es una clase principal de Dart para trabajar con operaciones asíncronas. Un objeto Future representa un valor potencial o un error que estará disponible en algún momento en el futuro.
  • La clase http.Response contiene los datos recibidos de una llamada http exitosa.

3. Convertir la respuesta en un objeto Dart personalizado

#

Aunque es fácil realizar una solicitud de red, trabajar con un Future<http.Response> crudo no es muy conveniente. Para facilitarte la vida, convierte la http.Response en un objeto de Dart.

Crear una clase Album

#

Primero, crea una clase Album que contenga los datos de la solicitud de red. Incluye un constructor factory que crea un Album a partir de JSON.

Convertir JSON utilizando pattern matching es solo una opción. Para obtener más información, consulta el artículo completo sobre JSON y serialización.

dart
class Album {
  final int userId;
  final int id;
  final String title;

  const Album({required this.userId, required this.id, required this.title});

  factory Album.fromJson(Map<String, dynamic> json) {
    return switch (json) {
      {'userId': int userId, 'id': int id, 'title': String title} => Album(
        userId: userId,
        id: id,
        title: title,
      ),
      _ => throw const FormatException('Failed to load album.'),
    };
  }
}

Convertir la http.Response en un Album

#

Ahora, utiliza los siguientes pasos para actualizar la función fetchAlbum() para que devuelva un Future<Album>:

  1. Convierte el cuerpo de la respuesta en un JSON Map con el paquete dart:convert.
  2. Si el servidor devuelve una respuesta correcta con un código de estado de 200, entonces convierte el JSON Map en un Album utilizando el método factory fromJson().
  3. Si el servidor no devuelve una respuesta correcta con un código de estado de 200, entonces lanza una excepción. (Incluso en el caso de una respuesta de servidor "404 Not Found", lanza una excepción. No devuelvas null. Esto es importante al examinar los datos en snapshot, como se muestra a continuación).
dart
Future<Album> fetchAlbum() async {
  final response = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/albums/1'),
    headers: {'Accept': 'application/json'},
  );

  if (response.statusCode == 200) {
    // If the server did return a 200 OK response,
    // then parse the JSON.
    return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
  } else {
    // If the server did not return a 200 OK response,
    // then throw an exception.
    throw Exception('Failed to load album');
  }
}

¡Genial! Ahora tienes una función que obtiene un álbum de internet.

4. Obtener los datos

#

Llama al método fetchAlbum() ya sea en initState() o en los métodos didChangeDependencies().

El método initState() se llama exactamente una vez y nunca más. Si deseas tener la opción de recargar la API en respuesta al cambio de un InheritedWidget, coloca la llamada en el método didChangeDependencies(). Consulta State para más detalles.

dart
class _MyAppState extends State<MyApp> {
  late Future<Album> futureAlbum;

  @override
  void initState() {
    super.initState();
    futureAlbum = fetchAlbum();
  }
  // ···
}

Este Future se utiliza en el siguiente paso.

5. Mostrar los datos

#

Para mostrar los datos en pantalla, utiliza el widget FutureBuilder. El widget FutureBuilder viene con Flutter y facilita el trabajo con fuentes de datos asíncronas.

Debes proporcionar dos parámetros:

  1. El Future con el que quieres trabajar. En este caso, el future devuelto por la función fetchAlbum().
  2. Una función builder que le indica a Flutter qué renderizar, según el estado del Future: cargando, éxito o error.

Ten en cuenta que snapshot.hasData solo devuelve true cuando el snapshot contiene un valor de datos no nulo.

Debido a que fetchAlbum solo puede devolver valores no nulos, la función debe lanzar una excepción incluso en el caso de una respuesta del servidor "404 Not Found". Lanzar una excepción establece snapshot.hasError a true, lo cual se puede utilizar para mostrar un mensaje de error.

De lo contrario, se mostrará el spinner.

dart
FutureBuilder<Album>(
  future: futureAlbum,
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      return Text(snapshot.data!.title);
    } else if (snapshot.hasError) {
      return Text('${snapshot.error}');
    }

    // By default, show a loading spinner.
    return const CircularProgressIndicator();
  },
)

¿Por qué se llama a fetchAlbum() en initState()?

#

Aunque es conveniente, no se recomienda realizar una llamada a la API en un método build().

Flutter llama al método build() cada vez que necesita cambiar algo en la vista, y esto sucede con sorprendente frecuencia. Si colocas el método fetchAlbum() dentro de build(), se llamará repetidamente en cada reconstrucción, lo que ralentizará la aplicación.

Almacenar el resultado de fetchAlbum() en una variable de estado asegura que el Future se ejecute solo una vez y luego se almacene en caché para las reconstrucciones posteriores.

Pruebas

#

Para obtener información sobre cómo probar esta funcionalidad, consulta las siguientes recetas:

Ejemplo completo

#
dart
import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

Future<Album> fetchAlbum() async {
  final response = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/albums/1'),
    headers: {'Accept': 'application/json'},
  );

  if (response.statusCode == 200) {
    // If the server did return a 200 OK response,
    // then parse the JSON.
    return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
  } else {
    // If the server did not return a 200 OK response,
    // then throw an exception.
    throw Exception('Failed to load album');
  }
}

class Album {
  final int userId;
  final int id;
  final String title;

  const Album({required this.userId, required this.id, required this.title});

  factory Album.fromJson(Map<String, dynamic> json) {
    return switch (json) {
      {'userId': int userId, 'id': int id, 'title': String title} => Album(
        userId: userId,
        id: id,
        title: title,
      ),
      _ => throw const FormatException('Failed to load album.'),
    };
  }
}

void main() => runApp(const MyApp());

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late Future<Album> futureAlbum;

  @override
  void initState() {
    super.initState();
    futureAlbum = fetchAlbum();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Fetch Data Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: Scaffold(
        appBar: AppBar(title: const Text('Fetch Data Example')),
        body: Center(
          child: FutureBuilder<Album>(
            future: futureAlbum,
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                return Text(snapshot.data!.title);
              } else if (snapshot.hasError) {
                return Text('${snapshot.error}');
              }

              // By default, show a loading spinner.
              return const CircularProgressIndicator();
            },
          ),
        ),
      ),
    );
  }

}