Flutter async : Beginner friendly guide for heavy lifting operations

Kartik Sharma
9 min readJul 8, 2018

In this article, I will try to explain how to maintain fluttery smooth UI and not dropping frames while doing complex CPU heavy operations.

Flutter uses Dart as language of choice. And Dart is a single threaded language. What this means is all the code is run synchronously i.e. line by line, which in turn can means that some complex time consuming code (for example network calls, image processing etc) can make your application freeze if not done correctly.

Example App

For this article, we have a super easy example of an application that prints the 10000th prime number. For some extra fun, the algorithm used to find prime number is a non-optimized slow algorithm.

Synchronous Code Example

Let’s start with naive code (starter code above). Here is gist of the naive code.

When the button is clicked, we call the _getPrime function, which calculates the prime number and we pass the result to a Text widget.

For small prime numbers, we don’t see a frame drop and it looks as if code is perfect. Try increasing the number from 1 to 10000.

UI response for n = 1 on the left and n = 10000 on the right.

The complete animation for progress bar and button splash and the gets stuck when n gets large.

So why is animation hanging up?

Flutter updates the animation in the build method of a widget which is usually called every frame. But, in our case, when the _getPrime started getting executed, flutter didn’t got a chance to call the build function since it was busy finding the prime numbers. Until, flutter is done calculating the prime number, build will not be called and all the UI would freeze up. This is consequence of synchronous programming on a single threaded language. The remaining code will not execute until previous code is done executing.

So, how do I fix it?

So one thing is clear, we want the flutter to execute the build method and update animation. We don’t want our code to block animation code for flutter widgets.

Imagine the scenario of a restaurant which works like this.

  1. Customer comes in, stands in a queue.
  2. First person in queue places an order, but instead of allowing next person to place the order, the first person waits till his food is prepared and he gets it.
  3. Then second and each consecutive person does the same. They place the order, and wait till they get the order, blocking the queue.

This process is kinda slow. Now, image a second scenario.

  1. Customer comes in, stands in a queue.
  2. First person in queue places an order, and moves aside, allowing next person to place the order.
  3. When the order is prepared, the person comes back to counter and collect it.

In this process, if there were multiple chef’s working in kitchen, multiple orders could be prepared at the same time, all of this, without blocking the queue.

First scenario is like synchronous programming, while the second one is asynchronous. We desire to achieve the same effect in flutter. Think of calls to build function as a queue. We do not want to block the queue. We want to simply tell flutter to calculate prime number, and when in the future it is done finding the number, it can give back us our prime number.

To do so, dart implements an event loop similar to that in js. I wont go into detail what an event loop is. To get an idea about event loop refer to this excellent video.

Introduction to Futures

Future<T> data types as return value represent that they will return a value of type T in the future, not immediately. For e.g. Future<int>will return an int.

Suppose, we have two functions, one prints values from 1 to 5 and other from 6 to 10. Each function waits for one second before printing the next value.

Future printSixToTen() async {
for(int i = 6; i <= 10; ++i) {
await new Future.delayed(const Duration(seconds: 1), () {
print(i);
});
}
}

Future printOneToFive() async {
for(int i = 1; i <= 5; ++i) {
await new Future.delayed(const Duration(seconds: 1), () {
print(i);
});
}
}

Here you see 2 new keywords async and await. What async does is tell flutter that the function is asynchronous, and it does not depend on other code, so it can run in parallel to other async functions.

What await tells flutter is to wait at that line of code, until the function has returned a value, as code after await may be dependable on value returned by the function.

Lets see the output when we change the code to like this momentarily.

void _getPrime() async {
var oneToFive = printOneToFive();
var sixToTen = printSixToTen();

setState((){
prime=10;
});
}

Notice, we are not awaiting for any of the function. So, flutter will execute like this.

  1. Start executing printOntToFive()
  2. Start executing printSixToTen() in parallel.
  3. At this point, both oneToFive and sixToTen are executing, since both are synchronous.
  4. Update state and then exit the method.

In console, you get output like

I/flutter ( 1673): 1
I/flutter ( 1673): 6
I/flutter ( 1673): 2
I/flutter ( 1673): 7
I/flutter ( 1673): 3
I/flutter ( 1673): 8
I/flutter ( 1673): 4
I/flutter ( 1673): 9
I/flutter ( 1673): 5
I/flutter ( 1673): 10

Notice, how both the function were printing the value in parallel. If, we were to update the function as follows, then first all number would print from one to ten sequentially, and finally the state would update when all numbers are done printing.

void _getPrime() async {
var oneToFive = await printOneToFive();
var sixToTen = await printSixToTen();

setState((){
prime=10;
});
}

Let’s update our prime number code and make it async.

Future<int> getnthPrime(int n) async{
int currentPrimeCount = 0;
int candidate = 1;
while(currentPrimeCount < n) {
++candidate;
if (isPrime(candidate)) {
++currentPrimeCount;
}
}
return candidate;
}


void _getPrime() async {
int ans = await getnthPrime(10000);
setState((){
prime=ans;
});
}

If, we run the code now, we find the UI still gets frozen. Why you ask?

async is concurrency on the same thread. — Günter Zöchbauer

We are still on the same UI thread which gets blocked, when we do CPU heavy task. So, how do we solve it? The answer is Isolates.

Flutter Isolates and Compute Function

An isolate is a new thread with its own event queue that can run in parallel with code in the main isolate (if there are enough cores to actually run code in parallel on your system, but that’s usually the case nowadays) — Günter Zöchbauer

Isolates in flutter are different from thread. They do not share memory, rather, they rely of message passing.

We want an isolate, to whom we send the message that we want nth prime number, and the isolate would later send us the message saying , hey, here is your nth prime number.

Flutter makes all this message passing really easy, using the Compute function.

Compute function requires 2 parameters, the function to execute, and the parameter to pass into that function. The function must be static or a global function.

static int getnthPrime(int n) {
int currentPrimeCount = 0;
int candidate = 1;
while(currentPrimeCount < n) {
++candidate;
if (isPrime(candidate)) {
++currentPrimeCount;
}
}
return candidate;
}
void _getPrime() async {
int ans = await compute(getnthPrime, 10000);
setState((){
prime=ans;
});
}

Just a single line of code does all the heavy lifting for us. If you run the code now, you will find that UI no longer freezes, and after some time, you get the 10000th prime printed out.

Understanding Isolates

Let’s say we don’t want to use compute function and want to get the same result using Isolates only.

Isolates rely on message passing to send and receive information. They do not share information and memory, which helps in getting rid of dead locks and anomaly behavior.

We send these messages on what dart calls as ports

Each isolate have two ports.

  1. Send Port — Sends message to isolate.
  2. Receive Port — Receive message from isolate.

We want to setup our code keeping these points in mind.

  1. There will be 2 isolates. Lets call one main flutter isolate, and other prime isolate.
  2. Main isolate must have both send and receive port of prime isolate.
  3. Send port of prime isolate will be used to send the parameter n.
  4. Receive port of prime isolate will be used to get resultant prime number.
  5. Prime isolate should have both send and receive port of main isolate.
  6. Receive port of main isolate will be used to receive the parameter n.
  7. Send port of main isolate will be used to send the resultant prime number.

At first, this seems like a lot, but all we are doing is creating a pipe between the two isolates using the ports.

First, let us create a receive port for main isolate.

ReceivePort receivePort = ReceivePort();

Each receive port also have a send port. Each message which is send on that port is received by that particular receive port. We need to pass this send port to our prime isolate, which we will spawn in just a moment.

await Isolate.spawn(getnthPrime, receivePort.sendPort);

We, now also need to update the definition of getnthPrime function. As isolates can communicate using messages only, we no longer need the value of n as a parameter.

static getnthPrime(SendPort sendPort) async {

At this point, we have set up the send port of the main isolates to the prime isolate. Using this port, we will now send the send port of the prime isolate back to main isolate. This is like a handshaking.

static getnthPrime(SendPort sendPort) async {
// Port for receiving message from main isolate.
// We will receive the value of n using this port.
ReceivePort receivePort = ReceivePort();
// Sending the send Port of isolate to receive port of main isolate
sendPort.send(receivePort.sendPort);

We will receive this port in the main isolate.

void _getPrime() async {
// Port where we will receive our answer to nth prime.
// From isolate to main isolate.
ReceivePort receivePort = ReceivePort();
await Isolate.spawn(getnthPrime, receivePort.sendPort);

// Send port for the prime number isolate. We will send parameter n
// using this port.
SendPort sendPort = await receivePort.first;

Now, both the isolates have send port of each other. We will now setup a middle ware function to send messages to the isolate.

Future sendReceive(SendPort send, message) {
ReceivePort receivePort = ReceivePort();
send.send([message, receivePort.sendPort]);
return receivePort.first;
}

This function sends the message and a port to receive the answer. We can simply use this function to read the answer.

int ans = await sendReceive(sendPort, 100);

The 3 function looks like this.

/// Returns the nth prime number.
static getnthPrime(SendPort sendPort) async {
// Port for receiving message from main isolate.
// We will receive the value of n using this port.
ReceivePort receivePort = ReceivePort();
// Sending the send Port of isolate to receive port of main isolate
sendPort.send(receivePort.sendPort);
var msg = await receivePort.first;

int n = msg[0];
SendPort replyPort = msg[1];
int currentPrimeCount = 0;
int candidate = 1;
while (currentPrimeCount < n) {
++candidate;
if (isPrime(candidate)) {
++currentPrimeCount;
}
}
replyPort.send(candidate);

}

void _getPrime() async {
// Port where we will receive our answer to nth prime.
// From isolate to main isolate.
ReceivePort receivePort = ReceivePort();
await Isolate.spawn(getnthPrime, receivePort.sendPort);

// Send port for the prime number isolate. We will send parameter n
// using this port.
SendPort sendPort = await receivePort.first;
int ans = await sendReceive(sendPort, 10000);
setState(() {
prime = ans;
});
}

Future sendReceive(SendPort send, message) {
ReceivePort receivePort = ReceivePort();
send.send([message, receivePort.sendPort]);
return receivePort.first;
}

Now, if you run the app, you would find that UI is buttery smooth. You do not need to set up all these isolates yourself. Notice how we achieved the same result using the compute function provided by flutter.

This article was inspired by the discussion on a issue here. You can find the complete code at git repo here.

More where this came from

This story is published in Noteworthy, where thousands come every day to learn about the people & ideas shaping the products we love.

Follow our publication to see more product & design stories featured by the Journal team.

--

--