JSON serialization

6 mins

6 mins

Ashutosh

Published on Oct 18, 2024

How to Parse JSON in the Background with Flutter: Parsing Large JSON Files to avoid Jank.

Introduction

Smooth app performance is essential for providing a seamless user experience in Flutter. However, parsing large JSON data directly on the main UI thread can cause jank, leading to UI freezes and negatively impacting the app's responsiveness. In this blog, we’ll explore how to handle large JSON parsing efficiently using background isolates in Flutter to prevent jank.

Looking to master JSON handling in your Flutter apps? This guide walks you through the essentials of JSON serialization in Flutter, helping you boost app performance and streamline data management. Get started now and level up your Flutter development skills with clear, practical insights. Read the full guide: JSON and Serialization in Flutter: A Comprehensive Guide for Flutter App Development

Why Parse JSON in the Background

When your Flutter app deals with large JSON data, parsing it on the main thread can block the UI, creating lag or jank. Since Flutter operates on a single-threaded model, expensive computations, like JSON parsing, can hog the UI thread, leading to performance issues. By offloading this work to a background thread, you can keep the UI smooth and responsive.

Why Parse JSON in the Background: Looking to master JSON handling in your Flutter apps?

Steps to Efficient JSON Parsing:

It looks like you shared the detailed steps for parsing large JSON data in Flutter, highlighting how to avoid jank using background isolates. This approach ensures that heavy JSON processing doesn't affect the UI performance, keeping the app smooth and responsive.

Here’s a concise breakdown of the process:

1. Add the http package

Start by adding the http package to make network requests.

flutter pub add http

2. Make a Network Request

Fetch the JSON data using the http.get() method. In this example, we use the JSONPlaceholder API to get a large list of photos.

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

3. Parse and Convert the JSON

Next, create a Photo class to model the data and use a parsePhotos function to convert the response into a list of Photo objects.

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'],
      id: json['id'],
      title: json['title'],
      url: json['url'],
      thumbnailUrl: json['thumbnailUrl'],
    );
  }
}

List<Photo> parsePhotos(String responseBody) {
  final parsed = (jsonDecode(responseBody) as List).cast<Map<String, dynamic>>();
  return parsed.map<Photo>((json) => Photo.fromJson(json)).toList();
}

To update the fetchPhotos() function so that it returns a Future<List<Photo>>, follow these steps:

  1. Create the parsePhotos() Function: This function will take the response body as a string, decode the JSON into a list of maps, and map each item to the Photo class.

  2. Update the fetchPhotos() Function: Inside fetchPhotos(), make the network request, retrieve the JSON response, and use the parsePhotos() function to convert it into a list of Photo objects.

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

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

// The fetchPhotos function makes a network request and uses parsePhotos.
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. Move Parsing to a Background Isolate

To prevent the UI from freezing when handling large JSON data, move the parsing logic to a separate isolate using Flutter’s compute() function.

Future<List<Photo>> fetchPhotos(http.Client client) async {
  final response = await client.get(Uri.parse('https://jsonplaceholder.typicode.com/photos'));
  return compute(parsePhotos, response.body);  // Run the parsing in a background isolate.
}

Notes on Isolates

Isolates in Flutter allow you to run code in parallel, preventing heavy computations from blocking the UI thread. Ensure that you only pass simple data between isolates, as complex objects like Future or http.Response can’t be passed.

  • Communication Between Isolates:
    Ensure you are sending simple data types, like strings or lists, between isolates for better performance.

  • Handling Complex Data Structures:
    For more complex data, ensure you structure the isolate functions to handle the mapping efficiently without introducing errors or bottlenecks.

Complete example

Here’s a complete example that demonstrates how to fetch large JSON data from a network, parse it in a background isolate, and use it in your Flutter app:

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).cast<Map<String, dynamic>>();

  return parsed.map<Photo>((json) => Photo.fromJson(json)).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);
      },
    );
  }
}

Conclusion

By moving JSON parsing to a background isolate using Flutter’s compute() function, you can prevent jank and ensure your app’s UI remains smooth and responsive. This method not only enhances user experience but also makes your app more robust, especially when handling large datasets.

FAQs:

  1. What are isolates in Flutter?
    Isolates are independent threads with their own memory space, enabling parallel execution without blocking the UI.

  2. When should I use the compute() function?
    Use compute() when dealing with CPU-intensive tasks like parsing large JSON data to avoid blocking the main UI thread.

  3. Does using isolates improve performance in all scenarios?
    Isolates are beneficial for heavy computations. However, for smaller data, the overhead of managing isolates might not be worth it.