線程和異步
編寫異步代碼
Dart采用單線程執行模型,支持Isolates(在另一個線程上運行Dart代碼)、事件循環和異步編程。除非生成一個Isolates,否則Dart代碼將在主UI線程中運行,并由事件循環驅動。Flutter的事件循環相當于iOS的主線程上的RunLoop。
Dart的單線程模型,不代表阻塞型的操作都會導致UI卡頓。實際上可以采用Dart語言提供的異步功能比如async/await來執行異步的操作。
因為要請求網絡,所以添加http模塊
$ fltter pub add http
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';void main() {runApp(const MainApp());
}class MainApp extends StatelessWidget {const MainApp({super.key}); Widget build(BuildContext context) {return const MaterialApp(home: ThreadSample());}
}class ThreadSample extends StatefulWidget {const ThreadSample({super.key}); State<ThreadSample> createState() => _ThreadSampleState();
}class _ThreadSampleState extends State<ThreadSample> {List<Map<String, Object?>> data = [];/// 1. 初始化_ThreadSampleState Widget的狀態void initState() {super.initState();/// 2.加載數據loadData();}Future<void> loadData() async {/// 3. 發起異步請求final Uri dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');final http.Response response = await http.get(dataURL);/// 4. 等響應結束后調用setState() 更新data 觸發build方法setState(() {data = (jsonDecode(response.body) as List).cast<Map<String, Object?>>();});}Widget getRow(int index) {return Padding(padding: const EdgeInsets.all(10),child: Text('Row ${data[index]['title']}'),);} Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('線程與異步示例')),// 5. 顯示列表,長度為data.length,內容通過getRow方法返回data的子元素body: ListView.builder(itemCount: data.length,itemBuilder: (context, index) {return getRow(index);},),);}
}
切到后臺線程
因為Flutter是單線程模型,不需要考慮線程管理相關的問題。在執行I/O密集型的操作時,比如訪問磁盤或網絡,可以使用async/await,但是當在執行CPU計算密集型的操作時,則應該將其移到獨立線程(Isolate)以避免阻塞事件循環。
Isolates 是獨立的執行線程,它們與主線程內存堆不共享任何內存。這意味著你無法訪問主線程中的變量,或通過調用 setState() 來更新用戶界面。
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;void main() {runApp(const SampleApp());
}class SampleApp extends StatelessWidget {const SampleApp({super.key}); Widget build(BuildContext context) {return const MaterialApp(title: 'Sample App', home: SampleAppPage());}
}class SampleAppPage extends StatefulWidget {const SampleAppPage({super.key}); State<SampleAppPage> createState() => _SampleAppPageState();
}class _SampleAppPageState extends State<SampleAppPage> {List<Map<String, Object?>> data = [];void initState() {super.initState();/// 主1. 加載數據loadData();}bool get showLoadingDialog => data.isEmpty;Future<void> loadData() async {/// Opens a long-lived port for receiving messages./// 打開端口用于接收數據final ReceivePort receivePort = ReceivePort();/// 主2.Isolate開啟子線程/// The [entryPoint] function must be able to be called with a single/// argument, that is, a function which accepts at least one positional/// parameter and has at most one required positional parameter.////// The entry-point function is invoked in the new isolate with [message]/// as the only argument./// 第一個參數:至少包含一個參數的函數指針,這里關聯的是dataLoader,參數是SendPort////// [message] must be sendable between isolates. Objects that cannot be sent/// include open files and sockets (see [SendPort.send] for details). Usually/// the initial [message] contains a [SendPort] so that the spawner and/// spawnee can communicate with each other./// 第二個參數: 不同Isolate之間傳遞的數據,通常初始化時傳的message包含一個SendPort////// receivePort.sendPort/// [SendPort]s are created from [ReceivePort]s./// Any message sent through a [SendPort] is delivered to its corresponding [ReceivePort]./// There might be many [SendPort]s for the same [ReceivePort]./// 通過SendPort發送的消息會傳送給關聯的ReceivePortawait Isolate.spawn(dataLoader, receivePort.sendPort);/// 主3. first是一個Future,它會在接收到第一個消息時完成/// 一旦收到第一個消息,它就會關閉ReceivePort,并且不再監聽其它消息/// 適用于只接收單個消息的情況final SendPort sendPort = await receivePort.first as SendPort;try {/// 主4. 使用await調用sendReceivefinal List<Map<String, dynamic>> msg = await sendReceive(sendPort,'https://jsonplaceholder.typicode.com/posts',);/// 主5.設置數據,通知Flutter刷新UIsetState(() {data = msg;});} catch (e) {print('Error in loadData:$e');}}// 子1. 執行子線程上的函數static Future<void> dataLoader(SendPort sendPort) async {// 子2.打開端口接收數據final ReceivePort port = ReceivePort();/// 子3. 發送自己的接收端口sendPort.send(port.sendPort);/// 子4:等待消息await for (final dynamic msg in port) {/// 子5: 接收到url + 主線程的接收端口final String url = msg[0] as String;final SendPort replyTo = msg[1] as SendPort;/// 子6: 發起網絡請求final Uri dataURL = Uri.parse(url);final http.Response response = await http.get(dataURL);/// 下面這種寫法在sendReceive會報/// Unhandled/// Exception: type 'Future<dynamic>' is not a subtype of type/// 'Future<List<Map<String, dynamic>>>'////// replyTo.send(jsonDecode(response.body) as List<Map<String, dynamic>>);/// 因為Dart在運行時無法檢查Future<T>中的T,直接轉換Future的泛型參數會失敗/// 強制類型轉換final data = jsonDecode(response.body) as List;final typedata = data.cast<Map<String, dynamic>>();/// 子7: 將網絡請求的結果發送到主線程replyTo.send(typedata);}}Future<dynamic> sendReceive(SendPort port, String msg) {// 主5.創建接收數據的端口final ReceivePort response = ReceivePort();// Sends an asynchronous [message] through this send port, to its corresponding [ReceivePort].// 主6. 主線程異步發送url + 通知其它線程接收端口port.send(<dynamic>[msg, response.sendPort]);return response.first;}Widget getBody() {/// 數據為空顯示進度條bool showLoadingDialog = data.isEmpty;if (showLoadingDialog) {return getProgressDialog();} else {return getListView();}}Widget getProgressDialog() {return const Center(child: CircularProgressIndicator());}ListView getListView() {return ListView.builder(itemCount: data.length,itemBuilder: (context, position) {return getRow(position);},);}Widget getRow(int i) {return Padding(padding: const EdgeInsets.all(10),child: Text("Row ${data[i]["title"]}"),);} Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('Sample App')),body: getBody(),);}
}
錯誤
[ERROR:flutter/runtime/dart_vm_initializer.cc(40)] Unhandled Exception: ClientException with SocketException: Failed host lookup: 'jsonplaceholder.typicode.com'
[ERROR:flutter/runtime/dart_vm_initializer.cc(40)] Unhandled Exception: ClientException with SocketException: Failed host lookup: ‘jsonplaceholder.typicode.com’ (OS Error: nodename nor servname provided, or not known, errno = 8), uri=https://jsonplaceholder.typicode.com/posts
首次啟動需要同意網絡權限,看報錯是DNS找不到域名,所以還是網絡問題,在手機上授權后再重新用flutter運行工程能恢復
參考
1.給 UIKit 開發者的 Flutter 指南
2.flutter 中 ReceivePort 的 first 和 listen