默认情况下,Dart 应用的所有工作都在单个线程上进行。在许多情况下,这种模型简化了编码,并且速度足够快,不会导致应用性能不佳或动画卡顿(通常称为“jank”)。

但是,你可能需要执行开销较大的计算,例如解析一个非常大的 JSON 文档。如果此工作耗时超过 16 毫秒,你的用户就会遇到卡顿 (jank)。

为了避免卡顿 (jank),你需要使用单独的 Isolate 在后台执行此类开销较大的计算。本菜谱 (recipe) 包含以下步骤:

  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 并将其转换为照片列表

#

接下来,遵循 从互联网获取数据 菜谱 (recipe) 中的指导,将 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);
}

使用隔离区 (isolate) 的注意事项

#

隔离区 (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