5

I'm working on an app that captures and processes an image. A simplified version of the code is:

build() {
   return FloatingActionButton(
        onPressed: processImage,
        child: Icon(
          Icons.camera_alt,
          color: color,
        ),
      ); 
}

processImage(Camera image) async {
   await image.process();   
}

And in another class:

Future<image> process() {
  return Future(() {
    for (int x = 0; x < width; x++) {
      for (int y = 0; y < height; y++) {
        //process image
      }
    }
  });
}

But when process() is running, the UI freezes.

Why is this happening? Isn't that function passed to Future constructor running in the background?

IAmJulianAcosta
  • 1,082
  • 2
  • 14
  • 30
  • 1
    I'm not 100% sure, but I do know dart uses an event loop and everything will run on the main thread (just perhaps at some point in the future, in the case of asynchronous code). As such, that code will eventually be executed on the main thread, causing a block. Consider using Dart's isolates; that should fix the issue. Flutter has a built-in convenience method to do this for you, but I can't remember what it is called off the top of my head. – Gregory Conrad Oct 25 '20 at 03:21
  • 2
    I believe I found it: https://api.flutter.dev/flutter/foundation/compute.html – Gregory Conrad Oct 25 '20 at 03:27
  • 1
    @GregoryConrad Thank you, that worked for me, is not a very elegant solution, but it works, if you want to make it an answer, I'll be happy to make it as the right answer – IAmJulianAcosta Oct 25 '20 at 04:25
  • 1
    what is not "elegant" in `compute()` function? – pskink Oct 25 '20 at 04:31
  • 1
    @pskink My function receives 6 parameters, and you cannot call a class method: `"The callback argument must be a top-level function, not a closure or an instance or static method of a class"`, and you can only pass one parameter, so I had to convert all my params to a `Map`, create a top-level function that receives the map, and destructure to original variables. – IAmJulianAcosta Oct 25 '20 at 04:35
  • this is how it works: the only solution for a background processing is `compute()` function or at lower level [Isolate](https://api.flutter.dev/flutter/dart-isolate/Isolate-class.html) stuff – pskink Oct 25 '20 at 04:39
  • Yes, I now know that is how it works, compute receives the top level function handler and one extra parameter, so I had to make my code work with these constraints, as I had a class method with several parameters, so I had to refactor it to a top level function with only one Map as parameter – IAmJulianAcosta Oct 25 '20 at 04:44
  • You can make your method take an instance of the class it is currently in as an argument. I can detail this more in my answer when I get around to creating it. This will make it cleaner than using a map. – Gregory Conrad Oct 25 '20 at 04:50
  • Oh I didn't know it is possible to pass objects, I read [here](https://api.flutter.dev/flutter/dart-isolate/SendPort/send.html) that only primitives can be passed, found that link in `compute()` documentation: > The content of message can be: primitive values (null, num, bool, double, String), instances of SendPort, and lists and maps whose elements are any of these. List and maps are also allowed to be cyclic. – IAmJulianAcosta Oct 25 '20 at 05:09
  • Whoops! I neglected to think about what types you can pass; you are correct. – Gregory Conrad Oct 25 '20 at 16:07

2 Answers2

4

As Dart uses an event loop, all code (synchronous and asynchronous) will simply be run on the same isolate (think thread in other languages as an analogy), just at different points in time. As such, when your process method is dequeued and executed, it will block the thread and cause frames to be dropped due to a longer execution time, despite being asynchronous. The optimal solution to the problem is to spawn another isolate in a new thread, and carry out the computation there. Flutter provides a convenience method for this exact use case, called compute. It takes a top-level function (not in a class, nor anonymous) that can have a primitive type parameter (including Map and List) as an argument and will return at some point in the future. For more information on compute, see its documentation linked above.

If you have multiple parameters that you need to pass to compute, a common pattern (outside of just this use case) is making a method that serializes a class' fields to a Map<String, dynamic>, and a factory constructor that creates an object from a Map<String, dynamic>. This process would be easier with reflection, but Flutter disables it due to performance reasons.

For a full example on compute from the Flutter documentation, see here: https://flutter.dev/docs/cookbook/networking/background-parsing

Gregory Conrad
  • 1,324
  • 11
  • 19
0

You can insert the gap into the event loop.
Simple way:

Future<image> process2() {
  return Future(() async {
    for (var x = 0; x < width; x++) {
      for (var y = 0; y < height; y++) {        
        // process       
      }

      if (x % 100 == 0) {
        await Future.delayed(Duration(seconds: 100));
      }
    }
  });
}

mezoni
  • 10,684
  • 4
  • 32
  • 54
  • Wrong answer, this will not help. UI still freezes. – Timo Bähr Jan 01 '22 at 16:39
  • I found this article talking about using event loop to handle heavy tasks. I don't know if the same method can be used for this case (processing image). [https://hackernoon.com/executing-heavy-tasks-without-blocking-the-main-thread-on-flutter-6mx31lh](https://hackernoon.com/executing-heavy-tasks-without-blocking-the-main-thread-on-flutter-6mx31lh) – jdevp2 Oct 04 '22 at 22:21