获取网络数据

对于大部分应用来说,获取网络数据都是必不可少的一个功能。幸运的是,Dart 和 Flutter 就为我们提供了这样的工具。

Fetching data from the internet is necessary for most apps. Luckily, Dart and Flutter provide tools, such as the http package, for this type of work.

这个教程包含以下步骤:

This recipe uses the following steps:

  1. 添加 http

    Add the http package.

  2. 使用 http 包进行网络请求

    Make a network request using the http package.

  3. 将返回的响应转换成一个自定义的 Dart 对象

    Convert the response into a custom Dart object.

  4. 使用 Flutter 对数据进行获取和展示

    Fetch and display the data with Flutter.

1. 添加 http

1. Add the http package

http 包为我们提供了获取网络数据最简单的方法。

The http package provides the simplest way to fetch data from the internet.

安装 http 包之前,你必须先把它添加到 pubspec.yaml 的依赖区域。你可以在 Pub site 找到 http 包的最新版本

To install the http package, add it to the dependencies section of the pubspec.yaml. You can find the latest version of the http package on the Pub site.

dependencies:
  http: <latest_version>

2. 进行网络请求

2. Make a network request

在这里,你可以使用 http.get() 方法从 JSONPlaceholder REST API 上获取到一个样本数据。

In this example, fetch a sample post from the JSONPlaceholder using the http.get() method.

Future<http.Response> fetchPost() {
  return http.get('https://jsonplaceholder.typicode.com/posts/1');
}

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

The http.get() method returns a Future that contains a Response.

  • Future 是 Dart 用来处理异步操作的一个核心类。它通常代表一个可能的值或者将来或许会用到的错误。

    Future is a core Dart class for working with async operations. A Future object represents a potential value or error that will be available at some time in the future.

  • http.Response 类包含成功的 http 请求接收到的数据。

    The http.Response class contains the data received from a successful http call.

3. 将返回的响应转换成一个自定义的 Dart 对象

3. Convert the response into a custom Dart object

虽然进行网络请求很容易,但是处理 Future<http.Response> 却并不简单。为了后续处理起来更加方便,我们需要将 http.Response转换成一个 Dart 对象。

While it’s easy to make a network request, working with a raw Future<http.Response> isn’t very convenient. To make your life easier, convert the http.Response into a Dart object.

创建一个 Post

Create a Post class

首先,创建一个包含网络请求返回数据的 Post 类。而且这个类还需要一个可以利用 json 创建 Post 的工厂构造器。

First, create a Post class that contains the data from the network request. It includes a factory constructor that creates a Post from JSON.

手动转换 JSON 是我们目前唯一的选项。想了解更多,请查看完整的文章 JSON 和序列化数据

Converting JSON by hand is only one option. For more information, see the full article on JSON and serialization.

class Post {
  final int userId;
  final int id;
  final String title;
  final String body;

  Post({this.userId, this.id, this.title, this.body});

  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      userId: json['userId'],
      id: json['id'],
      title: json['title'],
      body: json['body'],
    );
  }
}

http.Response 转换成 Post

Convert the http.Response to a Post

现在,我们需要更新 fetchPost() 函数并返回 Future<Post>。为了实现这个目标,我们需要做以下几步:

Now, use the following steps to update the fetchPost() function to return a Future<Post>:

  1. dart:convert 包将响应体转换成一个 json Map

    Convert the response body into a JSON Map with the dart:convert package.

  2. 如果服务器返回了一个状态码为 200 的 “OK” 响应,那么就使用 fromJson 工厂方法将 json Map 转换成 Post

    If the server returns an “OK” response with a status code of 200, convert the JSON Map into a Post using the fromJson() factory method.

  3. 如果服务器返回的不是我们预期的响应,那么就抛出错误。

    If the server returns an unexpected response, throw an error.

Future<Post> fetchPost() async {
  final response =
      await http.get('https://jsonplaceholder.typicode.com/posts/1');

  if (response.statusCode == 200) {
    // If server returns an OK response, parse the JSON.
    return Post.fromJson(json.decode(response.body));
  } else {
    // If that response was not OK, throw an error.
    throw Exception('Failed to load post');
  }
}

太棒了!现在你就拥有了一个可以获取网络数据的完整函数啦。

Hooray! Now you’ve got a function that fetches a post from the internet.

4. 获取并展示数据

4. Fetch and display the data

为了能够获取数据并在屏幕上展示它,你可以使用 FutureBuilder widget。这个由 Flutter 提供的 FutureBuilder 组件可以让处理异步数据源变的非常简单。

To fetch the data and display it on screen, use the FutureBuilder widget. The FutureBuilder widget comes with Flutter and makes it easy to work with async data sources.

此时,你必须要提供两个参数:

You must provide two parameters:

  1. 你想要处理的 Future。在这里就是调用 fetchPost() 函数。

    The Future you want to work with. In this case, call the fetchPost() function.

  2. 一个告诉 Flutter 渲染哪些内容的 builder 函数,同时这也依赖于 Future 的状态:loading、success 或者是 error。

    A builder function that tells Flutter what to render, depending on the state of the Future: loading, success, or error.

FutureBuilder<Post>(
  future: fetchPost(),
  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 CircularProgressIndicator();
  },
);

5. 将数据请求移出 build() 方法

5. Moving the fetch call out of the build() method

虽然这样会比较方便,但是我们仍然不推荐将 API 调用置于 build() 方法内部。

Although it’s convenient, it’s not recommended to put a call to an API in a build() method.

每当 Flutter 需要改变视图中的一些内容时(这个发生的频率非常高),就会调用 build() 方法。因此,如果你将数据请求置于 build() 内部,就会造成大量的无效调用,同时还会拖慢应用程序的速度。

Flutter calls the build() method every time it wants to change anything in the view, and this happens surprisingly often. If you leave the fetch call in your build() method, you’ll flood the API with unnecessary calls and slow down your app.

关于如何在页面初始化的时候,只调用 API,下面有一些更好的选择。

Here are some better options so it only hits the API when the page is initially loaded.

传入 StatelessWidget

Pass it into a StatelessWidget

使用这种策略的话,相当于父组件负责调用数据获取方法,存储结果并传入你的组件中。

With this strategy, the parent widget is responsible for calling the fetch method, storing its result, and then passing it to your widget.

class MyApp extends StatelessWidget {
  final Future<Post> post;

  MyApp({Key key, this.post}) : super(key: key);

你可以在下面看到一个关于这种策略的完整代码示例。

You can see a working example of this in the complete example below.

StatefulWidget 状态的生命周期中调用

Call it in the lifecycle of a StatefulWidget’s state

如果你的组件是有状态的,你可以在 initState() 或者 didChangeDependencies() 方法中调用 fetch 方法。

If your widget is stateful, call the fetch method in either the initState() or didChangeDependencies() methods.

initState() 只会被调用一次而且再也不会被调用。如果你需要在 InheritedWidget 改变的时候可以重新载入的话,可以把数据调用放在 didChangeDependencies() 方法中。想了解更多详细内容请查看 State

The initState() method is called exactly once and then never again. If you want to have the option of reloading the API in response to an InheritedWidget changing, put the call into the didChangeDependencies() method. See State for more details.

class _MyAppState extends State<MyApp> {
  Future<Post> post;

  @override
  void initState() {
    super.initState();
    post = fetchPost();
  }

测试

Testing

关于如何测试这个功能,请查看下面的说明:

For information on how to test this functionality, see the following recipes:

完整样例

Complete example

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

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

Future<Post> fetchPost() async {
  final response =
      await http.get('https://jsonplaceholder.typicode.com/posts/1');

  if (response.statusCode == 200) {
    // If the call to the server was successful, parse the JSON.
    return Post.fromJson(json.decode(response.body));
  } else {
    // If that call was not successful, throw an error.
    throw Exception('Failed to load post');
  }
}

class Post {
  final int userId;
  final int id;
  final String title;
  final String body;

  Post({this.userId, this.id, this.title, this.body});

  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      userId: json['userId'],
      id: json['id'],
      title: json['title'],
      body: json['body'],
    );
  }
}

void main() => runApp(MyApp(post: fetchPost()));

class MyApp extends StatelessWidget {
  final Future<Post> post;

  MyApp({Key key, this.post}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Fetch Data Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Fetch Data Example'),
        ),
        body: Center(
          child: FutureBuilder<Post>(
            future: post,
            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 CircularProgressIndicator();
            },
          ),
        ),
      ),
    );
  }
}