跳至主要内容

从互联网获取数据

大多数应用程序都需要从互联网获取数据。幸运的是,Dart 和 Flutter 提供了工具(例如 http 包)来完成此类工作。

此示例使用以下步骤

  1. 添加 http 包。
  2. 使用 http 包发起网络请求。
  3. 将响应转换为自定义 Dart 对象。
  4. 使用 Flutter 获取和显示数据。

1. 添加 http

#

http 包提供了从互联网获取数据的最简单方法。

要将 http 包添加为依赖项,请运行 flutter pub add

flutter pub add http

导入 http 包。

dart
import 'package:http/http.dart' as http;

如果您要部署到 Android,请编辑您的 AndroidManifest.xml 文件以添加 Internet 权限。

xml
<!-- Required to fetch data from the internet. -->
<uses-permission android:name="android.permission.INTERNET" />

同样,如果您要部署到 macOS,请编辑您的 macos/Runner/DebugProfile.entitlementsmacos/Runner/Release.entitlements 文件以包含网络客户端权限。

xml
<!-- Required to fetch data from the internet. -->
<key>com.apple.security.network.client</key>
<true/>

2. 发起网络请求

#

此示例介绍了如何使用 JSONPlaceholderhttp.get() 方法获取示例专辑。

dart
Future<http.Response> fetchAlbum() {
  return http.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));
}

http.get() 方法返回一个包含 ResponseFuture

  • Future 是用于处理异步操作的核心 Dart 类。Future 对象表示将来某个时间将可用的潜在值或错误。
  • http.Response 类包含从成功 http 调用接收到的数据。

3. 将响应转换为自定义 Dart 对象

#

虽然发起网络请求很容易,但使用原始 Future<http.Response> 并不方便。为了简化您的工作,请将 http.Response 转换为 Dart 对象。

创建 Album

#

首先,创建一个 Album 类,其中包含网络请求中的数据。它包括一个工厂构造函数,该构造函数从 JSON 创建 Album

使用 模式匹配 转换 JSON 只是一个选项。有关更多信息,请参阅关于 JSON 和序列化 的完整文章。

dart
class Album {
  final int userId;
  final int id;
  final String title;

  const Album({
    required this.userId,
    required this.id,
    required this.title,
  });

  factory Album.fromJson(Map<String, dynamic> json) {
    return switch (json) {
      {
        'userId': int userId,
        'id': int id,
        'title': String title,
      } =>
        Album(
          userId: userId,
          id: id,
          title: title,
        ),
      _ => throw const FormatException('Failed to load album.'),
    };
  }
}

http.Response 转换为 Album

#

现在,使用以下步骤更新 fetchAlbum() 函数以返回 Future<Album>

  1. 使用 dart:convert 包将响应正文转换为 JSON Map
  2. 如果服务器确实返回状态代码为 200 的 OK 响应,则使用 fromJson() 工厂方法将 JSON Map 转换为 Album
  3. 如果服务器未返回状态代码为 200 的 OK 响应,则抛出异常。(即使在“404 未找到”服务器响应的情况下,也抛出异常。不要返回 null。这在检查 snapshot 中的数据时非常重要,如下所示。)
dart
Future<Album> fetchAlbum() async {
  final response = await http
      .get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));

  if (response.statusCode == 200) {
    // If the server did return a 200 OK response,
    // then parse the JSON.
    return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
  } else {
    // If the server did not return a 200 OK response,
    // then throw an exception.
    throw Exception('Failed to load album');
  }
}

万岁!现在您拥有了一个从互联网获取专辑的函数。

4. 获取数据

#

initState()didChangeDependencies() 方法中调用 fetchAlbum() 方法。

initState() 方法恰好调用一次,然后不再调用。如果您希望有权在响应于 InheritedWidget 更改时重新加载 API,请将调用放入 didChangeDependencies() 方法中。有关更多详细信息,请参阅 State

dart
class _MyAppState extends State<MyApp> {
  late Future<Album> futureAlbum;

  @override
  void initState() {
    super.initState();
    futureAlbum = fetchAlbum();
  }
  // ···
}

此 Future 用于下一步。

5. 显示数据

#

要在屏幕上显示数据,请使用 FutureBuilder 小部件。FutureBuilder 小部件随 Flutter 提供,可轻松处理异步数据源。

您必须提供两个参数

  1. 您要使用的 Future。在本例中,是 fetchAlbum() 函数返回的 future。
  2. 一个 builder 函数,它告诉 Flutter 根据 Future 的状态(加载、成功或错误)渲染什么内容。

请注意,只有当快照包含非空数据值时,snapshot.hasData 才会返回 true

因为 fetchAlbum 只能返回非空值,所以即使在“404 未找到”服务器响应的情况下,该函数也应该抛出异常。抛出异常会将 snapshot.hasError 设置为 true,这可用于显示错误消息。

否则,将显示加载动画。

dart
FutureBuilder<Album>(
  future: futureAlbum,
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      return Text(snapshot.data!.title);
    } else if (snapshot.hasError) {
      return Text('${snapshot.error}');
    }

    // By default, show a loading spinner.
    return const CircularProgressIndicator();
  },
)

为什么在 initState() 中调用 fetchAlbum()?

#

虽然很方便,但不建议在 build() 方法中放置 API 调用。

每次需要更改视图中的任何内容时,Flutter 都会调用 build() 方法,这种情况出乎意料地频繁。如果将 fetchAlbum() 方法放在 build() 中,则会在每次重建时重复调用它,导致应用程序速度变慢。

fetchAlbum() 结果存储在状态变量中可确保仅执行一次 Future,然后将其缓存以供后续重建使用。

测试

#

有关如何测试此功能的信息,请参阅以下示例

完整示例

#
dart
import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

Future<Album> fetchAlbum() async {
  final response = await http
      .get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));

  if (response.statusCode == 200) {
    // If the server did return a 200 OK response,
    // then parse the JSON.
    return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
  } else {
    // If the server did not return a 200 OK response,
    // then throw an exception.
    throw Exception('Failed to load album');
  }
}

class Album {
  final int userId;
  final int id;
  final String title;

  const Album({
    required this.userId,
    required this.id,
    required this.title,
  });

  factory Album.fromJson(Map<String, dynamic> json) {
    return switch (json) {
      {
        'userId': int userId,
        'id': int id,
        'title': String title,
      } =>
        Album(
          userId: userId,
          id: id,
          title: title,
        ),
      _ => throw const FormatException('Failed to load album.'),
    };
  }
}

void main() => runApp(const MyApp());

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late Future<Album> futureAlbum;

  @override
  void initState() {
    super.initState();
    futureAlbum = fetchAlbum();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Fetch Data Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Fetch Data Example'),
        ),
        body: Center(
          child: FutureBuilder<Album>(
            future: futureAlbum,
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                return Text(snapshot.data!.title);
              } else if (snapshot.hasError) {
                return Text('${snapshot.error}');
              }

              // By default, show a loading spinner.
              return const CircularProgressIndicator();
            },
          ),
        ),
      ),
    );
  }
}