Cover image for Introduction to co-routines in flutter (1)

vmodal_ai

In Dart and Flutter, there is no native keyword or native mechanism explicitly named "coroutine" like there is in Kotlin or Go. However, Dart implements the exact functional equivalent of coroutines through Futures, Streams, Generators, and the Event Loop architecture.

To write deep, advanced asynchronous Flutter apps, you must master how Dart simulates coroutines under the hood, how its cooperative multitasking model functions, and how to utilize synchronous/asynchronous generators to mirror game-loop style coroutines.


1. The Core Architecture: Cooperative Multitasking and the Event Loop

Unlike systems languages that rely on preemptive multithreading (where the OS forces thread context-switches), Flutter relies on Dart’s single-threaded, cooperative execution model.
A Dart thread (contained within an Isolate) runs on a continuous event loop fueled by two internal FIFO queues:

  1. The Microtask Queue: Reserved for brief, internal framework operations that must execute immediately after the current code block finishes, before letting the UI redraw.
  2. The Event Queue: Handles external triggers like touch events, I/O operations, timers, and drawing frames.

[ Current Execution ]


[ Microtask Queue ] ──(If not empty)──► [ Execute Microtask ]
│ (When empty)

[ Event Queue ] ──(Pop oldest)────► [ Execute Event Handler ]

When you use the async and await keywords, you are creating a coroutine-like suspension point. The execution of that particular function halts, its state is captured in memory, control is yielded back to the event loop, and the function resumes precisely where it left off once its corresponding event is pulled from the queue.


2. Deconstructing async/await as True Coroutines

Under the hood, Dart compiles async/await syntax into a state machine. A Future is not a thread; it is a placeholder object representing an eventual result.
When a function hits an await statement:

  • Suspension: The function execution pauses. It releases its slice of the execution thread immediately so Flutter can maintain a stable 60Hz/120Hz rendering pipeline.
  • Continuation: Dart registers the remaining chunk of your function as a callback attached to the event loop.
  • Resumption: When the asynchronous process completes, a completion event is pushed to the event queue. The event loop picks it up and execution jumps directly back into your function context.

Code Mechanics

Future<void> fetchUserData() async {
  print("1. Fetching started..."); // Executes synchronously

  // Suspension point: control is returned to Flutter's Event Loop
  String data = await SimulatedNetwork.getString(); 

  print("2. Data received: $data"); // Resumes asynchronously when data arrives
}

Enter fullscreen mode Exit fullscreen mode


3. Generator Coroutines (sync* and async*)

If you want to build classic coroutines—like those found in game engines like Unity where a function pauses execution and iteratively returns data back to the caller over time—you must use Dart's Generators.

  • sync* (Synchronous Generator): Returns an Iterable. It acts as a synchronous coroutine, yielding control and data back to the loop iterator on demand.
  • async* (Asynchronous Generator): Returns a Stream. It acts as an asynchronous coroutine, pushing events over time as they become available.

Deep Implementation: A Game-Loop Style Coroutine in Flutter

This implementation shows how to build custom coroutines to handle frame-by-frame calculations or time-delayed animations cleanly without blocking the UI thread or nesting timers.

import 'dart:async';import 'package:flutter/material.dart';
class CoroutineRunner extends StatefulWidget {
  const CoroutineRunner({super.key});

  @override
  State<CoroutineRunner> createState() => _CoroutineRunnerState();
}
class _CoroutineRunnerState extends State<CoroutineRunner> {
  String _status = "Idle";
  int _countdown = 5;

  // An async generator working as a custom coroutine
  Stream<String> RoutineTask() async* {
    yield "Initializing Engine...";
    await Future.delayed(const Duration(seconds: 1)); // Yielding execution

    yield "Loading Assets...";
    await Future.delayed(const Duration(seconds: 1));

    while (_countdown > 0) {
      yield "Launching in $_countdown...";
      await Future.delayed(const Duration(seconds: 1));
      _countdown--;
    }

    yield "🚀 Lift off!";
  }

  void _startCoroutine() async {
    await for (String stage in RoutineTask()) {
      setState(() {
        _status = stage;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(_status, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: _startCoroutine,
              child: const Text("Run Coroutine Routine"),
            ),
          ],
        ),
      ),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode


4. Dart "Coroutines" vs. Kotlin Coroutines

For engineers bridging the gap from native Android development, understanding the structural layout differences helps prevent architectural design errors.

Feature Dart (Flutter) Kotlin (Native/KMP)
Concurrency Foundation Single-threaded Event Loop Multi-threaded Thread Pool / Work Stealing
Primary Data Types Future and Stream Deferred and Flow
Keywords async, await, yield, sync*, async* suspend
Thread Context Switching Done via Isolates (heavyweight, separate memory) Done via Dispatchers (lightweight, shared memory)
Structured Concurrency Managed via StreamSubscription or CancelableOperation Native CoroutineScope and parent-child cancellation hierarchy

5. Architectural Hazards: When Coroutines Freeze the UI

Because Dart is single-threaded, its "coroutines" are cooperative, not preemptive. A regular execution block inside an async function cannot be magically paused by the system. If you execute a highly intensive task without an explicit await or yield bridge, the entire Flutter UI will freeze.

The Anti-Pattern (UI Blockage)

Future<int> computeHeavySum() async {
  // This loop executes entirely on the main UI thread.
  // The 'async' keyword does not push this to a background thread!
  int sum = 0;
  for (int i = 0; i < 1000000000; i++) {
    sum += i;
  }
  return sum; // The framework frames drop completely during this loop.
}

Enter fullscreen mode Exit fullscreen mode

The Architecture Fix

To bypass single-thread calculation limits, spawn a true background worker thread using Isolates. The global compute helper function acts like Kotlin's withContext(Dispatchers.Default), offloading calculation workloads and passing the final result back to the main UI event loop via messaging ports.

import 'package:flutter/foundation.dart';
// Synchronous heavy workload function
int heavySumSync(int count) {
  int sum = 0;
  for (int i = 0; i < count; i++) {
    sum += i;
  }
  return sum;
}
// Spawns an Isolate coroutine wrapper cleanly
Future<void> runHeavyComputation() async {
  // compute isolates this operation completely onto a separate thread
  int result = await compute(heavySumSync, 1000000000);
  print("Computation finished safely on background isolate: $result");
}

Enter fullscreen mode Exit fullscreen mode