Performing JSON Parsing in Background Isolates in Flutter

Flutter is known for its ability to build performant, cross-platform applications. However, when dealing with complex tasks like parsing large JSON files, the main thread can become blocked, leading to a poor user experience. To mitigate this, Flutter provides a way to execute Dart code in background isolates, ensuring that the UI remains responsive. This post will guide you through the process of performing JSON parsing in background isolates in Flutter.

Understanding Background Isolates

Isolates are Dart’s way of enabling concurrency. Unlike threads, isolates have their own memory space and communicate with each other via message passing. This eliminates the possibility of shared state concurrency issues. When dealing with computationally intensive tasks like JSON parsing, isolates are ideal because they prevent the main UI thread from becoming blocked, thereby ensuring a smooth user experience.

Why Use Background Isolates for JSON Parsing?

  • Responsiveness: Keeps the UI responsive by offloading the parsing task to a separate isolate.
  • Performance: Improves performance by leveraging multiple cores on the device.
  • Avoids Jank: Prevents UI jank or stutter, especially when dealing with large JSON files.

How to Perform JSON Parsing in Background Isolates

To parse JSON in a background isolate, you’ll need to follow these steps:

Step 1: Add the dart:convert and dart:isolate Imports

Import the necessary libraries in your Dart file:

import 'dart:convert';
import 'dart:isolate';

Step 2: Create a Function to Parse JSON

Define a function that parses the JSON string into a Dart object (e.g., a List or Map). This function will be executed in the isolate:

List parseJson(String jsonString) {
  return jsonDecode(jsonString) as List;
}

Step 3: Create a Function to Run in the Isolate

Set up a function that will serve as the entry point for the isolate. This function will receive the JSON string and a SendPort for sending the result back to the main isolate:

void parseJsonInIsolate(Map message) {
  String jsonString = message['jsonString'] as String;
  SendPort sendPort = message['sendPort'] as SendPort;

  final parsedJson = parseJson(jsonString);
  sendPort.send(parsedJson);
}

Step 4: Use the Isolate.spawn Method

In your main Dart file, use Isolate.spawn to run the parseJsonInIsolate function in a new isolate. Pass the JSON string and a SendPort to the isolate:

import 'dart:convert';
import 'dart:isolate';
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'JSON Parsing in Isolate',
      home: Scaffold(
        appBar: AppBar(
          title: Text('JSON Parsing in Isolate'),
        ),
        body: Center(
          child: FutureBuilder>(
            future: fetchAndParseJson(),
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.waiting) {
                return CircularProgressIndicator();
              } else if (snapshot.hasError) {
                return Text('Error: ${snapshot.error}');
              } else if (snapshot.hasData) {
                return ListView.builder(
                  itemCount: snapshot.data!.length,
                  itemBuilder: (context, index) {
                    return ListTile(
                      title: Text(snapshot.data![index]['name']),
                      subtitle: Text('ID: ${snapshot.data![index]['id']}'),
                    );
                  },
                );
              } else {
                return Text('No data');
              }
            },
          ),
        ),
      ),
    );
  }

  Future> fetchAndParseJson() async {
    final jsonString = await loadJsonFromAsset('assets/large_json.json');
    return parseJsonInIsolateHelper(jsonString);
  }

  Future loadJsonFromAsset(String path) async {
    return await DefaultAssetBundle.of(buildContext!).loadString(path);
  }
    
  BuildContext? buildContext; // Define buildContext as nullable

  Future> parseJsonInIsolateHelper(String jsonString) async {
    final receivePort = ReceivePort();
    await Isolate.spawn(
      parseJsonInIsolate,
      {
        'jsonString': jsonString,
        'sendPort': receivePort.sendPort,
      },
    );

    // Capture the current context within the helper function
    return await receivePort.first.then((result) {
      receivePort.close();
      return result as List;
    });
  }
  
  @override
  Widget build(BuildContext context) {
      buildContext = context; // Assign the context to the buildContext variable
      return MaterialApp(
      title: 'JSON Parsing in Isolate',
      theme: ThemeData(
          primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
      );
  }
}

class MyHomePage extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
            title: Text('JSON Parsing Example'),
            ),
            body: Center(
            child: FutureBuilder>(
                future: MyApp().fetchAndParseJson(), // Call fetchAndParseJson through MyApp instance
                builder: (context, snapshot) {
                if (snapshot.connectionState == ConnectionState.waiting) {
                    return CircularProgressIndicator();
                } else if (snapshot.hasError) {
                    return Text('Error: ${snapshot.error}');
                } else if (snapshot.hasData) {
                    return ListView.builder(
                        itemCount: snapshot.data!.length,
                        itemBuilder: (context, index) {
                        return ListTile(
                            title: Text(snapshot.data![index]['name']),
                            subtitle: Text('ID: ${snapshot.data![index]['id']}'),
                        );
                        },
                    );
                } else {
                    return Text('No data');
                }
                },
            ),
            ),
        );
    }
}

List parseJson(String jsonString) {
    return jsonDecode(jsonString) as List;
}

void parseJsonInIsolate(Map message) {
    String jsonString = message['jsonString'] as String;
    SendPort sendPort = message['sendPort'] as SendPort;

    final parsedJson = parseJson(jsonString);
    sendPort.send(parsedJson);
}

In this setup:

  • We load a JSON string from an asset file (large_json.json).
  • fetchAndParseJson calls parseJsonInIsolateHelper to handle isolate creation and communication.
  • The parseJsonInIsolateHelper function creates a ReceivePort to listen for the result from the isolate.
  • Isolate.spawn runs the parseJsonInIsolate function with the JSON string and SendPort as parameters.
  • The result is received via receivePort.first, and the UI is updated using a FutureBuilder.

Step 5: Create a Large JSON File

Create a large JSON file (e.g., large_json.json) and place it in your assets folder. Make sure to declare assets folder in pubspec.yaml

Complete Example:

Here’s the complete Flutter app demonstrating JSON parsing in a background isolate:


import 'dart:convert';
import 'dart:isolate';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'JSON Parsing in Isolate',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State {
  Future>? _data;

  @override
  void initState() {
    super.initState();
    _data = fetchAndParseJson();
  }

  Future> fetchAndParseJson() async {
    final jsonString = await rootBundle.loadString('assets/large_json.json');
    return parseJsonInIsolateHelper(jsonString);
  }

  Future> parseJsonInIsolateHelper(String jsonString) async {
    final receivePort = ReceivePort();
    await Isolate.spawn(
      parseJsonInIsolate,
      {
        'jsonString': jsonString,
        'sendPort': receivePort.sendPort,
      },
    );

    return await receivePort.first.then((result) {
      receivePort.close();
      return result as List;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('JSON Parsing Example'),
      ),
      body: Center(
        child: FutureBuilder>(
          future: _data,
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              return CircularProgressIndicator();
            } else if (snapshot.hasError) {
              return Text('Error: ${snapshot.error}');
            } else if (snapshot.hasData) {
              return ListView.builder(
                itemCount: snapshot.data!.length,
                itemBuilder: (context, index) {
                  return ListTile(
                    title: Text(snapshot.data![index]['name']),
                    subtitle: Text('ID: ${snapshot.data![index]['id']}'),
                  );
                },
              );
            } else {
              return Text('No data');
            }
          },
        ),
      ),
    );
  }
}

List parseJson(String jsonString) {
  return jsonDecode(jsonString) as List;
}

void parseJsonInIsolate(Map message) {
  String jsonString = message['jsonString'] as String;
  SendPort sendPort = message['sendPort'] as SendPort;

  final parsedJson = parseJson(jsonString);
  sendPort.send(parsedJson);
}

Make sure to create **assets/large_json.json** file on your project. And also declare the assets folder under pubspec.yaml file.

Performance Measurement:

You can measure the performance using the flutter performance tools. Run flutter app on profile mode, and check the CPU usage and jank frames count. It will give insight to your implement on how it works, you also use debugger mode for performance insights.

Conclusion

By performing JSON parsing in background isolates, you can significantly improve the performance and responsiveness of your Flutter applications, especially when dealing with large JSON files. This approach prevents blocking the main UI thread, ensuring a smooth and seamless user experience. Understanding and implementing background isolates is an essential skill for any Flutter developer aiming to build high-performance applications.