In Flutter, understanding how tasks are executed—especially asynchronous ones—is crucial for building responsive apps. In this article, we'll break down Dart's event loop using a relatable metaphor: a factory conveyor belt. This analogy simplifies the concepts for beginners while connecting them to real-world code examples.
⚙️ Setting the Scene: A One-Worker Factory
Imagine you manage a factory with:
- One worker who processes tasks.
- A conveyor belt that carries tasks to the worker.
This worker can only handle one task at a time. Every task is placed in one of three areas:
- Immediate Processing Area for urgent, must-do-now tasks (synchronous tasks).
- High-Priority Queue for small but important tasks (microtask queue).
- Low-Priority Queue for everything else (event queue).
The worker (Dart runtime) picks tasks in this order of priority and processes them accordingly. Let’s explore each area.
🔧 The Three Areas of Task Management
1. Immediate Processing Area (Synchronous Tasks)
- Description: This is where tasks that must be done "right now" are handled.
- Behavior: The worker cannot stop or switch to another task until the current one finishes.
- In Dart: Synchronous code runs immediately and blocks the main thread.
Example:
void main() {
print('Task 1 - Start');
print('Task 1 - End');
}
Output:
Task 1 - Start
Task 1 - End
Here, all tasks in the Immediate Processing Area are completed sequentially.
2. High-Priority Queue (Microtask Queue)
- Description: Contains tasks that must run soon but can wait until synchronous tasks finish.
- Behavior: The worker processes all microtasks before moving to lower-priority tasks.
-
In Dart: Microtasks are typically scheduled with
scheduleMicrotask()
or byFuture
completions.
Example:
import 'dart:async';
void main() {
print('Task 1 - Start');
scheduleMicrotask(() => print('Microtask 1'));
Future(() => print('Event Queue Task 1'));
print('Task 1 - End');
}
Output:
Task 1 - Start
Task 1 - End
Microtask 1
Event Queue Task 1
Microtasks (like Microtask 1
) run before tasks in the event queue.
3. Low-Priority Queue (Event Queue)
- Description: Holds tasks like user input, timers, or network responses.
- Behavior: The worker only processes these tasks after all synchronous and microtask queue tasks are done.
-
In Dart: Examples include I/O callbacks,
onTap
events, andTimer
callbacks.
Example:
import 'dart:async';
void main() {
print('Task 1 - Start');
Timer(Duration(seconds: 1), () => print('Event Queue Task 1'));
Future(() => print('Event Queue Task 2'));
print('Task 1 - End');
}
Output:
Task 1 - Start
Task 1 - End
Event Queue Task 2
Event Queue Task 1
Tasks in the event queue are processed in order but only after higher-priority tasks.
🚧 Async, Await, and the “External Machines”
Sometimes, a task is time-consuming (e.g., network requests, file I/O, heavy computations). In our factory metaphor, these tasks can be handed off to external machines 🏭:
-
The worker (Dart runtime) receives a box (an
async
function) that needs multiple steps to complete. - Midway through the process, the worker realizes that this step requires a machine (an asynchronous task, such as calling an API or reading a file).
- The worker labels this step with "
await
machine" and hands the box over to the machine for processing. - At this point, the worker temporarily “sets aside” the box (pausing the
async
function at theawait
point) and continues working on other boxes on the conveyor belt (processing events or microtasks). This means the event loop continues running other tasks, not blocked by the paused task. - Once the machine finishes its work, the box is returned with a note saying “OK, it’s done!” Dart moves the code after the
await
into the microtask queue (or event queue) for the worker to pick up and finish processing. - At this point, the worker (event loop) resumes the paused box (continues the
async
function after theawait
) and runs it to completion.
Important:
await
doesn’t block the whole app. It only suspends the current async function. Everything else continues running on the event loop.
Example:
void main() async {
print('Task 1 - Start');
await Future.delayed(Duration(seconds: 2), () => print('Async Task Complete'));
print('Task 1 - End');
}
Output:
Task 1 - Start
Async Task Complete
Task 1 - End
Here’s how it works:
- The worker starts "Task 1".
- At
await
, the worker hands off the task to an external machine (like a timer). - While waiting, the worker handles other tasks (if any).
- When the external machine finishes, the task returns to the worker, who resumes where it left off.
🏋️ Putting It All Together
The Event Loop’s Workflow
- The worker processes all synchronous tasks first (Immediate Processing Area).
- It then moves to the microtask queue and processes everything there.
- Finally, it processes tasks from the event queue.
- If new tasks appear in the microtask queue while processing the event queue, the worker goes back to the microtask queue first.
This prioritization ensures responsiveness and keeps your Flutter app smooth.
🎉 Wrapping Up
Understanding the Dart/Flutter event loop through this "factory conveyor belt" metaphor helps demystify how synchronous and asynchronous tasks are managed. By properly using async
, await
, and understanding task prioritization, you can write efficient and responsive Flutter apps.
Further Reading
Happy coding!
Top comments (0)