跳至主要內容

在背景中解析 JSON

預設情況下,Dart 應用程式的所有工作都在單一執行緒上進行。在許多情況下,此模型簡化了程式碼編寫,而且速度夠快,不會導致應用程式效能不佳或動畫卡頓,通常稱為「jank」。

但是,您可能需要執行昂貴的計算,例如解析非常大的 JSON 文件。如果這項工作花費超過 16 毫秒,您的使用者就會體驗到 jank。

為了避免 jank,您需要在背景執行像這樣的昂貴計算。在 Android 上,這意味著在不同的執行緒上排程工作。在 Flutter 中,您可以使用單獨的 Isolate。此食譜使用以下步驟

  1. 新增 http 套件。
  2. 使用 http 套件發出網路請求。
  3. 將回應轉換為照片列表。
  4. 將此工作移至獨立的 Isolate。

1. 新增 http 套件

#

首先,將 http 套件新增至您的專案。http 套件讓執行網路請求(例如從 JSON 端點擷取資料)變得更容易。

若要將 http 套件新增為依賴項,請執行 flutter pub add

flutter pub add http

2. 發出網路請求

#

此範例說明如何使用 http.get() 方法,從 JSONPlaceholder REST API 擷取包含 5000 個照片物件的大型 JSON 文件。

dart
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

dart
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. 建立一個 parsePhotos() 函式,將回應主體轉換為 List<Photo>
  2. fetchPhotos() 函式中使用 parsePhotos() 函式。
dart
// A function that converts a response body into a List<Photo>.
List<Photo> parsePhotos(String responseBody) {
  final parsed =
      (jsonDecode(responseBody) as List).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'));

  // Synchronously run parsePhotos in the main isolate.
  return parsePhotos(response.body);
}

4. 將此工作移至獨立的 Isolate

#

如果您在速度較慢的裝置上執行 fetchPhotos() 函式,您可能會注意到應用程式在解析和轉換 JSON 時會暫時凍結。這就是 jank,您想要擺脫它。

您可以使用 Flutter 提供的 compute() 函式,將解析和轉換移至背景 Isolate 來消除 jank。compute() 函式會在背景 Isolate 中執行昂貴的函式並傳回結果。在此範例中,在背景執行 parsePhotos() 函式。

dart
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 的注意事項

#

Isolate 會透過來回傳遞訊息進行溝通。這些訊息可以是原始值,例如 nullnumbooldoubleString,或是簡單的物件,例如此範例中的 List<Photo>

如果您嘗試在 Isolate 之間傳遞更複雜的物件(例如 Futurehttp.Response),您可能會遇到錯誤。

作為替代解決方案,請查看用於背景處理的 worker_managerworkmanager 套件。

完整範例

#
dart
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) as List).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 StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late Future<List<Photo>> futurePhotos;

  @override
  void initState() {
    super.initState();
    futurePhotos = fetchPhotos(http.Client());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: FutureBuilder<List<Photo>>(
        future: futurePhotos,
        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