在後台處理 JSON 資料解析
Dart 應用通常只會在單執行緒中處理它們的工作。並且在大多數情況中,這種模式不但簡化了程式碼而且速度也夠快,基本不會出現像動畫卡頓以及效能不足這種「不靠譜」的問題。
但是,當你需要進行一個非常複雜的計算時,例如解析一個巨大的 JSON 文件。如果這項工作耗時超過了 16 毫秒,那麼你的使用者就會感受到掉幀。
為了避免掉幀,像上面那樣消耗效能的計算就應該放在後台處理。在 Android 平臺上,這意味著你需要在不同的執行緒中進行排程工作。而在 Flutter 中,你可以使用一個單獨的 Isolate。
使用步驟
-
新增
http
這個 package; -
使用
http
package 發起一個網路請求; -
將響應轉換成一列照片;
-
將這個工作移交給一個單獨的 isolate。
http
包
1. 新增 首先,在你的專案中新增 http
這個 package,
http
package 會讓網路請求變的像從 JSON 端點獲取資料一樣簡單。
dependencies:
http: <latest_version>
2. 發起一個網路請求
在這個例子中,你將會使用 http.get()
方法透過
JSONPlaceholder REST API 獲取到一個包含
5000 張圖片物件的超大 JSON 文件。
Future<http.Response> fetchPhotos(http.Client client) async {
return client.get(Uri.parse('https://jsonplaceholder.typicode.com/photos'));
}
3. 解析並將 json 轉換成一列圖片
接下來,根據 獲取網路資料 的說明,為了讓接下來的資料處理更簡單,你需要將 http.Response
轉換成一列 Dart 物件。
Photo
類
建立一個 首先,建立一個包含圖片資料的 Photo
類別。還需要一個 fromJson
的工廠方法,使得透過 json 建立 Photo
變的更加方便。
class Photo {
final int albumId;
final int id;
final String title;
final String url;
final String thumbnailUrl;
const Photo({
required this.albumId,
required this.id,
required this.title,
required this.url,
required this.thumbnailUrl,
});
factory Photo.fromJson(Map<String, dynamic> json) {
return Photo(
albumId: json['albumId'] as int,
id: json['id'] as int,
title: json['title'] as String,
url: json['url'] as String,
thumbnailUrl: json['thumbnailUrl'] as String,
);
}
}
將響應轉換成一列圖片
現在,為了讓 fetchPhotos()
方法可以返回一個
Future<List<Photo>>
,我們需要以下兩點更新:
-
建立一個可以將回應內文轉換成
List<Photo>
的方法:parsePhotos()
-
在
fetchPhotos()
方法中使用parsePhotos()
方法
// A function that converts a response body into a List<Photo>.
List<Photo> parsePhotos(String responseBody) {
final parsed = jsonDecode(responseBody).cast<Map<String, dynamic>>();
return parsed.map<Photo>((json) => Photo.fromJson(json)).toList();
}
Future<List<Photo>> fetchPhotos(http.Client client) async {
final response = await client
.get(Uri.parse('https://jsonplaceholder.typicode.com/photos'));
// Use the compute function to run parsePhotos in a separate isolate.
return parsePhotos(response.body);
}
4. 將這部分工作移交到單獨的 isolate 中
如果你在一臺很慢的手機上執行 fetchPhotos()
函式,你或許會注意到應用會有點卡頓,因為它需要解析並轉換 json。顯然這並不好,所以你要避免它。
那麼我們究竟可以做什麼呢?那就是透過 Flutter 提供的 compute()
方法將解析和轉換的工作移交到一個後臺 isolate 中。這個 compute()
函式可以在後台 isolate 中運行復雜的函式並返回結果。在這裡,我們就需要將 parsePhotos()
方法放入後臺。
Future<List<Photo>> fetchPhotos(http.Client client) async {
final response = await client
.get(Uri.parse('https://jsonplaceholder.typicode.com/photos'));
// Use the compute function to run parsePhotos in a separate isolate.
return compute(parsePhotos, response.body);
}
使用 Isolates 需要注意的地方
Isolates 透過來回傳遞訊息來交流。這些訊息可以是任何值,它們可以是 null
、num
、bool
、double
或者 String
,哪怕是像這個例子中的 List<Photo>
這樣簡單物件都沒問題。
當你試圖傳遞更復雜的物件時,你可能會遇到錯誤,例如在 isolates 之間的 Future
或者 http.Response
。
與此同時,後臺處理序的其他解決方案是使用
worker_manager
或 workmanager
package。
完整範例
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
Future<List<Photo>> fetchPhotos(http.Client client) async {
final response = await client
.get(Uri.parse('https://jsonplaceholder.typicode.com/photos'));
// Use the compute function to run parsePhotos in a separate isolate.
return compute(parsePhotos, response.body);
}
// A function that converts a response body into a List<Photo>.
List<Photo> parsePhotos(String responseBody) {
final parsed = jsonDecode(responseBody).cast<Map<String, dynamic>>();
return parsed.map<Photo>((json) => Photo.fromJson(json)).toList();
}
class Photo {
final int albumId;
final int id;
final String title;
final String url;
final String thumbnailUrl;
const Photo({
required this.albumId,
required this.id,
required this.title,
required this.url,
required this.thumbnailUrl,
});
factory Photo.fromJson(Map<String, dynamic> json) {
return Photo(
albumId: json['albumId'] as int,
id: json['id'] as int,
title: json['title'] as String,
url: json['url'] as String,
thumbnailUrl: json['thumbnailUrl'] as String,
);
}
}
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
const appTitle = 'Isolate Demo';
return const MaterialApp(
title: appTitle,
home: MyHomePage(title: appTitle),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: FutureBuilder<List<Photo>>(
future: fetchPhotos(http.Client()),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Center(
child: Text('An error has occurred!'),
);
} else if (snapshot.hasData) {
return PhotosList(photos: snapshot.data!);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
),
);
}
}
class PhotosList extends StatelessWidget {
const PhotosList({super.key, required this.photos});
final List<Photo> photos;
@override
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: photos.length,
itemBuilder: (context, index) {
return Image.network(photos[index].thumbnailUrl);
},
);
}
}