Event Loop — Flutter

Gaurav Swarankar
6 min readFeb 5, 2024

--

Dart uses an event-driven programming model and you often work with event loops to handle asynchronous operations efficiently. Dart provides the EventLoop class as part of the dart:async library to manage events and asynchronous tasks. The event loop is crucial for handling events, such as user input, network requests, and timers, without blocking the application's main thread.

The event loop is an invisible process that manages the execution of code events and callbacks. It’s a crucial part of the Dart runtime ensuring that all the calls in your Dart code run in the correct order. The Dart event loop works by continuously checking two queues:

The event queue and the micro task queue.

The event loop job is to handle events like mouse events, tap events, or other external events like data from a server. These events are added to the event queue.

The micro task queue is used for short asynchronous internal actions that come from your Dart code.

Role of Event Loop in Dart

Dart is a single-threaded language. The event loop plays a critical role in managing how Dart code runs. When you start your Dart app, the main isolate (a new isolate) is created and the event loop starts. The main function often void main() is the first to execute synchronously.

Asynchronous programming in Dart involves the use of Future objects and async functions. Whenever we write asynchronous code we schedule tasks to be run later. For instance, when we use await Future, we’re telling the Dart event loop to complete other Dart code first and come back when the Future is complete.

The Dart event loop considers the microtask queue first. If the microtask queue is empty, it moves on to the next event in the event queue. This mechanism ensures that synchronous code and short asynchronous internal actions are prioritized over handling events like user input or data from a server.

How Dart Event Loop Works

The Dart event loop mechanism is an infinite loop that keeps your Dart app running. It starts when your app starts and continues until there are no more events to process. The Dart event loop works by checking two queues: the event queue and the microtask queue.

Here is a simplified view of how the Dart event loop works:

     void main() {
print('Dart app starts');
Future(() => print('This is a new Future'));
scheduleMicrotask(() => print('This is a micro task'));
print('Dart app ends');
}

In the above sample code, the main function executes synchronously. The print(‘Dart app starts’) and print(‘Dart app ends’) are executed immediately. The Future and scheduleMicrotask calls are added to their respective queues and are executed once the main function has completed.

Event queue is used for handling events like user input or data from a server. When we create a new Future, we’re adding an event to the event queue.

    void main() {
print('Dart app starts');
Future(() => print('This is a new Future'));
print('Dart app ends');
}

Microtask Queue

Microtask queue is a part of Dart’s event loop mechanism that handles short asynchronous internal actions. These actions are often the result of scheduleMicrotask calls in your Dart code. The microtask queue is given priority over the event queue in the Dart event loop. This means that all tasks in the microtask queue will be processed before the event loop considers the event queue.

The microtask queue is ideal for tasks that you want to have completed as soon as possible, but not immediately. For instance, you might use the microtask queue to delay some computation or to allow the UI to update between tasks.

     void main() {
print('Dart app starts');
scheduleMicrotask(() => print('Microtask 1'));
scheduleMicrotask(() => print('Microtask 2'));
Future(() => print('This is a new Future'));
print('Dart app ends');
}

In this sample code, Microtask 1 and Microtask 2 are processed before the new Future, even though the Future was added to the event queue before Microtask 2 was added to the microtask queue. This demonstrates how Dart prioritizes microtasks over events.

Event Queue

The event queue is another critical component of the Dart event loop. It’s responsible for handling events such as user interactions (like mouse events or tap events), timers, and I/O operations. When you create a new Future or when an external event occurs, an event is added to the event queue.

The event queue is processed after the microtask queue. This means that all the tasks in the microtask queue are completed before the Dart event loop starts processing the event queue.

Here’s a simple example of adding an event to the event queue:

     void main() {
print('Dart app starts');
Future(() => print('This is a new Future'));
print('Dart app ends');
}

the new Future is added to the event queue and is processed after all microtasks have been completed.

Differences between Microtask and Event Queues

The main difference between the microtask and event queues lies in their priority in the Dart event loop. The microtask queue is given priority over the event queue. This means that all tasks in the microtask queue are processed before the Dart event loop starts processing the event queue.

Let’s go for some examples.

In asynchronous programming the Dart event loop plays a crucial role in managing how your Dart code runs. When you use Future objects or async functions, you’re scheduling tasks to be run later. These tasks are added to the event queue and are processed when the Dart event loop gets to them.

Let’s consider a real-world scenario where the event loop is crucial for handling asynchronous operations in a Flutter application: handling user authentication and fetching user data from a server.

User Authentication: Imagine you have a Flutter app that requires user authentication. When the user enters their credentials and clicks the “Login” button, the app needs to communicate with a server to verify the credentials.

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: LoginPage(),
);
}
}

class LoginPage extends StatefulWidget {
@override
_LoginPageState createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
bool isLoading = false;

void _login() async {
setState(() {
isLoading = true;
});

// Simulate a network request for authentication
await Future.delayed(Duration(seconds: 2));

// After authentication, fetch user data
_fetchUserData();
}

void _fetchUserData() async {
// Simulate another network request to fetch user data
await Future.delayed(Duration(seconds: 2));

// Navigate to the home screen with user data
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => HomeScreen(userData: "User123")),
);
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Login Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: isLoading ? null : _login,
child: Text('Login'),
),
if (isLoading) CircularProgressIndicator(),
],
),
),
);
}
}

class HomeScreen extends StatelessWidget {
final String userData;

HomeScreen({required this.userData});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home Screen'),
),
body: Center(
child: Text('Welcome, $userData!'),
),
);
}
}

In the above examples. The _login method simulates a network request for user authentication. While waiting for the authentication to complete, the UI remains responsive thanks to the event loop and the asynchronous nature of Dart.

  • Once authentication is successful, the _fetchUserData method simulates another network request to fetch additional user data. Again, this operation is handled asynchronously without blocking the main thread.
  • The app navigates to the home screen (HomeScreen) after both authentication and data fetching are complete.

In a real-world scenario, the network requests might involve actual authentication APIs and data endpoints. By leveraging Dart’s asynchronous capabilities and the event loop, Flutter applications can provide a smooth user experience even when dealing with potentially time-consuming operations like user authentication and data fetching. The app remains responsive, and users can interact with the UI while the app communicates with the server in the background.

Clap 👏 If this article helps you.

See you all again soon!

--

--

Gaurav Swarankar

Mobile application developer | Flutter | dart | Getx | API | Mobility | Bloc |