Skip to main content

Command Palette

Search for a command to run...

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

Updated
4 min read
Why you should use Flutter's compute() method for intensive tasks
D

I am a software developer specialized in mobile apps. About a decade ago I started my career as a web developer, but I soon moved into Android native development; however for the last few years I've been building hybrid apps with Flutter. I consider myself a passionate programmer, I enjoy writing clean and scalable code. In addition to developing apps, I also have knowledge of backend services development, web development, installation and maintenance of servers, marketing applied to the growth of web applications... among other things. I also like to create video games in my free time and write about topics that interest me in the technology world: the last tech trends, experiments that I do, and topics regarding user privacy. You can find more about me, my articles and my projects on my website: davidserrano.io

📽 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!

More from this blog

David Serrano

70 posts

I'm a mobile developer and an entrepreneur. In this blog you will find articles, tutorials and tricks to design, build and grow your mobile apps.