Saltar al contenido principal

Actualizar datos a través de internet

Cómo utilizar el paquete http para actualizar datos en Internet.

Actualizar datos a través de internet es necesario para la mayoría de las aplicaciones. ¡El paquete http lo tiene cubierto!

Esta receta utiliza los siguientes pasos:

  1. Agrega el paquete http.
  2. Actualiza datos a través de internet utilizando el paquete http.
  3. Convierte la respuesta en un objeto Dart personalizado.
  4. Obtener los datos de internet.
  5. Actualiza el título existente a partir de la entrada del usuario.
  6. Actualizar y mostrar la respuesta en pantalla.

1. Agregar el paquete http

#

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. Actualizar datos a través de internet utilizando el paquete http

#

Esta receta cubre cómo actualizar el título de un álbum en JSONPlaceholder utilizando el método http.put().

dart
Future<http.Response> updateAlbum(String title) {
  return http.put(
    Uri.parse('https://jsonplaceholder.typicode.com/albums/1'),
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, String>{'title': title}),
  );
}

El método http.put() 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.
  • El método updateAlbum() toma un argumento, title, que se envía al servidor para actualizar el Album.

3. Convertir http.Response 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 id;
  final String title;

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

  factory Album.fromJson(Map<String, dynamic> json) {
    return switch (json) {
      {'id': int id, 'title': String title} => Album(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 updateAlbum() 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 UPDATED 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 UPDATED con un código de estado de 200, entonces lanza una excepción. (Incluso en el caso de una respuesta del 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> updateAlbum(String title) async {
  final response = await http.put(
    Uri.parse('https://jsonplaceholder.typicode.com/albums/1'),
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, String>{'title': title}),
  );

  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 update album.');
  }
}

¡Genial! Ahora tienes una función que actualiza el título de un álbum.

Obtener los datos de internet

#

Obtén los datos de internet antes de poder actualizarlos. Para ver un ejemplo completo, consulta la receta Obtener datos.

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

  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');
  }
}

Idealmente, utilizarás este método para establecer _futureAlbum durante initState para obtener los datos de internet.

4. Actualizar el título existente a partir de la entrada del usuario

#

Crea un TextField para ingresar un título y un ElevatedButton para actualizar los datos en el servidor. También define un TextEditingController para leer la entrada del usuario desde un TextField.

Cuando se presiona el ElevatedButton, el _futureAlbum se establece con el valor devuelto por el método updateAlbum().

dart
Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    Padding(
      padding: const EdgeInsets.all(8),
      child: TextField(
        controller: _controller,
        decoration: const InputDecoration(hintText: 'Enter Title'),
      ),
    ),
    ElevatedButton(
      onPressed: () {
        setState(() {
          _futureAlbum = updateAlbum(_controller.text);
        });
      },
      child: const Text('Update Data'),
    ),
  ],
);

Al presionar el botón Actualizar datos, una solicitud de red envía los datos del TextField al servidor como una solicitud PUT. La variable _futureAlbum se utiliza en el siguiente paso.

5. Mostrar la respuesta en pantalla

#

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 updateAlbum().
  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. Es por eso que la función updateAlbum debe lanzar una excepción incluso en el caso de una respuesta del servidor "404 Not Found". Si updateAlbum devuelve null, entonces el CircularProgressIndicator se mostrará indefinidamente.

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

    return const CircularProgressIndicator();
  },
);

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'),
  );

  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');
  }
}

Future<Album> updateAlbum(String title) async {
  final response = await http.put(
    Uri.parse('https://jsonplaceholder.typicode.com/albums/1'),
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, String>{'title': title}),
  );

  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 update album.');
  }
}

class Album {
  final int id;
  final String title;

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

  factory Album.fromJson(Map<String, dynamic> json) {
    return switch (json) {
      {'id': int id, 'title': String title} => Album(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() {
    return _MyAppState();
  }
}

class _MyAppState extends State<MyApp> {
  final TextEditingController _controller = TextEditingController();
  late Future<Album> _futureAlbum;

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Update Data Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: Scaffold(
        appBar: AppBar(title: const Text('Update Data Example')),
        body: Container(
          alignment: Alignment.center,
          padding: const EdgeInsets.all(8),
          child: FutureBuilder<Album>(
            future: _futureAlbum,
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.done) {
                if (snapshot.hasData) {
                  return Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      Text(snapshot.data!.title),
                      TextField(
                        controller: _controller,
                        decoration: const InputDecoration(
                          hintText: 'Enter Title',
                        ),
                      ),
                      ElevatedButton(
                        onPressed: () {
                          setState(() {
                            _futureAlbum = updateAlbum(_controller.text);
                          });
                        },
                        child: const Text('Update Data'),
                      ),
                    ],
                  );
                } else if (snapshot.hasError) {
                  return Text('${snapshot.error}');
                }
              }

              return const CircularProgressIndicator();
            },
          ),
        ),
      ),
    );
  }
}