flutter-concurrency
Background JSON parsing and state management for jank-free Flutter UI rendering.
- Provides decision tree for choosing between manual serialization (
dart:convert) and code generation (json_serializable) based on model complexity - Supports three concurrency strategies: main-thread async/await for small payloads, short-lived
Isolate.run()for heavy one-off computations, and long-lived isolates withReceivePort/SendPortfor continuous two-way communication - Includes platform-aware fallback: uses
compute()for Flutter Web sincedart:isolatethreading is unsupported - Demonstrates integration with
FutureBuilderfor responsive UI state binding and enforces key constraints like no UI access in isolates and immutable message passing
Flutter Concurrency and Data Management
Goal
Implements advanced Flutter data handling, including background JSON serialization using Isolates, asynchronous state management, and platform-aware concurrency to ensure jank-free 60fps+ UI rendering. Assumes a standard Flutter environment (Dart 2.19+) with access to dart:convert, dart:isolate, and standard state management paradigms.
Decision Logic
Use the following decision tree to determine the correct serialization and concurrency approach before writing code:
- Serialization Strategy:
- Condition: Is the JSON model simple, flat, and rarely changed?
- Action: Use Manual Serialization (
dart:convert).
- Action: Use Manual Serialization (
- Condition: Is the JSON model complex, nested, or part of a large-scale application?
- Action: Use Code Generation (
json_serializableandbuild_runner).
- Action: Use Code Generation (
- Condition: Is the JSON model simple, flat, and rarely changed?
- Concurrency Strategy:
- Condition: Is the data payload small and parsing takes < 16ms?
- Action: Run on the Main UI Isolate using standard
async/await.
- Action: Run on the Main UI Isolate using standard
- Condition: Is the data payload large (e.g., > 1MB JSON) or computationally expensive?
- Action: Offload to a Background Isolate using
Isolate.run().
- Action: Offload to a Background Isolate using
- Condition: Does the background task require continuous, two-way communication over time?
- Action: Implement a Long-lived Isolate using
ReceivePortandSendPort.
- Action: Implement a Long-lived Isolate using
- Condition: Is the target platform Web?
- Action: Use
compute()as a fallback, as standarddart:isolatethreading is not supported on Flutter Web.
- Action: Use
- Condition: Is the data payload small and parsing takes < 16ms?
Instructions
1. Determine Environment and Payload Context
STOP AND ASK THE USER:
- "Are you targeting Flutter Web, Mobile, or Desktop?"
- "What is the expected size and complexity of the JSON payload?"
- "Do you prefer manual JSON serialization or code generation (
json_serializable)?"
2. Implement JSON Serialization Models
Based on the user's preference, implement the data models.
Option A: Manual Serialization
import 'dart:convert';
class User {
final String name;
final String email;
User(this.name, this.email);
User.fromJson(Map<String, dynamic> json)
: name = json['name'] as String,
email = json['email'] as String;
Map<String, dynamic> toJson() => {'name': name, 'email': email};
}
Option B: Code Generation (json_serializable)
Ensure json_annotation is in dependencies, and build_runner / json_serializable are in dev_dependencies.
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';
(explicitToJson: true)
class User {
final String name;
(name: 'email_address', defaultValue: 'unknown@example.com')
final String email;
User(this.name, this.email);
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
Validate-and-Fix: Instruct the user to run dart run build_runner build --delete-conflicting-outputs to generate the *.g.dart file.
3. Implement Background Parsing (Isolates)
To prevent UI jank, offload heavy JSON parsing to a background isolate.
Option A: Short-lived Isolate (Dart 2.19+)
Use Isolate.run() for one-off heavy computations.
import 'dart:convert';
import 'dart:isolate';
import 'package:flutter/services.dart';
Future<List<User>> fetchAndParseUsers() async {
// 1. Load data on the main isolate
final String jsonString = await rootBundle.loadString('assets/large_users.json');
// 2. Spawn an isolate, pass the computation, and await the result
final List<User> users = await Isolate.run<List<User>>(() {
// This runs on the background isolate
final List<dynamic> decoded = jsonDecode(jsonString) as List<dynamic>;
return decoded.cast<Map<String, dynamic>>().map(User.fromJson).toList();
});
return users;
}
Option B: Long-lived Isolate (Continuous Data Stream)
Use ReceivePort and SendPort for continuous communication.
import 'dart:isolate';
Future<void> setupLongLivedIsolate() async {
final ReceivePort mainReceivePort = ReceivePort();
await Isolate.spawn(_backgroundWorker, mainReceivePort.sendPort);
final SendPort backgroundSendPort = await mainReceivePort.first as SendPort;
// Send data to the background isolate
final ReceivePort responsePort = ReceivePort();
backgroundSendPort.send(['https://api.example.com/data', responsePort.sendPort]);
final result = await responsePort.first;
print('Received from background: $result');
}
static void _backgroundWorker(SendPort mainSendPort) async {
final ReceivePort workerReceivePort = ReceivePort();
mainSendPort.send(workerReceivePort.sendPort);
await for (final message in workerReceivePort) {
final String url = message[0] as String;
final SendPort replyPort = message[1] as SendPort;
// Perform heavy work here
final parsedData = await _heavyNetworkAndParse(url);
replyPort.send(parsedData);
}
}
4. Integrate with UI State Management
Bind the asynchronous isolate computation to the UI using FutureBuilder to ensure the main thread remains responsive.
import 'package:flutter/material.dart';
class UserListScreen extends StatefulWidget {
const UserListScreen({super.key});
State<UserListScreen> createState() => _UserListScreenState();
}
class _UserListScreenState extends State<UserListScreen> {
late Future<List<User>> _usersFuture;
void initState() {
super.initState();
_usersFuture = fetchAndParseUsers(); // Calls the Isolate.run method
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Users')),
body: FutureBuilder<List<User>>(
future: _usersFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text('No users found.'));
}
final users = snapshot.data!;
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(users[index].name),
subtitle: Text(users[index].email),
);
},
);
},
),
);
}
}
Constraints
- No UI in Isolates: Never attempt to access
dart:ui,rootBundle, or manipulate Flutter Widgets inside a spawned isolate. Isolates do not share memory with the main thread. - Web Platform Limitations:
dart:isolateis not supported on Flutter Web. If targeting Web, you MUST use thecompute()function frompackage:flutter/foundation.dartinstead ofIsolate.run(), ascompute()safely falls back to the main thread on web platforms. - Immutable Messages: When passing data between isolates via
SendPort, prefer passing immutable objects (like Strings or unmodifiable byte data) to avoid deep-copy performance overhead. - State Immutability: Always treat
Widgetproperties as immutable. UseStatefulWidgetandsetState(or a state management package) to trigger rebuilds when asynchronous data resolves. - Reflection: Do not use
dart:mirrorsfor JSON serialization. Flutter disables runtime reflection to enable aggressive tree-shaking and AOT compilation. Always use manual parsing or code generation.