top of page

Concurrency and Parallelism in Dart and how it is used in Flutter


Concurrency and Parallelism in Dart and how it is used in Flutter

Concurrency and parallelism are essential concepts in programming that allow developers to optimize application performance and enhance user experience. In Dart, the programming language used in developing Flutter apps, concurrency and parallelism can be achieved using various mechanisms such as Isolates, Futures, and Streams. In this blog, we will discuss the basics of concurrency and parallelism in Dart, and how they can be used to improve the performance of Flutter apps.


What is Concurrency?


Concurrency is the ability of a system to run multiple tasks or processes simultaneously.


Isolates in Dart


In Dart, concurrency can be achieved through Isolates, which are Dart's lightweight units of concurrency that run in their own memory space, have their own event loop, and do not share memory with other isolates.


Isolates can communicate with each other through message passing, which involves sending and receiving messages between isolates. Isolates are designed to be safe and isolate the app's code from errors or bugs that may occur in other isolates. This means that if an isolate crashes, it will not affect the rest of the app or other isolates.


Isolates can be used to perform CPU-bound or long-running operations without blocking the UI thread or main isolate. This is important in Flutter apps, where long-running operations can cause the app to become unresponsive and affect the user experience.


To create an isolate in Dart, we can use the Isolate.spawn() method, which takes a function to be executed in the isolate as its argument. Here is an example:


import 'dart:isolate';

void main() async {
  final receivePort = ReceivePort();
  await Isolate.spawn(isolateFunction, receivePort.sendPort);
  receivePort.listen((message) => print('Received: $message'));
}

void isolateFunction(SendPort sendPort) {
  sendPort.send('Hello from isolate!');
}


In this example, we create a new isolate using the Isolate.spawn() method and pass a function called isolateFunction to be executed in the isolate. The receivePort is used to receive messages sent from the isolate, and we listen for incoming messages using the listen() method. When the isolate sends a message using the sendPort.send() method, it is received by the receivePort, and we print the message to the console.


What is Parallelism?


Parallelism is the ability of a system to execute multiple tasks or processes simultaneously on multiple processors or cores. In Dart, parallelism can be achieved through asynchronous programming using Futures and Streams.


Futures in Dart


Futures in Dart represent a value that may not be available yet but will be at some point in the future. Futures can be used to perform asynchronous operations such as network requests, file I/O, and other long-running operations that do not block the UI thread.


To use a Future in Dart, we can create a new instance of the Future class and pass a function that returns the value of the Future as its argument. Here is an example:


void main() {
  final future = Future(() => 'Hello, world!');
  future.then((value) => print(value));
}


In this example, we create a new Future using the Future() constructor and pass a function that returns the value 'Hello, world!' as its argument. We then use the then() method to listen for the completion of the Future and print its value to the console.


Streams in Dart


Streams in Dart represent a sequence of values that can be asynchronously produced and consumed. Streams can be used to perform asynchronous operations that produce a series of values such as user input, sensor data, and other real-time data.


To use a Stream in Dart, we can create a new instance of the Stream class and pass a function that produces the values of the Stream as its argument. Here is an example:


import 'dart:async';

void main() {
  final stream = Stream.periodic(Duration(seconds: 1), (value) => value);
  stream.listen((value) => print(value));
}


In this example, we create a new Stream using the Stream.periodic() constructor and pass a function that produces the value of the Stream as its argument. The function returns the value of a counter that increments by one every second. We then use the listen() method to listen for the values produced by the Stream and print them to the console.


Concurrency and Parallelism in Flutter


In Flutter, concurrency and parallelism can be used to improve the performance of the app and enhance the user experience. Here are some examples of how concurrency and parallelism can be used in Flutter:

  • Performing long-running operations: Long-running operations such as network requests, file I/O, and database queries can be performed in isolates or using Futures to avoid blocking the UI thread and improve app performance.


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

class MyWidget extends StatelessWidget {
  Future<String> fetchData() async {
    // perform long-running operation
    return 'Hello, world!';
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<String>(
      future: fetchData(),
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          return Text(snapshot.data);
        } else if (snapshot.hasError) {
          return Text('${snapshot.error}');
        }
        return CircularProgressIndicator();
      },
    );
  }
}


In this example, we use a Future to perform a long-running operation that returns the value 'Hello, world!'. We then use a FutureBuilder widget to display the value returned by the Future when it is available.

  • Handling real-time data: Real-time data such as user input and sensor data can be handled using Streams to provide a responsive user experience.


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

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  final _streamController = StreamController<String>();

  @override
  void dispose() {
    _streamController.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<String>(
      stream: _streamController.stream,
      builder: (context, snapshot) {
        return TextField(
          onChanged: (value) => _streamController.add(value),
          decoration: InputDecoration(
            hintText: 'Enter text',
            labelText: 'Text',
          ),
        );
      },
    );
  }
}


In this example, we use a StreamController to handle user input from a TextField widget. We then use a StreamBuilder widget to listen for the values produced by the Stream and update the UI when new values are available.


  • Isolates are an excellent tool for providing concurrency in Flutter apps. They allow developers to perform computationally intensive operations in the background without blocking the main UI thread, which can improve the app's performance and responsiveness.


import 'dart:isolate';

import 'package:flutter/material.dart';

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  String _result = '';

  @override
  void initState() {
    super.initState();
    _calculate();
  }

  void _calculate() async {
    final receivePort = ReceivePort();
    final isolate = await Isolate.spawn(_compute, receivePort.sendPort);

    receivePort.listen((message) {
      setState(() {
        _result = 'Result: $message';
      });
      receivePort.close();
      isolate.kill();
    });
  }

  static void _compute(SendPort sendPort) {
    // Do some expensive computation here...
    final result = 42;
    sendPort.send(result);
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(_result),
    );
  }
}


In this example, we create a StatefulWidget called MyWidget. In the initState() method, we call the _calculate() method to perform some expensive computation in an isolate.

The _calculate() method creates a ReceivePort and spawns an isolate using the Isolate.spawn() method. We pass the sendPort of the ReceivePort to the _compute() function in the isolate.

In the _compute() function, we perform some expensive computation and send the result back to the main isolate using the sendPort.send() method.

In the receivePort.listen() callback, we update the _result variable with the computed result and call setState() to update the UI. We also close the ReceivePort and kill the isolate.

Finally, in the build() method, we display the computed result in a Text widget in the center of the screen.

Note that isolates cannot access the BuildContext object directly, so we cannot use Scaffold.of(context) or Navigator.of(context) inside an isolate. However, we can pass arguments to the _compute() function using the Isolate.spawn() method if needed.

Conclusion


Concurrency and parallelism are essential concepts in programming that can be used to optimize application performance and enhance user experience. In Dart, concurrency can be achieved using Isolates, while parallelism can be achieved using Futures and Streams.


In Flutter, concurrency and parallelism can be used to perform long-running operations, handle real-time data, and improve app performance. Understanding these concepts and how to use them in Flutter can help developers create fast and responsive apps that provide an excellent user experience.


Blog for Mobile App Developers, Testers and App Owners

 

This blog is from Finotes Team. Finotes is a lightweight mobile APM and bug detection tool for iOS and Android apps.

In this blog we talk about iOS and Android app development technologies, languages and frameworks like Java, Kotlin, Swift, Objective-C, Dart and Flutter that are used to build mobile apps. Read articles from Finotes team about good programming and software engineering practices, testing and QA practices, performance issues and bugs, concepts and techniques. 

bottom of page