Saltar al contenido principal

Parsear JSON en segundo plano

Cómo realizar una tarea en segundo plano.

Por defecto, las aplicaciones de Dart hacen todo su trabajo en un solo hilo. En muchos casos, este modelo simplifica la codificación y es lo suficientemente rápido como para no provocar un rendimiento deficiente de la aplicación ni animaciones entrecortadas, lo que a menudo se denomina "jank."

Sin embargo, es posible que necesites realizar un cálculo costoso, como parsear un documento JSON muy grande. Si este trabajo tarda más de 16 milisegundos, tus usuarios experimentarán jank.

Para evitar el jank, debes realizar cálculos costosos como este en segundo plano, utilizando un Isolate independiente. 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 una lista de fotos.
  4. Mueve este trabajo a un isolate independiente.

1. Agregar el paquete http

#

Primero, agrega el paquete http a tu proyecto. El paquete http facilita la realización de solicitudes de red, como la obtención de datos desde un endpoint JSON.

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

flutter pub add http

2. Realizar una solicitud de red

#

Este ejemplo cubre cómo obtener un documento JSON grande que contiene una lista de 5000 objetos de fotos desde la API REST de JSONPlaceholder, utilizando el método http.get().

dart
Future<http.Response> fetchPhotos(http.Client client) async {
  return client.get(Uri.parse('https://jsonplaceholder.typicode.com/photos'));
}

3. Parsear y convertir el JSON en una lista de fotos

#

A continuación, siguiendo la guía de la receta Obtener datos de internet, convierte la respuesta http.Response en una lista de objetos de Dart. Esto hace que sea más fácil trabajar con los datos.

Crear una clase Photo

#

Primero, crea una clase Photo que contenga datos sobre una foto. Incluye un método factory fromJson() para facilitar la creación de una Photo a partir de un objeto JSON.

dart
class Photo {
  final int albumId;
  final int id;
  final String title;
  final String url;
  final String thumbnailUrl;

  const Photo({
    required this.albumId,
    required this.id,
    required this.title,
    required this.url,
    required this.thumbnailUrl,
  });

  factory Photo.fromJson(Map<String, dynamic> json) {
    return Photo(
      albumId: json['albumId'] as int,
      id: json['id'] as int,
      title: json['title'] as String,
      url: json['url'] as String,
      thumbnailUrl: json['thumbnailUrl'] as String,
    );
  }
}

Convertir la respuesta en una lista de fotos

#

Ahora, utiliza las siguientes instrucciones para actualizar la función fetchPhotos() para que devuelva un Future<List<Photo>>:

  1. Crea una función parsePhotos() que convierta el cuerpo de la respuesta en una List<Photo>.
  2. Utiliza la función parsePhotos() en la función fetchPhotos().
dart
// A function that converts a response body into a List<Photo>.
List<Photo> parsePhotos(String responseBody) {
  final parsed = (jsonDecode(responseBody) as List<Object?>)
      .cast<Map<String, Object?>>();

  return parsed.map<Photo>(Photo.fromJson).toList();
}

Future<List<Photo>> fetchPhotos(http.Client client) async {
  final response = await client.get(
    Uri.parse('https://jsonplaceholder.typicode.com/photos'),
  );

  // Synchronously run parsePhotos in the main isolate.
  return parsePhotos(response.body);
}

4. Mover este trabajo a un isolate independiente

#

Si ejecutas la función fetchPhotos() en un dispositivo más lento, es posible que notes que la aplicación se congela por un breve momento mientras parsea y convierte el JSON. Esto es jank, y querrás deshacerte de él.

Puedes eliminar el jank moviendo el parseo y la conversión a un isolate en segundo plano utilizando la función compute() proporcionada por Flutter. La función compute() ejecuta funciones costosas en un isolate en segundo plano y devuelve el resultado. En este caso, ejecuta la función parsePhotos() en segundo plano.

dart
Future<List<Photo>> fetchPhotos(http.Client client) async {
  final response = await client.get(
    Uri.parse('https://jsonplaceholder.typicode.com/photos'),
  );

  // Use the compute function to run parsePhotos in a separate isolate.
  return compute(parsePhotos, response.body);
}

Notas sobre el trabajo con isolates

#

Los isolates se comunican pasando mensajes de un lado a otro. Estos mensajes pueden ser valores primitivos, como null, num, bool, double, o String, u objetos simples como la List<Photo> en este ejemplo.

Es posible que experimentes errores si intentas pasar objetos más complejos, como un Future o http.Response entre isolates.

Como solución alternativa, echa un vistazo a los paquetes worker_manager o workmanager para el procesamiento en segundo plano.

Ejemplo completo

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

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

Future<List<Photo>> fetchPhotos(http.Client client) async {
  final response = await client.get(
    Uri.parse('https://jsonplaceholder.typicode.com/photos'),
  );

  // Use the compute function to run parsePhotos in a separate isolate.
  return compute(parsePhotos, response.body);
}

// A function that converts a response body into a List<Photo>.
List<Photo> parsePhotos(String responseBody) {
  final parsed = (jsonDecode(responseBody) as List<Object?>)
      .cast<Map<String, Object?>>();

  return parsed.map<Photo>(Photo.fromJson).toList();
}

class Photo {
  final int albumId;
  final int id;
  final String title;
  final String url;
  final String thumbnailUrl;

  const Photo({
    required this.albumId,
    required this.id,
    required this.title,
    required this.url,
    required this.thumbnailUrl,
  });

  factory Photo.fromJson(Map<String, dynamic> json) {
    return Photo(
      albumId: json['albumId'] as int,
      id: json['id'] as int,
      title: json['title'] as String,
      url: json['url'] as String,
      thumbnailUrl: json['thumbnailUrl'] as String,
    );
  }
}

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

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

  @override
  Widget build(BuildContext context) {
    const appTitle = 'Isolate Demo';

    return const MaterialApp(
      title: appTitle,
      home: MyHomePage(title: appTitle),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late Future<List<Photo>> futurePhotos;

  @override
  void initState() {
    super.initState();
    futurePhotos = fetchPhotos(http.Client());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: FutureBuilder<List<Photo>>(
        future: futurePhotos,
        builder: (context, snapshot) {
          if (snapshot.hasError) {
            return const Center(child: Text('An error has occurred!'));
          } else if (snapshot.hasData) {
            return PhotosList(photos: snapshot.data!);
          } else {
            return const Center(child: CircularProgressIndicator());
          }
        },
      ),
    );
  }
}

class PhotosList extends StatelessWidget {
  const PhotosList({super.key, required this.photos});

  final List<Photo> photos;

  @override
  Widget build(BuildContext context) {
    return GridView.builder(
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
      ),
      itemCount: photos.length,
      itemBuilder: (context, index) {
        return Image.network(photos[index].thumbnailUrl);
      },
    );
  }
}

Demostración de isolate