Mastering Asynchronous Flutter

Explore the depths of asynchronous programming in Flutter with our detailed guide. 

Whether you're a beginner or an experienced developer, discover best practices and code samples to elevate your Flutter projects to the next level. 


Don't miss out on essential insights into async programming with Flutter and enhance your skills for large-scale project success! 🚀


Synchronous: Code is executed sequentially, and each operation must wait for the previous one to complete.

Asynchronous: Code doesn't wait for the completion of each operation and continues with the next one. Callbacks, Promises, or Futures are often used.

..



async and await:

  •   async: Used to define a function that performs asynchronous operations. It allows the function to use the await keyword.
  •   await: Used inside an async function to pause execution until a Future completes and returns its result.


  Future<int> fetchData() async {
    await Future.delayed(Duration(seconds: 2));
    return 42;
  }

..



Future and Future<void>:

  •   Future: Represents a value that might be available in the future. It's used for asynchronous computations that yield a single result.
  •   Future<void>: Represents an asynchronous operation that doesn't return a value.


  Future<String> fetchData() {
    return Future.delayed(Duration(seconds: 2), () => 'Data fetched');
  }

..


Using try, catch, and finally:

you can handle exceptions in asynchronous code using try, catch, and finally blocks


Future<void> fetchData() async {
  try {
    // Asynchronous operation that might throw an exception
    await Future.delayed(Duration(seconds: 2));
    throw Exception('Simulated error');
  } catch (error) {
    // Handle the exception
    print('Error: $error');
  } finally {
    // Code that runs whether an exception occurred or not
    print('Cleanup or additional logic');
  }
}

void main() {
  fetchData();
}

..



Using catchError: method is used to register a callback that will be invoked if the Future completes with an error. 

It allows you to handle errors in a way that doesn't break the entire program


Future<void> fetchData() async {
  // Asynchronous operation that might throw an exception
  await Future.delayed(Duration(seconds: 2));
  throw Exception('Simulated error');
}

void main() {
  fetchData().catchError((error) {
    // Handle the exception
    print('Error: $error');
  });
}

..



Future.then: method is used to register a callback that will be invoked when the Future completes successfully. 

It allows you to chain multiple asynchronous operations together.


Future<int> fetchData() async {
  // ... asynchronous operation ...
  return 42;
}

fetchData().then((result) {
  print('Data fetched successfully: $result');
});

It's common to use both then and catchError together for comprehensive error handling..

..



Stream:

Represents a sequence of asynchronous events. It's used for continuous data flow, allowing you to react to data as it arrives.

  • async*: This indicates that the function is asynchronous and returns a stream.
  • yield i: This statement adds the current value of i to the stream, allowing consumers of the stream to receive these values asynchronously.


// Using Stream
Stream<int> countNumbers() async* {
  for (int i = 1; i <= 5; i++) {
    await Future.delayed(const Duration(seconds: 1));
    yield i;
  }
}

void main() {
  // Subscribe to the stream
  countNumbers().listen((int number) {
    // This callback will be invoked for each value emitted by the stream
    print(number.toString());
    // You can perform any action you need with the emitted values
  });
}

..



await for:

await for is used to asynchronously iterate over stream events.

It allows awaiting each event in a stream sequentially, making it suitable for asynchronous stream processing.


// Using Stream
Stream<int> countNumbers() async* {
  for (int i = 1; i <= 5; i++) {
    await Future.delayed(const Duration(seconds: 1));
    yield i;
  }
}

Future<void> main() async {
  // Using await for to iterate over the stream
  await for (int number in countNumbers()) {
    // This block will be executed for each value emitted by the stream
    print(number.toString());
  }

  print('Stream completed.');
}

..



FutureBuilder is a Flutter widget that rebuilds when a Future completes. It is used for handling asynchronous operations in the context of building UI.

..



Completer

  • A Completer is used to produce a Future and control when it completes.
  • It is handy when working with non-Future-based asynchronous APIs.

import 'dart:async';

void main() {
  // Create a Completer
  Completer<String> completer = Completer<String>();

  // Create a Future using the Completer
  Future<String> futureResult = completer.future;

  // Simulate an asynchronous operation
  simulateAsyncOperation(completer);

  // Attach a callback to the Future
  futureResult.then((result) {
    print('Async operation completed: $result');
  });
}

void simulateAsyncOperation(Completer<String> completer) {
  // Simulate an asynchronous operation
  Future.delayed(Duration(seconds: 2), () {
    // Complete the Future with a result
    completer.complete('Operation successful');
  });
}

It's often used in scenarios where you want to manually control when a Future completes.
..




Isolate
  • An Isolate is a separate Dart process that runs concurrently with the main program.
  • It is used for parallelizing computation-intensive tasks without affecting the main program's performance.
To really understand isolates, first we need to go further back and make sure some basic


Processor Cores and Threads

Core is a physical hardware component whereas thread is the virtual component that manages the tasks of the core.

More cores mean the CPU can handle multiple tasks simultaneously. Tasks are divided among the cores, allowing for parallel processing.

Threads are the smallest units of execution within a process. Each core can handle multiple threads at the same time.



Concurrent and parallel processing

Concurrency is when two or more tasks can start, run, and complete in overlapping time periods. It doesn’t necessarily mean they’ll ever both be running at the same instant. For example, multitasking on a single-core machine. 

Parallelism is when tasks literally run at the same time, e.g., on a multicore processor.
..

Using isolates, Dart code can perform multiple independent tasks at once, using additional cores if they’re available. Each Isolate has its own memory and a single thread running an event loop. We’ll get to event loop in a minute.


// Isolates with run function
Future<String> startDownloadUsingRunMethod() async {
  final imageData = await Isolate.run(_readAndParseJsonWithoutIsolateLogic);
  return imageData;
}

Future<String> _readAndParseJsonWithoutIsolateLogic() async {
  await Future.delayed(const Duration(seconds: 2));
  return 'this is downloaded data';
}
..




StreamController is a class that provides a way to create, send, and listen for stream events. It serves as a bridge between producers of events (the source) and consumers (the listeners) by managing the stream's lifecycle and handling the flow of asynchronous data.

Remember to close the stream when you're done to avoid resource leaks.
  • Single Subscription Streams: Ordered sequences with one listener, crucial when event order matters.

import 'dart:async';

void main() {
  // Create a StreamController of integers
  var controller = StreamController<int>();

  // Listen to the stream
  var subscription = controller.stream.listen(
    (data) => print('Data: $data'),
    onError: (error) => print('Error: $error'),
    onDone: () => print('Stream closed'),
    cancelOnError: true, // Cancel the subscription on error
  );

  // Add data to the stream
  controller.add(1);
  controller.add(2);
  controller.add(3);

  // Simulate an error
  controller.addError('Error message');

  // Close the stream
  controller.close();

  // Try adding more data after closing (will not be received)
  controller.add(4);

  // Cancel the subscription manually (not necessary if using onDone)
  // subscription.cancel();
}
..
  • Broadcast Streams: Flexible streams allowing multiple listeners, with instant event access and the ability to rejoin after canceling.

import 'dart:async';

void main() {
  // Create a BroadcastStreamController of integers
  var controller = StreamController<int>.broadcast();

  // Listen to the broadcast stream with multiple listeners
  var subscription1 = controller.stream.listen(
    (data) => print('Listener 1: $data'),
    onError: (error) => print('Listener 1 Error: $error'),
    onDone: () => print('Listener 1 Stream closed'),
    cancelOnError: true,
  );

  var subscription2 = controller.stream.listen(
    (data) => print('Listener 2: $data'),
    onError: (error) => print('Listener 2 Error: $error'),
    onDone: () => print('Listener 2 Stream closed'),
    cancelOnError: true,
  );

  // Add data to the broadcast stream
  controller.add(1);
  controller.add(2);
  controller.add(3);

  // Simulate an error
  controller.addError('Error message');

  // Close the broadcast stream
  controller.close();

  // Try adding more data after closing (will not be received)
  controller.add(4);

  // Cancel the subscriptions manually (not necessary if using onDone)
  subscription1.cancel();
  subscription2.cancel();
}
..



Yield*
The ‘yield*’ keyword is used in the generator function to delegate the control of execution to another generator function. 
The delegation occurs when the generator function encounters a yield* expression.


void main() {
  // Generate numbers using the generateMoreNumbers function
  var numbers = generateMoreNumbers();

  // Print each generated number
  for (var number in numbers) {
    print('Generated Number: $number');
  }
}

// Generator function that yields three numbers: 1, 2, and 3
Iterable<int> generateNumbers() sync* {
  yield 1;
  yield 2;
  yield 3;
}

// Generator function that includes numbers from generateNumbers and adds 4 and 5
Iterable<int> generateMoreNumbers() sync* {
  // Yield numbers from generateNumbers
  yield* generateNumbers();

  // Add two more numbers: 4 and 5
  yield 4;
  yield 5;
}

Output:
Generated Number: 1
Generated Number: 2
Generated Number: 3
Generated Number: 4
Generated Number: 5
..
Notes:
Use sync* when you are generating a sequence of values without involving asynchronous operations.
Use async* when generating a sequence of values involves asynchronous operations.
..

Comments