Saltar al contenido principal

Enviar datos a internet

Cómo utilizar el paquete http para enviar datos a través de Internet.

Enviar datos a internet es necesario para la mayoría de las aplicaciones. El paquete http también tiene eso cubierto.

Esta receta utiliza los siguientes pasos:

  1. Agrega el paquete http.
  2. Envía datos a un servidor utilizando el paquete http.
  3. Convierte la respuesta en un objeto Dart personalizado.
  4. Obtén un título de la entrada del usuario.
  5. 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. Enviar datos al servidor

#

Esta receta cubre cómo crear un Album enviando el título de un álbum a JSONPlaceholder utilizando el método http.post().

Importa dart:convert para acceder a jsonEncode para codificar los datos:

dart
import 'dart:convert';

Utiliza el método http.post() para enviar los datos codificados:

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

El método http.post() 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 createAlbum() toma un argumento title que se envía al servidor para crear un 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

#

Utiliza los siguientes pasos para actualizar la función createAlbum() 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 CREATED con un código de estado de 201, entonces convierte el JSON Map en un Album utilizando el método factory fromJson().
  3. Si el servidor no devuelve una respuesta CREATED con un código de estado de 201, 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> createAlbum(String title) async {
  final response = await http.post(
    Uri.parse('https://jsonplaceholder.typicode.com/albums'),
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, String>{'title': title}),
  );

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

¡Genial! Ahora tienes una función que envía el título a un servidor para crear un álbum.

4. Obtener un título de la entrada del usuario

#

A continuación, crea un TextField para ingresar un título y un ElevatedButton para enviar los datos al 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 createAlbum().

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

Al presionar el botón Crear datos, realiza la solicitud de red, que envía los datos en el TextField al servidor como una solicitud POST. El Future, _futureAlbum, se utilizará 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 createAlbum().
  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 createAlbum() debe lanzar una excepción incluso en el caso de una respuesta del servidor "404 Not Found". Si createAlbum() 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> createAlbum(String title) async {
  final response = await http.post(
    Uri.parse('https://jsonplaceholder.typicode.com/albums'),
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, String>{'title': title}),
  );

  if (response.statusCode == 201) {
    // If the server did return a 201 CREATED response,
    // then parse the JSON.
    return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
  } else {
    // If the server did not return a 201 CREATED response,
    // then throw an exception.
    throw Exception('Failed to create 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();
  Future<Album>? _futureAlbum;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Create Data Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: Scaffold(
        appBar: AppBar(title: const Text('Create Data Example')),
        body: Container(
          alignment: Alignment.center,
          padding: const EdgeInsets.all(8),
          child: (_futureAlbum == null) ? buildColumn() : buildFutureBuilder(),
        ),
      ),
    );
  }

  Column buildColumn() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        TextField(
          controller: _controller,
          decoration: const InputDecoration(hintText: 'Enter Title'),
        ),
        ElevatedButton(
          onPressed: () {
            setState(() {
              _futureAlbum = createAlbum(_controller.text);
            });
          },
          child: const Text('Create Data'),
        ),
      ],
    );
  }

  FutureBuilder<Album> buildFutureBuilder() {
    return 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();
      },
    );
  }
}