Execute long running logic asynchronously
You'll want to use an isolate to do your computation work and you can display the progress through the ports available on the isolate. The following code provides a makeIsolate
function that can be called to start the isolate and start executing the heavy task. It will send events on the provided port for every iteration. This could probably be reduced to every 100 or something iterations so you don't have so many updates.
Future<ReceivePort> makeIsolate() async {
ReceivePort receivePort = ReceivePort();
Isolate isolate = await Isolate.spawn(
isolateFunction,
receivePort.sendPort,
);
return receivePort;
}
void isolateFunction(SendPort sendPort) async {
//Do long running task
for (int i = 0; i < 100000; i++) {
doSth();
double progress = i/100000; // Calculating progress based on loop index
sendPort.send(progress);
}
sendPort.send(true); // Send true on completion to indicate work is done
}
Then in your widget you can call makeIsolate
and obtain a stream from its ReceivePort. Then use a StreamBuilder to update the progress indicator of your choice.
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
Stream<double> progress() async* {
ReceivePort receivePort = await makeIsolate();
await for(var event in receivePort) {
if(event is double) {
yield event;
}
if(event is bool) {
receivePort.close();
return;
}
}
}
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: progress(),
builder: (context, snapshot) {
if(snapshot.connectionState == ConnectionState.done) {
Navigator.of(context).pop(); // Pop from dialog on completion
// This could also be put in the stream generator.
}
if(snapshot.hasData) {
return CircularProgressIndicator(
value: snapshot.data,
);
}
return CircularProgressIndicator();
}
);
}
}
In your button onPressed:
onPressed: () async {
//Do NOT explicitly call your computation function or make the isolate here.
//Just show the dialog
await showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
return SizedBox(
child: MyStatefulWidget(),
height: 20,
width: 20,
);
},
);
},
This implementation is kinda rough, but it should be workable.
S-Man
Author Advanced PostgreSQL Querying ISBN 978-3752967340
Updated on December 27, 2022Comments
-
S-Man over 1 year
In Flutter I have a
StatefulWidget
with a Button. When hitting the Button, an extremely long running function is called.Currently the UI is blocked as long as the result hasn't arrived. Instead we want to show a progress indicator.
We tried following:
Future<MyResultType> runTheLongRunningFunction() async { doSth(); } ... onPressed: () { runTheLongRunningFunction(); // expected to be non-blocking raiseProgressIndicator(); // expected to be executed immidiately }
Well, we expected that the first function will be called in another thread and the second function is call immidiately after the first call. Instead the second function is called after the first one has finished.
To demonstrate the behaviour take this fully runnable Dart example which is a minimized version of our problem:
Future run() async { print('ASLEEP'); while (true) {} print('AWAKE'); } main() { run(); print('FOO'); }
We are expecting, that
FOO
would be shown directly before or afterASLEEP
. But it waits for the function to be finished. This can be shown, if you take asleep(Duration(seconds: 1));
instead of the infinitywhile
loop. The result would be:ASLEEP AWAKE FOO
We are not sure what we are missing. What do we need to do, to shift the first function into another thread to keep the UI non-blocked.
Additionally: We already saw the "threading" Flutter plugin and with using
new Thread(() async => runTheLongRunningFunction())
we already got the expected result. But we don't want to use this plugin because- It doesn't seem to be supported anymore
- We believe that our use case, to create a non-blocking UI is something so usual that there must be a simple Flutter/Dart native way to achieve this. We want to know what we are missing for using asnychonous calls.
Edit:
To make it even more complex:
My long running function runs a loop a few thousand times:
Future<MyResultType> runTheLongRunningFunction() async { for (int i = 0; i < 100000; i++) { doSth(); } }
Now I want to trigger some kind of ProgressBar (a simple
Text
widget is fine for the beginning). So, the function should run in the background, but every n-th time (let's say every1000
steps) the UI should be updated (either with more progress in a ProgressBar widget or a new line in aText
widget) with this value.So, the question is not only: How to get it asynchronous, but also, how to update the UI according to the state of this asynchronous function.
-
JayDev over 3 yearsDoes this answer your question? Dart - make long running synchronous function asynchronous
-
S-Man over 3 yearsInteresting link. I'll have a look. However, since the threading plugin seems not to use Isolates, it must be another way than using Isolates. Or am I wrong?
-
Ranvir Mohanlal over 3 yearsThis is interesting - could you try defining the first function as : void runTheLongRunningFunction() async { doSth(); }
-
S-Man over 3 years@RanvirMohanlal Nothing happens. Still the same output
-
Christopher Moore over 3 yearsWhat's preventing you from using isolates?
-
S-Man over 3 years@ChristopherMoore we are currently trying :) But we struggle. Maybe you could should show us an example for the problem?
-
Christopher Moore over 3 yearsYou should put that struggle in the question. It's difficult to understand what kind long-running function you're talking about. Is it just something that takes a long time? or something that is heavy/requires lots of processor time? And FYI, regarding the answer you currently have, state management has nothing to do with this problem.
-
S-Man over 3 years@ChristopherMoore The function runs a mathematical operation in a loop, several million times, possibly hundred of million times if the input is badly chosen. So, yes, I guess it needs much of processor time. However, since the loop variable and the loop length is know a-priori, I want to show a progress bar while the function is running.
-
Christopher Moore over 3 yearsAnd it seems you want to show the actual progress, not just an infinite one. Is that correct?
-
S-Man over 3 years@ChristopherMoore Yes, that would be the best case
-
Christopher Moore over 3 yearsCould you share what you have with isolates so far? It's not necessary, but it would save me some work.
-
S-Man over 3 years@ChristopherMoore Unfortunately not quickly. My collegue is working on it. I'll try it, but I guess that's not something useful at the moment...
-
Christopher Moore over 3 yearsDon't bother I have an answer coming along.
-
S-Man over 3 yearsWhat is "snapshot" and how does it get its "connectionState" or "hasData"?
-
Christopher Moore over 3 yearsIt is an AsyncSnapshot object. I just left out the type for my convenience.
-
S-Man over 3 yearsI tried your approach: notepad.pw/6jz18uh2 But some things are not clear to me: 1. Do I need a flag to show the StreamBuilder, maybe in onPressed(). Currently the CircularIndicator is display the whole time. 2. Where do I start the calculation? I believe I need to add my button (and the rest of the frontend) somewhere? Do I need to put it into the builder or separately? 3. In your builder you always return the CircularIndicator. Is that correct or are there some ELSE commands missing? Thanks for you help, I appreciate.
-
Christopher Moore over 3 years@S-Man It depends on what you want. This code starts the operation as soon as the widget is built. So you could have it in a dialog or something. It doesn't really matter, this approach is adaptable, but you would have to tell or preferably show me what your intent for this is. There is a part of the builder I left to show a successful completion message.
-
S-Man over 3 yearsDid you noticed the link I gave in the last comment? I sketches a small widget which only contains a button. Until now: If I hit the button, I run the function. Now I need to integrate your code into mine. I tried something (as shown in the link) but than the question above came up.
-
Christopher Moore over 3 years@S-Man How do you want the progress indicator to be shown though? A separate Navigator page? A separate page in a pageview? A dialog?
-
S-Man over 3 yearsA simple overlay or dialog would be the very best.
-
S-Man over 3 yearsAh... I see my problem :) Your StatefulWidget is not the widget with the button (meaning the app), but a separate widget, isn't it? I guess, that created the problems in my mind on how to handle it. Well, great, I'll try it! Thank you so far and so much for your guidance and patience!
-
S-Man over 3 yearsIt seems to work! Thank you so much for all your efforts!