pl_isolate
https://github.com/user-attachments/assets/31985cfa-f3b4-440e-8459-fdc4f8dc7fe1
A powerful Flutter plugin that simplifies isolate communication and management. Run heavy computations in separate isolates without blocking the UI thread.
Features
- π Easy Isolate Management: Simple API to create and manage isolates
- π Automatic Disposal: Auto-dispose isolates after inactivity
- π― Type-Safe Operations: Define operations with clear interfaces
- π Thread-Safe: Built-in synchronization for concurrent operations
- π Multiple Isolates: Each operation can have its own isolate helper
- π¨ UI Isolate Support: Support for both Dart isolates and UI isolates
- β‘ Performance: Run CPU-intensive tasks without blocking the main thread
- π¦ Transferable Data for Large Payloads: Efficient support for sending/receiving large data between isolates using Dart's transferable objects
Installation
Add this to your package's pubspec.yaml file:
dependencies:
pl_isolate: ^1.0.0
Then run:
flutter pub get
Quick Start
1. Define Your Operation
Create an operation class that implements IsolateOperation:
import 'package:pl_isolate/pl_isolate.dart';
class MyCalculationOperation implements IsolateOperation {
@override
String get tag => 'calculation';
@override
Future<dynamic> run(dynamic args) async {
// Your heavy computation here
if (args is int) {
int result = 0;
for (var i = 0; i < args; i++) {
result += i;
}
return result;
}
throw Exception('Invalid arguments');
}
}
2. Create Your Isolate Helper
Extend IsolateHelper to create your helper:
class MyIsolateHelper extends IsolateHelper<int> {
@override
bool get isDartIsolate => false; // Use false for regular isolates, true for UI isolates
@override
String get name => 'MyIsolateHelper'; // Unique name for this helper
@override
bool get isAutoDispose => true; // Auto-dispose after inactivity
@override
Stream get messages => throw UnimplementedError(); // Required but not used
}
3. Use the Helper
class MyWidget extends StatefulWidget {
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
final MyIsolateHelper _helper = MyIsolateHelper();
Future<void> _runCalculation() async {
try {
final result = await _helper.runIsolate(
1000000, // Arguments
MyCalculationOperation(), // Your operation
);
print('Result: $result');
} catch (e) {
print('Error: $e');
}
}
@override
void dispose() {
_helper.dispose(); // Don't forget to dispose
super.dispose();
}
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: _runCalculation,
child: Text('Run Calculation'),
);
}
}
Managing Multiple Tasks with IsolateManager
When you need to queue different helpers and execute them with concurrency limits, use IsolateManager.
1. Initialize the Manager
void main() {
// Allow 2 concurrent tasks and up to 10 queued jobs.
IsolateManager.init(2, 10);
runApp(const MyApp());
}
2. Add Tasks to the Queue
void scheduleTasks() {
final manager = IsolateManager.instance;
manager.addIsolateHelper(
MyIsolateHelper(),
MyCalculationOperation(),
1000000,
);
manager.addIsolateHelper(
OtherHelper(),
OtherOperation(),
{'duration': 2000},
);
}
Each call enqueues the helper/operation pair along with its arguments. You can enqueue as many as maxSizeOfQueue.
3. Listen for Results (Optional)
@override
void initState() {
super.initState();
IsolateManager.instance.listenIsolateResult((IsolateResult result) {
if (result.errorMessage != null) {
debugPrint('Task ${result.name} failed: ${result.errorMessage}');
} else {
debugPrint('Task ${result.name} completed: ${result.result}');
}
});
}
4. Run the Batch
Future<void> runQueue() async {
await IsolateManager.instance.runAllInBatches();
}
The manager executes the queue in batches, honoring the maxConcurrentTasks limit. Results are pushed through the listener as each task finishes.
5. Clean Up
Call disposeAll() when you are done to release helpers left in the queue or running list.
@override
void dispose() {
IsolateManager.instance.disposeAll();
super.dispose();
}
Complete Example
Here's a complete example showing multiple operations:
import 'package:flutter/material.dart';
import 'package:pl_isolate/pl_isolate.dart';
// Define operations
class CountOperation implements IsolateOperation {
@override
String get tag => 'count';
@override
Future<dynamic> run(dynamic args) async {
if (args is int) {
int count = 0;
for (var i = 0; i < args; i++) {
count++;
}
return count;
}
return 0;
}
}
class SumOperation implements IsolateOperation {
@override
String get tag => 'sum';
@override
Future<dynamic> run(dynamic args) async {
if (args is List) {
return args.fold<int>(0, (sum, item) => sum + (item as int));
}
return 0;
}
}
// Create helpers
class CountHelper extends IsolateHelper<int> {
@override
bool get isDartIsolate => false;
@override
String get name => 'CountHelper';
@override
bool get isAutoDispose => true;
@override
Stream get messages => throw UnimplementedError();
}
class SumHelper extends IsolateHelper<int> {
@override
bool get isDartIsolate => false;
@override
String get name => 'SumHelper';
@override
bool get isAutoDispose => true;
@override
Stream get messages => throw UnimplementedError();
}
// Use in your app
class MyApp extends StatefulWidget {
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final CountHelper _countHelper = CountHelper();
final SumHelper _sumHelper = SumHelper();
String _countResult = 'No result';
String _sumResult = 'No result';
bool _isLoading = false;
Future<void> _runCount() async {
setState(() => _isLoading = true);
try {
final result = await _countHelper.runIsolate(
10000000,
CountOperation(),
);
setState(() {
_countResult = 'Count: $result';
_isLoading = false;
});
} catch (e) {
setState(() {
_countResult = 'Error: $e';
_isLoading = false;
});
}
}
Future<void> _runSum() async {
setState(() => _isLoading = true);
try {
final result = await _sumHelper.runIsolate(
List.generate(1000000, (i) => i),
SumOperation(),
);
setState(() {
_sumResult = 'Sum: $result';
_isLoading = false;
});
} catch (e) {
setState(() {
_sumResult = 'Error: $e';
_isLoading = false;
});
}
}
@override
void dispose() {
_countHelper.dispose();
_sumHelper.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Isolate Helper Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_isLoading) CircularProgressIndicator(),
Text(_countResult),
ElevatedButton(
onPressed: _isLoading ? null : _runCount,
child: Text('Run Count'),
),
SizedBox(height: 20),
Text(_sumResult),
ElevatedButton(
onPressed: _isLoading ? null : _runSum,
child: Text('Run Sum'),
),
],
),
),
),
);
}
}
API Reference
IsolateHelper
Abstract class for managing isolates.
Properties
isDartIsolate(bool):truefor Dart UI isolates,falsefor regular isolatesname(String): Unique name for the helperisAutoDispose(bool): Whether to auto-dispose after inactivityisIsolateSpawn(bool): Whether the isolate is currently spawnedautoDisposeInterval(Duration): Time before auto-dispose (default: 10 seconds)
Methods
Future<T> runIsolate(dynamic args, IsolateOperation operation): Run an operation in the isolateFuture<void> dispose(): Manually dispose the isolate
IsolateOperation
Abstract class for defining operations.
Properties
tag(String): Unique tag for the operation
Methods
Future<dynamic> run(dynamic args): Execute the operation
Dart Isolate vs UI Isolate
Regular Isolate (isDartIsolate: false)
- Requires
RootIsolateTokenfor platform channel access - Use for computations that don't need UI access
- Better performance for CPU-intensive tasks
class MyHelper extends IsolateHelper<dynamic> {
@override
bool get isDartIsolate => false;
// ... other properties
}
UI Isolate (isDartIsolate: true)
- Can access UI-related APIs
- Use when you need platform channels or UI access
- Requires Flutter bindings to be initialized
class MyHelper extends IsolateHelper<dynamic> {
@override
bool get isDartIsolate => true;
// ... other properties
}
Best Practices
1. One Helper per Operation Type
Each helper should manage one type of operation:
// Good: Separate helpers for different operations
class ImageProcessingHelper extends IsolateHelper<Uint8List> { ... }
class DataAnalysisHelper extends IsolateHelper<AnalysisResult> { ... }
// Avoid: One helper for multiple unrelated operations
class AllOperationsHelper extends IsolateHelper<dynamic> { ... }
2. Always Dispose Helpers
Make sure to dispose helpers when they're no longer needed:
@override
void dispose() {
_helper.dispose();
super.dispose();
}
3. Handle Errors Properly
Always wrap isolate operations in try-catch:
try {
final result = await _helper.runIsolate(args, operation);
// Handle success
} catch (e) {
// Handle error
}
4. Use Auto-Dispose for Temporary Operations
Enable auto-dispose for operations that are run infrequently:
@override
bool get isAutoDispose => true;
5. Pass Serializable Data
Only pass data that can be serialized between isolates:
// Good: Primitive types, lists, maps
final result = await helper.runIsolate(42, operation);
final result = await helper.runIsolate([1, 2, 3], operation);
final result = await helper.runIsolate({'key': 'value'}, operation);
// Avoid: Complex objects, closures, functions
// These cannot be serialized between isolates
Advanced Usage
Custom Auto-Dispose Interval
Override autoDisposeInterval to customize the disposal time:
class MyHelper extends IsolateHelper<dynamic> {
@override
Duration get autoDisposeInterval => const Duration(seconds: 30);
// ... other properties
}
Running Multiple Operations Concurrently
Each helper can run operations independently:
// Run multiple operations at the same time
final result1 = _helper1.runIsolate(args1, operation1);
final result2 = _helper2.runIsolate(args2, operation2);
final result3 = _helper3.runIsolate(args3, operation3);
// Wait for all to complete
final results = await Future.wait([result1, result2, result3]);
Checking Isolate Status
Monitor isolate state:
if (_helper.isIsolateSpawn) {
print('Isolate is active');
} else {
print('Isolate is inactive');
}
Troubleshooting
Error: "Root isolate token is not set"
Solution: If using isDartIsolate: false, make sure you're running in a Flutter app context. For UI isolates, use isDartIsolate: true.
Isolate Not Disposing
Solution: Check if isAutoDispose is set to true and ensure no active operations are running.
Serialization Errors
Solution: Ensure all data passed to runIsolate is serializable. Avoid passing complex objects, closures, or functions.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Support
For issues and feature requests, please use the GitHub Issues page.
Made with β€οΈ by the NexPlugs team