Why you should use Flutter's compute() method for intensive tasks

Why you should use Flutter's compute() method for intensive tasks

ยท

4 min read

๐Ÿ“ฝ Video version available on YouTube and Odysee

Let's say you have a method that does the following:

  • Downloads an image from the internet

  • Create several copies of the image but resize them

  • Shows the original image and the copies on the screen

Specifically, it would be a method like this:

Future<List<File>> _createImages(String url, int count) async {
  // Downloads the image
  final response = await http.get(Uri.parse(url));
  final List<File> files = [];

  // Save the image in the cache directory
  Directory tempDir = await getTemporaryDirectory();
  File file = await File('${tempDir.path}/image.jpg')
      .writeAsBytes(response.bodyBytes);
  img.Image? image = img.decodeImage(file.readAsBytesSync());

  // Generate copies with 50% less size than the previous
  for (var i = 0; i < count; i++) {
    if (image == null) continue;

    img.Image resizedImage = img.copyResize(image,
        width: (image.width * 0.5).round(),
        height: (image.height * 0.5).round());
    List<int> resizedImageData = img.encodeJpg(resizedImage);

    file = await File('${tempDir.path}/image$i.jpg')
        .writeAsBytes(resizedImageData);
    files.add(file);

    image = img.decodeImage(file.readAsBytesSync());
  }

  // Return the generated files paths
  return files;
}

For this to work, this method is called within a FutureBuilder, and once it gets the result it displays it as follows:

FutureBuilder<List<File>>(
  future: _createImages(_url, 4),
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      // Show all the images within a list
    } else {
      // Show a loading while processing the images
      return const Center(
        child: CircularProgressIndicator(),
      );
    }
  },
)

If you attempt to execute this code, you'll notice the user interface becomes sluggish, possibly even unresponsive. Additionally, the frame rate experiences a significant decrease:

The reason for this is that even though Flutter provides us with a mechanism to execute asynchronous code via Futures, both synchronous and asynchronous code is actually executed on the same thread. Taking into account that working with images is expensive, part of the CPU load in carrying out these processes makes the fluidity of the application suffer, resulting in the image freezing for a few seconds until the image processing is over.

How to move intensive computation to a Flutter background thread with compute()

The compute() method is intended to make it easier to work with background threads in Flutter. Specifically in the context of Flutter, these threads are called Isolates, and although we can call and use them in different ways, in today's example we will see how the Flutter API handles all these tasks when working with compute().

First, we are going to modify the previous method that downloads the image so that it can work correctly in a background thread. Specifically, we will create a class that represents its parameters, we will also have to pass the temporary directory as a parameter, since background threads have problems when executing platform code:

// Create a class that bundles the parameters
class CreateImagesParams {
  final String url;
  final int count;
  final Directory directory;

  CreateImagesParams({
    required this.url,
    required this.count,
    required this.directory,
  });
}

// This MUST be a top-level function now
Future<List<File>> createImages(CreateImagesParams params) async {
  final response = await http.get(Uri.parse(params.url));
  final List<File> files = [];

  File file = await File('${params.directory.path}/image.jpg')
      .writeAsBytes(response.bodyBytes);
  img.Image? image = img.decodeImage(file.readAsBytesSync());

  for (var i = 0; i < params.count; i++) {
    if (image == null) continue;

    img.Image resizedImage = img.copyResize(image,
        width: (image.width * 0.5).round(),
        height: (image.height * 0.5).round());
    List<int> resizedImageData = img.encodeJpg(resizedImage);

    file = await File('${params.directory.path}/image$i.jpg')
        .writeAsBytes(resizedImageData);
    files.add(file);

    image = img.decodeImage(file.readAsBytesSync());
  }

  return files;
}

Isolates have a completely separate memory space from other threads, so they cannot access instances created from another thread. For that reason, we will have to create a top-level function (a static function would also work).

We will also make some modifications to the rest of the widget:

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: const Text('WITH Compute')),
    body: FutureBuilder<List<File>>(
      future: _createImages(),
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          // Show all the images within a list
        } else {
          return const Center(
            child: CircularProgressIndicator(),
          );
        }
      },
    ),
  );
}

Future<List<File>> _createImages() async {
  // We call compute() with a reference to the top-level
  // function, as well as an instance of its parameters
  return compute(
    createImages,
    CreateImagesParams(
      url: _url,
      count: 4,
      directory: await getTemporaryDirectory(),
    ),
  );
}

If you try to execute this code now, despite the processing time for all the images being approximately the same, you will notice that the UI does not freeze. The loading widget we have implemented continues to spin throughout. This is because we have shifted the resource-intensive task to a background thread, allowing the main thread to maintain a smooth interface movement.

Conclusion

Flutter incorporates different ways of working with threads. As I have already commented in the article, these are called Isolates and they can be called directly, controlling each of the steps of their life cycle. However for simple tasks that only run for a few seconds and then complete it is better to use the compute() method, since it abstracts us from the relevant management of those threads.

You can find the complete project shown in this article here.

I hope you have found this article useful, see you at the next one!

Did you find this article valuable?

Support David Serrano by becoming a sponsor. Any amount is appreciated!