This is the 13th day of my participation in Gwen Challenge
preface
Most of the front-end development frameworks we are familiar with are event-driven. Event-driven means that you must have event loops and event queues in your program. An event loop constantly retrieves and processes events from the event queue. This means that your program must support asynchrony. In Android the structure is Looper/Handler; In iOS, RunLoop; In JavaScript, it’s an Event Loop. Dart has a single-threaded execution model that supports Isolate (a method of running Dart code on another thread), an event loop, and asynchronous programming. Unless you create an Isolate yourself, your Dart code always runs on the main UI thread and is driven by an Event loop. The event loop of a Flutter is similar to the main loop in iOS: the Looper is attached to the main thread.
1. Flutter asynchronous code
Dart has a single-threaded execution model that supports Isolate (a method of running Dart code on another thread), an event loop, and asynchronous programming. Unless you create an Isolate yourself, your Dart code always runs on the main UI thread and is driven by an Event loop. The event loop of a Flutter is similar to the main loop in iOS: the Looper is attached to the main thread. The Dart event loop is shown below. It’s basically the same as JavaScript. There are two queues in the loop. One is the MicroTask queue and the other is the Event queue.
Event queues contain external events, such as I/O, Timer, draw events, and so on.
The microtask queue contains Dart microtasks, which are scheduled using scheduleMicrotask.
1.1 Asynchronous Execution
So how do you make your code execute asynchronously in Dart? It is simple to place code to be executed asynchronously in a microtask queue or event queue. ScheduleMicrotask can be called to make code execute asynchronously as a microtask.
scheduleMicrotask((){
print('a microtask');
});
Copy the code
You can call timer.run to make your code execute asynchronously as an Event.
Timer.run((){
print('a event');
});
Copy the code
Ok, now you know how to make your Dart code execute asynchronously. It doesn’t seem too complicated, but you need to be clear about the order in which your asynchronous code executes. This is one of the most common front-end interview questions. To take a simple example, does the following code say “executed”?
main() {
Timer.run(() { print("executed"); });
foo() {
scheduleMicrotask(foo);
}
foo();
}
Copy the code
The answer is no, because there will always be a foo in the microtask queue. The Event Loop has no chance to process the Event queue. There are more complex examples where a lot of asynchronous code is mixed and nested and asked what the order of execution is, which requires careful analysis according to the Event Loop rule above. Just like JS, it’s easy to fall into “Callback hell” using only Callback functions for asynchracy. To avoid this problem, JS introduced promises. Similarly, Dart introduces Future.
1.2 the Dart asynchronous
Dart’s single-threaded model doesn’t mean that you have to write code that runs as a blocking operation, blocking the UI. Instead, asynchronous operations can be implemented using asynchronous tools provided by the Dart language, such as async/await. For example, you can use async/await to let Dart do the heavy lifting for you, writing network request code without hanging the UI:
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
Copy the code
This is equivalent to runOnUiThread in Android. The full portion of the above code snippet can be found in the course source code. Once the network request for await completes, update the UI by calling setState(), which triggers a rebuild of the widget subtree and updates the associated data.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App', theme: ThemeData( primarySwatch: Colors.blue, ), home: SampleAppPage(), ); }}class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List widgets = [];
@override
void initState() {
super.initState();
loadData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
}));
}
Widget getRow(int i) {
return Padding(
padding: EdgeInsets.all(10.0),
child: Text("Row ${widgets[i]["title"]}")); } loadData()async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL); setState(() { widgets = json.decode(response.body); }); }}Copy the code
2. Asynchronous implementation of Flutter
The asynchronous mechanism of flutter involves keywords such as Future, async, await, async, sync, Iterator, Iterable, Stream, Timer, etc. The more common collocation are async, await and Future.
2.1 the Future
There are several functions in the future:
-
Then: Asynchronous operation logic is written here.
-
WhenComplete: Callback when asynchronous completion.
-
CatchError: callback to catch an exception or asynchronous error.
In our daily development, we use it like this. First, we add async keyword after our function to indicate asynchronous operation, and then write the return value of the function as Future. Then we can new a Future, and add an await keyword before the logic, and then we can use future.then and other operations. Here is an example operation, where the return value is null for demonstration purposes. In normal development, if the request network returns JSON, we can write the generic as String; Generics can also be entity/domain classes, but do json to entity classes.
// For example
// Simulate asynchronously loading user information
Future _getUserInfo() async{
await new Future.delayed(new Duration(milliseconds: 3000));
return "I'm an example of a public account.";
}
// Load the user information and print the time to see the order
Future _loadUserInfo() async{
print("_loadUserInfo:The ${new DateTime.now()}");
print(await _getUserInfo());
print("_loadUserInfo:The ${new DateTime.now()}");
}
@override
void initState(){
// Initialize the state and load the user information
print("initState:The ${new DateTime.now()}");
_loadUserInfo();
print("initState:The ${new DateTime.now()}");
super.initState();
}
Copy the code
A Future represents something that will happen “in the Future,” from which a value can be taken. When a method returns something for a Future, two things happen: the method queues something and returns an unfinished Future
When the event is complete, the Future state becomes completed, and the return value of the event can be retrieved.
To get to this “return value”, there are two ways:
-
Use async with await
-
Use the API provided by the Future
The async await keyword is so familiar that you’ll see it in almost every object-oriented language, and it’s also a dart language feature that lets you write “asynchronous” code that looks like “synchronous.”
2.2 Flutter Asynchronous Request
Because Flutter is single-threaded and runs an event loop (just like Node.js), you don’t have to worry about thread management or generating background threads. If you are doing I/O operations such as accessing disks or network requests, it is safe to do so using async/await. If you need to do busy computationally intensive tasks for the CPU, you need to use Isolate to avoid blocking event loops. In Android, you usually create an AsyncTask when accessing a network resource. When you need a time-consuming background task, you usually need IntentService. Flutter doesn’t have to be so tedious. For I/O operations, declare a method as an asynchronous method with the keyword async and wait for the asynchronous method to complete with the await keyword:
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
Copy the code
In Android, when you inherit AsyncTask, you typically override three methods, OnPreExecute, doInBackground, and onPostExecute. There is no equivalent of this pattern in Flutter because you just need to await the completion of the function and Dart’s event loop will take care of the rest. This is typical for I/O operations such as network requests, database access, and so on. However, sometimes you need to process a lot of data, which can cause your UI to hang. In Flutter, the Isolate is used to take advantage of multi-core cpus to handle long running or computationally intensive tasks. The Isolate is a separate running thread that does not share memory with the main thread’s memory heap. This means you can’t access variables in the main thread, or use setState() to update the UI. As their name suggests, the Isolate cannot share memory. The following example shows how a simple Isolate can return data to the main thread to update the UI:
import 'dart:isolate'; . loadData()async {
// Open ReceivePort to receive incoming messages
ReceivePort receivePort = ReceivePort();
// Create and generate an Isolate that shares the same code as the current Isolate
await Isolate.spawn(dataLoader, receivePort.sendPort);
// The first element of the stream
SendPort sendPort = await receivePort.first;
// The listener is closed after the first element of the stream is received, so you need to open a new ReceivePort to receive the incoming message
ReceivePort response = ReceivePort();
// Send asynchronous [messages] from this port to its corresponding ReceivePort ①. The "messages" refer to the sent parameter ②.
sendPort.send(
["https://jsonplaceholder.typicode.com/posts", response.sendPort]);
// Get the data from the port ③
List msg = await response.first;
setState(() {
widgets = msg;
});
}
// The isolate's entry function, which will be called on the new ISOLATE. Spawn will be called with the isolate. spawn message as the only argument
static dataLoader(SendPort sendPort) async {
// Open ReceivePort① to receive incoming messages
ReceivePort port = ReceivePort();
// Notify other networks of the monitored ports of this ISOLATE
sendPort.send(port.sendPort);
/ / for other port to send an asynchronous message MSG (2) - > [. "https://jsonplaceholder.typicode.com/posts", the response sendPort]
await for (var msg in port) {
// equivalent to List MSG = await port.first;
String data = msg[0];
SendPort replyTo = msg[1];
String dataURL = data;
http.Response response = await http.get(dataURL);
// The corresponding ReceivePort sends the parsed JSON datareplyTo.send(json.decode(response.body)); }}Copy the code
Here, the dataLoader() is an Isolate running on its own independent thread of execution. With Isolate, you can perform CPU intensive tasks (parsing a huge JSON, which can be time consuming) or computationally intensive mathematical operations such as encryption or signal processing. For example, run the full example below:
import 'dart:convert';
import 'dart:isolate';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App', theme: ThemeData( primarySwatch: Colors.blue, ), home: SampleAppPage(), ); }}class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List widgets = [];
@override
void initState() {
super.initState();
loadData();
}
showLoadingDialog() {
if (widgets.length == 0) {
return true;
}
return false;
}
getBody() {
if (showLoadingDialog()) {
return getProgressDialog();
} else {
return getListView();
}
}
getProgressDialog() {
return Center(child: CircularProgressIndicator());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: getBody());
}
ListView getListView() => ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
});
Widget getRow(int i) {
return Padding(
padding: EdgeInsets.all(10.0),
child: Text("Row ${widgets[i]["title"]}"));
}
loadData() async {
// Open ReceivePort to receive incoming messages
ReceivePort receivePort = ReceivePort();
// Create and generate an Isolate that shares the same code as the current Isolate
await Isolate.spawn(dataLoader, receivePort.sendPort);
// The first element of the stream
SendPort sendPort = await receivePort.first;
// The listener is closed after the first element of the stream is received, so you need to open a new ReceivePort to receive the incoming message
ReceivePort response = ReceivePort();
// Send asynchronous [messages] from this port to its corresponding ReceivePort ①. The "messages" refer to the sent parameter ②.
sendPort.send(
["https://jsonplaceholder.typicode.com/posts", response.sendPort]);
// Get the data from the port ③
List msg = await response.first;
setState(() {
widgets = msg;
});
}
// The isolate's entry function, which will be called on the new ISOLATE. Spawn will be called with the isolate. spawn message as the only argument
static dataLoader(SendPort sendPort) async {
// Open ReceivePort① to receive incoming messages
ReceivePort port = ReceivePort();
// Notify other networks of the monitored ports of this ISOLATE
sendPort.send(port.sendPort);
/ / for other port to send an asynchronous message MSG (2) - > [. "https://jsonplaceholder.typicode.com/posts", the response sendPort]
await for (var msg in port) {
// equivalent to List MSG = await port.first;
String data = msg[0];
SendPort replyTo = msg[1];
String dataURL = data;
http.Response response = await http.get(dataURL);
// The corresponding ReceivePort sends the parsed JSON datareplyTo.send(json.decode(response.body)); }}}Copy the code
3, Flutter network request
Making web requests using the popular HTTP package in Flutter is very simple. It abstracts out web requests that you might need to do yourself, making it easy to initiate. To use the HTTP package, add the following dependencies to pubspec.yaml:
dependencies:
...
http: ^0.12. 0+1
Copy the code
To initiate a network request, use await in the async method http.get() :
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' ashttp; [...]. loadData()async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL); setState(() { widgets = json.decode(response.body); }); }}Copy the code
Once the result has been obtained, you can tell Flutter to update its state by calling setState. SetState will update the UI with the result of the network call.
Add a request progress indicator
In Android, we often use the ProgressBar when running time-consuming tasks in the background. In iOS, we usually use UIProgressView when running time-consuming tasks in the background. There is also a corresponding widget on Flutter called the ProgressIndicator. A Boolean flag controls whether progress is displayed. Tells Flutter to update its status at the start of a task and hides it after it finishes. In the following example, the build function is split into three functions. If showLoadingDialog() is true (when widgets.length == 0), render the ProgressIndicator. Otherwise, render the ListView when the data is returned from the network request:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App', theme: ThemeData( primarySwatch: Colors.blue, ), home: SampleAppPage(), ); }}class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
List widgets = [];
@override
void initState() {
super.initState();
loadData();
}
showLoadingDialog() {
return widgets.length == 0;
}
getBody() {
if (showLoadingDialog()) {
return getProgressDialog();
} else {
return getListView();
}
}
getProgressDialog() {
return Center(child: CircularProgressIndicator());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: getBody());
}
ListView getListView() => ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
});
Widget getRow(int i) {
return Padding(padding: EdgeInsets.all(10.0), child: Text("Row ${widgets[i]["title"]}"));
}
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL); setState(() { widgets = json.decode(response.body); }); }}Copy the code
conclusion
To sum up, Dart can be programmed asynchronously in two ways: Future and Stream. Future is equivalent to a 40m machete and Stream is equivalent to a bundle of 40m machetes. Dart provides the keywords async and await, which are common and convenient daggers that we often use. Async and await are familiar to the front end.
When an async operation is encountered, we shall await it in the queue of delayed operation. The part that does not need to be delayed shall be executed first, and the part that needs to be delayed shall be processed at last. Return await directly in the request method.. ., which actually returns a deferred Future object. Two points to note here: the await keyword must be used inside async functions; Calling async functions must use the await keyword.