Multithreading in Flutter

6031 VIEWS

· ·

Multithreading is a technique by which several processors can use a single code set at different execution stages. Multithreading is essential because when working on mobile platforms, some expensive operations within this flow either need to be asynchronous or should run on the background threads. We accomplish this process with the use of Isolates.

All dart code runs in an isolate, making them so important that the entirety of dart applications runs in an isolate. This article will guide you on utilizing isolates to run multiple threads in your Flutter application. Let us begin!

What is an Isolate 

Earlier, we said all dart code runs in an isolate. Thus, we can call it a memory space as one isolate contains memory and an event loop where all the code runs. In layman’s terms, isolate stores things in memory, and when an event happens, it goes into the event loop.

The event loop is a thread that executes the event within our dart application. Unlike other programming languages such as Java and C++, in which multiple threads can use the same memory, this is not the case when it comes to dart. Isolates can not share a memory with another isolate because they are separate and execute logic in their thread.

Since isolates are separate, we can have more than one isolate within our app, and that’s how we can have multithreaded applications using dart. Most apps only need one event loop and one isolate running all of our code simultaneously. Multiple isolates only become necessary when dealing with complex logic that takes time and stops frames from being rendered on the main loop.

There are two ways to create an isolate, and they are listed below:

  1. Isolate.spawn()
  2. compute()

In a moment, we will create a simple Flutter application to show you how to run two different isolates within the same dart application.

Getting Started

We will start by building a simple Flutter application that increments a number once we click a button. We will need to have Flutter installed in our operating system to proceed. Here is a link showing how to install Flutter in your respective operating system. We will use android studio and a physical emulator to test our Flutter application for this tutorial.

To proceed, follow the steps below:

  • We begin by firing up our android studio, and we select the option where we get to start a new Flutter project.

  • The android studio will prompt us to choose the path to our Flutter SDK in the next section. We click on Next after selecting the path to the Flutter SDK.

  • In this section, we will enter our desired project name (note: Flutter has a designated naming convention) and click on Finish.

Voila, we have created a new Flutter project.

Following this, Android Studio automatically opens the main.dart file. However, we still need to make some changes to this file, as we will need part of the default counter syntax in Flutter.

Our additions include adding a simple text button at the center of our app and removing the debug banner on our app bar. To do this, we will clear out the previous widget and replace it with the one below:

Widget build(BuildContext context) {
 return Scaffold(
   appBar: AppBar(
     title: const Text('Sample Code'),
   ),
   body: Center(child: Column(
     mainAxisAlignment: MainAxisAlignment.center,
     children: <Widget>[
       Text(_count.toString()),
       TextButton(
         child: const Text("Add"),
         onPressed: () async{
           _count++;
           setState(() {});
         },
       ),
     ]
   )),
 );
}

If we add the debugShowCheckedModeBanner to the MaterialApp class and set it to false, we will remove the debug banner in the app bar’s right-hand corner.

Isolate.spawn()

To add an isolate to our Flutter application, we will run our isolate from the initstate function. Then we will add the code Isolate.spawn() to import our dart isolate dependency.

Within the Isolate.spawn() class, we declare two variables, the function we want to run and the parameters we want to pass. Below is what the code looks like:

void initState() {
 Isolate.spawn(isolateFunction, 1000);
 super.initState();
}

We will declare the function out of any class since the app is currently running on the primary isolate. Our goal is to create a local count that goes through a “for loop” and increments until it reaches the number in our isolateFunction.

If our count is divisible by a hundred(100), we get a print to show us that it works. Below is an implementation:

void isolateFunction (int finalNum) {
 int _count = 0;
 for (int i = 0; i < finalNum; i++) {
   _count++;
   if((_count % 100) == 0) {
     print("isolate: " +_count.toString());
   }
 }
}

Our result from running this code is shown below:

Result

To prove that this is in a different isolate, we will create a breakpoint at the print statement to stop the execution of that isolate. If the increment button still works, then our application is multithreaded.

Now we can do heavy and complex computations within the other isolate, and our primary isolate will be running without stress.

Compute()

Before proceeding, we need to add another button below the add button to our scaffold widget. To do this, we will create an elevated button with the text Add Isolate and set it to a function runCompute.

ElevatedButton(
 child: const Text("Add Isolate"),
 onPressed: runCompute,
),

Next, we will implement something similar to the isolateFunction, except we want to return an integer from it. 

Since the app is currently running on the primary isolate, we should declare the function out of any class.

int computeFunction(int finalNum) {
  int _count = 0;

  for (int i = 0; i < finalNum; i++) {
    _count++;
    if ((_count % 100) == 0) {
      print("compute: " + _count.toString());
    }
  }
  return _count;
}

For our runCompute function, we want it to return us a future.

In this function, we will set the state of _count to await for our computeFunction. We have two variables within the compute class; computeFunction (a function) and 2000 (a parameter). Finally, we will call a setState, and now we can have a background logic that will affect our app. 

Future<void> runCompute() async {
  _count = await compute (computeFunction, 2000);
  setState(() {});
}
Result

At face value, our code looks like it is not doing much, but that is not the case. With the compute function, we have a background thread running. How do we know that?

If we create a breakpoint on the print statement and run our app, the primary isolate will still be able to increment numbers. That proves the presence of one isolate, but when we remove the breakpoint and click the Add Isolate button, the number is automatically incremented by 2000 (our parameter). Thus, this proves the presence of a second or background isolate.

Here is how our app looks at the moment:

Conclusion

We have given you a quick run-through on the use of isolates. You can now go a lot deeper and learn how to control isolates differently and use the send and receive port to share memory between isolates. However, remember that you only need isolates if you are working on complex logic. Happy coding!


Muyiwa Femi-ige is a technical writer who has a knack for putting codes and coding processes into a well documented format.


Discussion

Click on a tab to select how you'd like to leave your comment

Leave a Comment

Your email address will not be published. Required fields are marked *

Menu
Skip to toolbar