Isolate for repetitive tasks in the?Flutter
Suppose you have a list of doubles like the array given below.
[9999999999,9999999989,9999999997]
And you want to calculate the sum of all the numbers from 1 up to the given number. To do this you are using for loop instead of a formula.
If you call the method on the UI thread it will freeze the UI until it finishes computation.
double heavyComputation(double value)
double sum = 0;
for (double i = 0; i < value; i++) {
sum = sum + i;
}
return sum;
}{
We all have heard that dart is single-threaded, you can’t create a thread using dart. Now your question would be then, how we can unblock our UI from the heavy computation?
We have isolates in the dart to separate heavy computation from the UI thread.
Isolates are similar to threads but they don’t share memory. Therefore, we have to use ports when we need to make two isolates communicate with each other.
There are multiple ways to create isolates in the Flutter, 2 most commonly used methods are?:
compute is useful for doing a single task once. If you start getting values in the form of streams then, compute method will make your application crash because it is creating a new isolate every time you get a new value for computation.
To handle this problem we need to use Isolate.spawn. We will use spawn isolate once and then reuse that isolate, to do all the heavy computation work.
Before we spawn our isolate we need to understand. How we can communicate between two isolates?
领英推荐
To make communication happen between two isolates we need to use ports. I have mentioned ports before.
So, What are ports? Which type of ports do we have to use to make communication happen?
In layman's language, ports are a combination of a postbox and postal address.
Child isolate will send everything on the parent’s send port. And the parent will listen on its receiver port. Similarly, the parent will send everything on the child send port and vice versa.
It’s good to send, send port as an initial message to the spawned isolate. We are done with the theoretical part for now.
DTO
abstract class IsolateDto<T> extends Object{
Future<T> compute();
}
class ComputeMyData extends IsolateDto<double> {
ComputeMyData({
required this.value,
});
final double value;
@override
Future<double> heavyComputation() async {
double sum = 0;
for (double i = 0; i < value; i++) {
sum = sum + i;
}
return sum;
}
}
After reading the code above your question would be why do we have to extend our abstract class with the Object? Isolates only accept instances of type Object or of primitive type.
Isolate Executor
import 'dart:async';
import 'dart:isolate';
import 'compute_my_data.dart';
class IsolateExecutor<T> {
Isolate? _isolate;
SendPort? _sendPortChild;
final _isolateReady = Completer<void>();
final StreamController<T> _streamController = StreamController();
IsolateExecutor() {
_createIsolate();
}
void sendDataToChild(IsolateDto<T> data) {
_sendPortChild?.send(data);
return;
}
Stream<T> receiveDataStream() {
return _streamController.stream.asBroadcastStream();
}
Future<void> _createIsolate() async {
ReceivePort receivePortParent = ReceivePort();
receivePortParent.listen(_handleMessageFromChild);
_isolate ??= await Isolate.spawn(_isolateEntry, receivePortParent.sendPort);
return;
}
Future<void> get isolateReady => _isolateReady.future;
void handleComputation(IsolateDto<T> messageFromParent,SendPort? sendPortParent) async{
T value = await messageFromParent.heavyComputation();
sendPortParent?.send(value);
return;
}
void _isolateEntry(SendPort initialMessageFromParent) {
SendPort? sendPortParent;
ReceivePort receivePortChild = ReceivePort();
receivePortChild.listen((messageFromParent) {
handleComputation(messageFromParent, sendPortParent);
});
sendPortParent = initialMessageFromParent;
sendPortParent.send(receivePortChild.sendPort);
return;
}
void _handleMessageFromChild(messageFromChild) {
if (messageFromChild is SendPort) {
_sendPortChild = messageFromChild;
_isolateReady.complete();
} else if (messageFromChild is T) {
_streamController.add(messageFromChild);
}
return;
}
void dispose() {
_isolate?.kill();
}
}
After looking at the code above you might be thinking why there is a need of creating an isolate executor of a generic type. And, why we are keeping parent and child ports in a single class?
Generic isolate allowed us to handle the repetitive task of type T. It helped us to reduce some code. In _isolateEntry method we don’t have to do a type check because we have already restricted the type of data we are going to send to the child using the sendDataToChild method. By encapsulating ports in the same class we have also reduced the type of data we are going to receive in child or parent isolate.
UI
import 'package:flutter/material.dart';
import 'compute_my_data.dart';
import 'isolate_executor.dart';
import 'dart:math' as math;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({
Key? key,
}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final ValueNotifier<double> _counter = ValueNotifier<double>(0);
IsolateExecutor<double> isolateExecutor = IsolateExecutor<double>();
@override
void initState() {
super.initState();
isolateReady();
}
Future<void> isolateReady() async {
await isolateExecutor.isolateReady;
isolateExecutor.receiveDataStream().listen((event) {
_counter.value = event;
});
return;
}
@override
void dispose() {
isolateExecutor.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: ValueListenableBuilder(
valueListenable: _counter,
builder: (context, value, child) {
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
return Text(
'${_counter.value}',
style: Theme.of(context).textTheme.headlineMedium,
);
},
itemCount: 100,
);
}),
),
TextButton(
onPressed: () {
isolateExecutor.sendDataToChild(ComputeMyData(value: math.Random().nextDouble()*1000000000));
},
child: const Text('Run Heavy Computation'),
)
],
),
);
}
}