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). fetchAndParseJsoncallsparseJsonInIsolateHelperto handle isolate creation and communication.- The
parseJsonInIsolateHelperfunction creates aReceivePortto listen for the result from the isolate. Isolate.spawnruns theparseJsonInIsolatefunction with the JSON string andSendPortas parameters.- The result is received via
receivePort.first, and the UI is updated using aFutureBuilder.
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.