在後台處理 JSON 資料解析

Dart 應用通常只會在單執行緒中處理它們的工作。並且在大多數情況中,這種模式不但簡化了程式碼而且速度也夠快,基本不會出現像動畫卡頓以及效能不足這種「不靠譜」的問題。

但是,當你需要進行一個非常複雜的計算時,例如解析一個巨大的 JSON 文件。如果這項工作耗時超過了 16 毫秒,那麼你的使用者就會感受到掉幀。

為了避免掉幀,像上面那樣消耗效能的計算就應該放在後台處理。在 Android 平臺上,這意味著你需要在不同的執行緒中進行排程工作。而在 Flutter 中,你可以使用一個單獨的 Isolate

使用步驟

  1. 新增 http 這個 package;

  2. 使用 http package 發起一個網路請求;

  3. 將響應轉換成一列照片;

  4. 將這個工作移交給一個單獨的 isolate。

1. 新增 http

首先,在你的專案中新增 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>>,我們需要以下兩點更新:

  1. 建立一個可以將回應內文轉換成 List<Photo> 的方法:parsePhotos()

  2. 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 透過來回傳遞訊息來交流。這些訊息可以是任何值,它們可以是 nullnumbooldouble 或者 String,哪怕是像這個例子中的 List<Photo> 這樣簡單物件都沒問題。

當你試圖傳遞更復雜的物件時,你可能會遇到錯誤,例如在 isolates 之間的 Future 或者 http.Response

與此同時,後臺處理序的其他解決方案是使用 worker_managerworkmanager 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);
      },
    );
  }
}

Isolate demo