Execute long running logic asynchronously

164

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.

Share:
164
S-Man
Author by

S-Man

Author Advanced PostgreSQL Querying ISBN 978-3752967340

Updated on December 27, 2022

Comments

  • S-Man
    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 after ASLEEP. But it waits for the function to be finished. This can be shown, if you take a sleep(Duration(seconds: 1)); instead of the infinity while 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

    1. It doesn't seem to be supported anymore
    2. 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 every 1000 steps) the UI should be updated (either with more progress in a ProgressBar widget or a new line in a Text 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
      JayDev over 3 years
    • S-Man
      S-Man over 3 years
      Interesting 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
      Ranvir Mohanlal over 3 years
      This is interesting - could you try defining the first function as : void runTheLongRunningFunction() async { doSth(); }
    • S-Man
      S-Man over 3 years
      @RanvirMohanlal Nothing happens. Still the same output
    • Christopher Moore
      Christopher Moore over 3 years
      What's preventing you from using isolates?
    • S-Man
      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
      Christopher Moore over 3 years
      You 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
      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
      Christopher Moore over 3 years
      And it seems you want to show the actual progress, not just an infinite one. Is that correct?
    • S-Man
      S-Man over 3 years
      @ChristopherMoore Yes, that would be the best case
    • Christopher Moore
      Christopher Moore over 3 years
      Could you share what you have with isolates so far? It's not necessary, but it would save me some work.
    • S-Man
      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
      Christopher Moore over 3 years
      Don't bother I have an answer coming along.
  • S-Man
    S-Man over 3 years
    What is "snapshot" and how does it get its "connectionState" or "hasData"?
  • Christopher Moore
    Christopher Moore over 3 years
    It is an AsyncSnapshot object. I just left out the type for my convenience.
  • S-Man
    S-Man over 3 years
    I 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
    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
    S-Man over 3 years
    Did 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
    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
    S-Man over 3 years
    A simple overlay or dialog would be the very best.
  • S-Man
    S-Man over 3 years
    Ah... 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
    S-Man over 3 years
    It seems to work! Thank you so much for all your efforts!