Itnext. IO /delayed- COD…
Author: numen31337.medium.com/
Published: February 7th -8 minutes to read
Microtask, Future or postFrameCallback, which should I use?
In this article, I want to take you into the depths of Flutter to learn more about the little journey of scheduling code execution. To start the conversation, let’s assume that we are building an application using the standard BLoC architecture, using the Provider library. To make this task challenging, after opening a new screen, we would have to initiate a network request to get something over the Internet. In this case, we have several options to initiate our request.
1. Getting data before showing our screen and then showing it with pre-loaded data may not be the best option. This may not be the best option. If you decide to get only part of the data you need, you are likely to load a lot of unnecessary data or clog the user interface with a spreader.
-
Start the loading process in BLoC, just before the screen displays, when creating BLoC itself or using the coordinator object to start it for you. This is the recommended approach if you want to keep your architecture clean.
-
Start the loading process in initState of the screen, trying to encapsulate this logic in the screen itself.
The third option may not be the best in terms of architectural correctness, but is actually quite common in the Flutter world. Let’s look at it, because it perfectly demonstrates our theme in a real-world scenario.
For demonstration purposes, here is a sample code. Notice what’s wrong with it?
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
MaterialApp(
title: 'Demo',
home: ChangeNotifierProvider(
create: (_) => MyHomePageBloc(),
child: MyHomePage(),
),
),
);
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
void initState() {
super.initState();
context.read<MyHomePageBloc>().fetchData();
}
@override
Widget build(BuildContext context) {
final bloc = context.watch<MyHomePageBloc>();
return Scaffold(
appBar: AppBar(),
body: Center(
child: bloc.loading ? CircularProgressIndicator() : Text(bloc.data),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<MyHomePageBloc>().fetchData(),
tooltip: 'Fetch', child: Icon(Icons.add), ), ); }}class MyHomePageBloc with ChangeNotifier {
String data = "Loading";
bool loading = false;
void fetchData() {
loading = true;
data = "Loading";
notifyListeners();
Future.delayed(Duration(seconds: 3), () {
loading = false;
data = "Done"; notifyListeners(); }); }}Copy the code
At first glance, everything seems fine. However, if you run it, it will inevitably crash and you will see something similar in the logs. ‘package: flutter/SRC/widgets/framework. The dart’ : Failed an assertion: line 4349 pos 12: ‘! _dirty ‘: is not true.
This error indicates that we are trying to modify the Widget tree at build time. The widget’s initState method is called in the middle of the build process, so any attempt to modify the widget tree from there will fail. In our example, when the FETCH method is called, it executes notifyListeners() synchronously, causing the widget tree to change.
Similar mistakes can occur when you try to do more seemingly unrelated things. Displaying a conversation, for example, will fail for a similar reason because the context (Element) is not currently mounted in the widget tree.
Whatever you want to do, you must delay code execution until the build process is complete. In other words, you need to execute your code asynchronously. Now for our choice.
How to delay code execution in Flutter?
By researching this topic on the Internet, I’ve compiled a list of the most common recommended solutions. You can even find a few extra options, but here are the most compelling ones.
- scheduleMicrotask
Future<T>.microtask
Future<T>
Future<T>.delayed
- Timer.run
- WidgetsBinding.addPostFrameCallback
- SchedulerBinding.addPostFrameCallback
That’s a lot of options, you might say, and you’d be right. Speaking of our problems, any of them can be solved. But since we’re faced with so many choices, let’s indulge our curiosity and try to understand the differences.
Event loops and multithreading
Dart, as you probably know, is a single-threaded system. Surprisingly, your application can do more than one thing at a time, or at least it seems that way. That’s where the event loop comes in. An event Loop is literally an endless Loop that executes predetermined events (Run Loop for iOS developers). These events (or just blocks of code, if you prefer) must be lightweight, or your application will feel lagging or frozen completely. Each event, such as a button press or network response, is scheduled in an event queue and waits to be received and executed by the event loop. This design pattern is fairly common in UIs and other systems that handle any type of event. This concept can be difficult to explain in two sentences, so if you’re new to it, I suggest you watch something on the side. Don’t think too much about it, let’s take it literally. We’re talking about a simple infinite loop with a list of tasks (code blocks) scheduled to be executed, one for each iteration of the loop.
The special guest for our upcoming Dart event loop gathering is Microtask. Our Event Loop has an additional queue, called the Microtask queue. The only thing to note about this queue is that all tasks scheduled in it are executed in one iteration of the event loop before the event itself is executed.
Each iteration executes all the microtasks and then an event. The loop repeats.
Unfortunately, there’s not a lot of data on this, and the best explanation I’ve seen can be found here or in the web archive here.
With that in mind, let’s take a look at all the options listed above and see how they work and the differences between them.
The event
Anything that enters the event queue. This is the default way you schedule asynchronous tasks in Flutter. To schedule an event, we add it to the event queue, which is received by the event loop. This method is used by many Flutter mechanisms, such as I/O, gesture events, timers, etc.
The timer
Timers are the basis for asynchronous tasks in Flutter. It is used to schedule the execution of code in the event queue, with or without delay. The interesting fact from this is that if the queue is busy, your timer will never be executed, even when the time is up.
How to use.
Timer.run(() {
print("Timer");
});
Copy the code
Future<T>
andFuture<T>.delayed
A well known and widely used Dart feature. This may come as a surprise, but if you take a look under the hood, you’ll see that it’s just a wrapper around the aforementioned timer.
How to use.
Future<void> (() {print("Future Event");
});
Future<void>.delayed(Duration.zero, () {
print("Future.delayed Event");
});
Copy the code
Internal implementation (link).
factory Future(FutureOr<T> computation()) {
_Future<T> result = new _Future<T>();
Timer.run(() {
try {
result._complete(computation());
} catch(e, s) { _completeWithErrorCallback(result, e, s); }});return result;
}
Copy the code
Tiny tasks
As mentioned earlier, all scheduled microtasks are executed before the next scheduled event. It is recommended to avoid using this queue unless it is absolutely necessary to execute code asynchronously, but before the next event in the event queue. You can also think of this queue as a queue of tasks belonging to the previous event, because they will be completed before the next event. Overloading the queue can completely freeze your application because it must execute everything in the queue before it can proceed to the next iteration of its event queue, such as processing user input or even rendering the application itself. Nevertheless, there is a choice here.
scheduleMicrotask
As the name suggests, a block of code is scheduled in a microtask queue. Similar to a timer, if something goes wrong, it will crash the application.
How to use.
scheduleMicrotask(() {
print("Microtask");
});
Copy the code
Future<T>.microtask
Similar to what we saw earlier, wrapping our microtasks in a try-catch block returns execution results or errors in a nice neat way.
How to use.
Future<void>.microtask(() {
print("Microtask");
});
Copy the code
Internal implementation (link).
factory Future.microtask(FutureOr<T> computation()) {
_Future<T> result = new _Future<T>();
scheduleMicrotask(() {
try {
result._complete(computation());
} catch(e, s) { _completeWithErrorCallback(result, e, s); }});return result;
}
Copy the code
After the frame callback
While the previous two approaches involved low-level event loops, we now move into the realm of Flutter. This callback is called when the render pipe completes, so it is associated with the widget’s life cycle. When it is scheduled, it is called only once, not on every frame. Using the addPostFrameCallback method, you can schedule one or more callbacks to be executed after the frame is built. All scheduled callbacks are executed at the end of the frame in the order they were added. When this callback is invoked, the widget build process is ensured to complete. With some smoke and mirrors, you can even access the widget’s layout (RenderBox), such as its size, and do various other unrecommended hacks. The callback itself will run in a normal queue of events, almost all of which Flutter uses by default.
Scheduler binding
This is a mixin responsible for drawing callbacks that implements the method we are interested in.
How to use.
SchedulerBinding.instance.addPostFrameCallback((_) {
print("SchedulerBinding");
});
Copy the code
WidgetsBinding
I purposely included this one because it is often mentioned along with SchedulerBinding. It inherits this method from the SchedulerBinding, and there are additional methods that are irrelevant to our topic. In general, whether you use a SchedulerBinding or a WidgetsBinding, both will execute exactly the same code in a SchedulerBinding.
How to use.
WidgetsBinding.instance.addPostFrameCallback((_) {
print("WidgetsBinding");
});
Copy the code
Put our knowledge into practice
Since we’ve been doing a lot of theoretical stuff today, I highly recommend you play around a little bit to make sure you get it right. We can use the following code in initState before and try to predict its execution order, which doesn’t seem like an easy thing to do.
SchedulerBinding.instance.addPostFrameCallback((_) {
print("SchedulerBinding");
});
WidgetsBinding.instance.addPostFrameCallback((_) {
print("WidgetsBinding");
});
Timer.run(() {
print("Timer");
});
scheduleMicrotask(() {
print("scheduleMicrotask");
});
Future<void>.microtask(() {
print("Future Microtask");
});
Future<void> (() {print("Future");
Future<void>.microtask(() {
print("Microtask from Event");
});
});
Future<void>.delayed(Duration.zero, () {
print("Future.delayed");
Future<void>.microtask(() {
print("Microtask from Future.delayed");
});
Copy the code
conclusion
Now that we know so much detail, you can make a thoughtful decision about how to arrange your code. As a rule of thumb, if you need your context or something related to Layout or UI, use addPostFrameCallback. In any other case, scheduling in the event queue with Future
or Future
.delayed should be sufficient. Microtask queues are a very niche thing that you’ll probably never come across, but it’s worth knowing about. Of course, if your task is heavy, you’ll want to consider creating an Isolate, which, as you might have guessed, will be communicated by event queues. But that’s the subject of another article. Thank you for your time. See you next time.
Oleksandrkirichenko.com, February 7, 2021
Translation via www.DeepL.com/Translator (free version)