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): true for Dart UI isolates, false for regular isolates
  • name (String): Unique name for the helper
  • isAutoDispose (bool): Whether to auto-dispose after inactivity
  • isIsolateSpawn (bool): Whether the isolate is currently spawned
  • autoDisposeInterval (Duration): Time before auto-dispose (default: 10 seconds)

Methods

  • Future<T> runIsolate(dynamic args, IsolateOperation operation): Run an operation in the isolate
  • Future<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 RootIsolateToken for 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

Libraries

pl_isolate