Performance programming in Flutter using Isolates

By | September 8, 2019
Flutter Isolates

Flutter Isolates


Watch Video Tutorial


As you all know Flutter is a single threaded App platform. So then

  • How to do multithreading ?.
  • How to execute heavy operations in one thread ?.
  • How to run big operations without affecting the UI ?.

But yes, all these can be achieved with the help of Isolates.


What are Isolates?

Isolates are similar to threads in UNIX, but there is a difference.
Like threads, Isolates don’t share memory. Isolates communicate with the help of messages.

A Flutter app is running in a single Isolate. So if we run a heavy operation in this thread, it’s definitely going to block the UI
and ruin the User experience. Isolates comes to help in this case.

The two ways to use Isolates

1. Using the ‘compute’ function in the Isolates package (High Level convenient function).
2. Using the SendPort and Receive Port for message passing (Low-level APIs).


So lets start…

For this example, I will be creating an animation widget which runs in the UI while we run the heavy operation in the Isolate.

create a class named ‘AnimationWidget‘.

We are going to animate only the border of this widget.

Here is the complete AnimationWidget code

class AnimationWidget extends StatefulWidget {
  @override
  AnimationWidgetState createState() {
    return AnimationWidgetState();
  }
}

class AnimationWidgetState extends State<AnimationWidget>
    with TickerProviderStateMixin {
  //
  AnimationController _animationController;
  Animation<BorderRadius> _borderRadius;

  @override
  void initState() {
    super.initState();
    _animationController =
        AnimationController(duration: const Duration(seconds: 1), vsync: this)
          ..addStatusListener((status) {
            if (status == AnimationStatus.completed) {
              _animationController.reverse();
            } else if (status == AnimationStatus.dismissed) {
              _animationController.forward();
            }
          });

    _borderRadius = BorderRadiusTween(
      begin: BorderRadius.circular(100.0),
      end: BorderRadius.circular(0.0),
    ).animate(CurvedAnimation(
      parent: _animationController,
      curve: Curves.linear,
    ));

    _animationController.forward();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _borderRadius,
      builder: (context, child) {
        return Center(
          child: Container(
            child: FlutterLogo(
              size: 200,
            ),
            alignment: Alignment.bottomCenter,
            width: 350,
            height: 200,
            decoration: BoxDecoration(
              gradient: LinearGradient(
                begin: Alignment.topLeft,
                colors: [Colors.blueAccent, Colors.redAccent],
              ),
              borderRadius: _borderRadius.value,
            ),
          ),
        );
      },
    );
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }
}

BorderRadiusTween is used to animate the Border of the Widget. Here we are reversing the animation when it completes, making it run infinitely.


Using Isolates

Declare a future which is the future for the Isolate.

Future<void> computeFuture = Future.value();

We will add two buttons one executes a long running operation on the Main Isolate thread and other in a Seperate Isolate thread.

 addButton1() {
    return FutureBuilder<void>(
      future: computeFuture,
      builder: (context, snapshot) {
        return OutlineButton(
          child: const Text('Main Isolate'),
          onPressed: createMainIsolateCallback(context, snapshot),
        );
      },
    );
}
addButton2() {
    return FutureBuilder<void>(
      future: computeFuture,
      builder: (context, snapshot) {
        return OutlineButton(
          child: const Text('Secondary Isolate'),
          onPressed: createSecondaryIsolateCallback(context, snapshot),
        );
      },
    );
}

Here are the callback functions.


VoidCallback createMainIsolateCallback(
    BuildContext context, AsyncSnapshot snapshot) {
  if (snapshot.connectionState == ConnectionState.done) {
    return () {
      setState(() {
        computeFuture = computeOnMainIsolate().then((val) {
          showSnackBar(context, 'Main Isolate Done $val');
        });
      });
    };
  } else {
    return null;
  }
}

VoidCallback createSecondaryIsolateCallback(
    BuildContext context, AsyncSnapshot snapshot) {
  if (snapshot.connectionState == ConnectionState.done) {
    return () {
      setState(() {
        computeFuture = computeOnSecondaryIsolate().then((val) {
          showSnackBar(context, 'Secondary Isolate Done $val');
        });
      });
    };
  } else {
    return null;
  }
}

Future<int> computeOnMainIsolate() async {
  return await Future.delayed(Duration(milliseconds: 100), () => fib(40));
}

Future<int> computeOnSecondaryIsolate() async {
  return await compute(fib, 40);
}

showSnackBar(context, message) {
  Scaffold.of(context).showSnackBar(SnackBar(
    content: Text(message),
  ));
}

We are using a Fibonacci function to do a long running operation.

int fib(int n) {
  int number1 = n - 1;
  int number2 = n - 2;
  if (0 == n) {
    return 0;
  } else if (0 == n) {
    return 1;
  } else {
    return (fib(number1) + fib(number2));
  }
}

The ‘computeOnMainIsolate‘ function will create a Delayed Future that runs on the Main Isolate thread.
The ‘computeOnSecondaryIsolate‘ uses the ‘compute’ function that creates a new Isolate. The new Isolate will accept the function and the parameters.

We pass in the Fibonacci function and the parameters to it.

Finally we are showing the result in a SnackBar.


If you run the application and run the MainIsolate, you will see that it freezes the Animation and the UI until it completes. But if we run the Secondary Isolate, we will see that the animation runs smoothly all the time it is running. That means, the ‘compute’ is spawning a new isolate and running the code there. So it doesn’t affect the running of the Main Isolate and the UI will be smooth and responsive.


Watch the complete Youtube Tutorial above to see it in action.

Here is the complete example.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

class PerformancePage extends StatefulWidget {
  @override
  _PerformancePageState createState() => _PerformancePageState();
}

class _PerformancePageState extends State<PerformancePage> {
  //
  Future<void> computeFuture = Future.value();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Performance using Isolate'),
      ),
      body: Container(
        color: Colors.white,
        child: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              AnimationWidget(),
              addButton1(),
              addButton2(),
            ],
          ),
        ),
      ),
    );
  }

  addButton1() {
    return FutureBuilder<void>(
      future: computeFuture,
      builder: (context, snapshot) {
        return OutlineButton(
          child: const Text('Main Isolate'),
          onPressed: createMainIsolateCallback(context, snapshot),
        );
      },
    );
  }

  addButton2() {
    return FutureBuilder<void>(
      future: computeFuture,
      builder: (context, snapshot) {
        return OutlineButton(
          child: const Text('Secondary Isolate'),
          onPressed: createSecondaryIsolateCallback(context, snapshot),
        );
      },
    );
  }

  VoidCallback createMainIsolateCallback(
      BuildContext context, AsyncSnapshot snapshot) {
    if (snapshot.connectionState == ConnectionState.done) {
      return () {
        setState(() {
          computeFuture = computeOnMainIsolate().then((val) {
            showSnackBar(context, 'Main Isolate Done $val');
          });
        });
      };
    } else {
      return null;
    }
  }

  VoidCallback createSecondaryIsolateCallback(
      BuildContext context, AsyncSnapshot snapshot) {
    if (snapshot.connectionState == ConnectionState.done) {
      return () {
        setState(() {
          computeFuture = computeOnSecondaryIsolate().then((val) {
            showSnackBar(context, 'Secondary Isolate Done $val');
          });
        });
      };
    } else {
      return null;
    }
  }

  Future<int> computeOnMainIsolate() async {
    return await Future.delayed(Duration(milliseconds: 100), () => fib(40));
  }

  Future<int> computeOnSecondaryIsolate() async {
    return await compute(fib, 40);
  }

  showSnackBar(context, message) {
    Scaffold.of(context).showSnackBar(SnackBar(
      content: Text(message),
    ));
  }
}

int fib(int n) {
  int number1 = n - 1;
  int number2 = n - 2;
  if (0 == n) {
    return 0;
  } else if (1 == n) {
    return 1;
  } else {
    return (fib(number1) + fib(number2));
  }
}

class AnimationWidget extends StatefulWidget {
  @override
  AnimationWidgetState createState() {
    return AnimationWidgetState();
  }
}

class AnimationWidgetState extends State<AnimationWidget>
    with TickerProviderStateMixin {
  //
  AnimationController _animationController;
  Animation<BorderRadius> _borderRadius;

  @override
  void initState() {
    super.initState();
    _animationController =
        AnimationController(duration: const Duration(seconds: 1), vsync: this)
          ..addStatusListener((status) {
            if (status == AnimationStatus.completed) {
              _animationController.reverse();
            } else if (status == AnimationStatus.dismissed) {
              _animationController.forward();
            }
          });

    _borderRadius = BorderRadiusTween(
      begin: BorderRadius.circular(100.0),
      end: BorderRadius.circular(0.0),
    ).animate(CurvedAnimation(
      parent: _animationController,
      curve: Curves.linear,
    ));

    _animationController.forward();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _borderRadius,
      builder: (context, child) {
        return Center(
          child: Container(
            child: FlutterLogo(
              size: 200,
            ),
            alignment: Alignment.bottomCenter,
            width: 350,
            height: 200,
            decoration: BoxDecoration(
              gradient: LinearGradient(
                begin: Alignment.topLeft,
                colors: [Colors.blueAccent, Colors.redAccent],
              ),
              borderRadius: _borderRadius.value,
            ),
          ),
        );
      },
    );
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }
}

Note: Isolates only accept functions that are either static or the method should not be a part of a class instance.

Thanks for reading, if you found my post useful, please leave your valuable comments below and subscribe to my youtube channel for more videos.

Leave a Reply

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