获取网络数据
1. 添加 http 包
1. Add the http package
2. 进行网络请求
2. Make a network request
3. 将返回的响应转换成一个自定义的 Dart 对象
3. Convert the response into a custom Dart object
4. 获取数据
4. Fetch the data
5. 显示数据
5. Display the data
为何要在 initState() 中调用 fetchPost()?
Why is fetchAlbum() called in initState()?
测试
Testing
完整样例
Complete example
对于大部分应用来说,获取网络数据都是必不可少的一个功能。幸运的是,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:
-
添加
http
包Add the
http
package. -
使用
http
包进行网络请求Make a network request using the
http
package. -
将返回的响应转换成一个自定义的 Dart 对象
Convert the response into a custom Dart object.
-
使用 Flutter 对数据进行获取和展示
Fetch and display the data with Flutter.
http
包
1. 添加
http
package
1. Add the http
包为我们提供了获取网络数据最简单的方法。
The http
package provides the
simplest way to fetch data from the internet.
安装 http
包之前,你必须先把它添加到 pubspec.yaml
的依赖区域。你可以在 [pub.dev 找到 http 包的最新版本][http package]。
To install the http
package, add it to the
dependencies section of the pubspec.yaml
file.
You can find the latest version of the
http
package the pub.dev.
dependencies:
http: <latest_version>
Import the http package.
import 'package:http/http.dart' as http;
Additionally, in your AndroidManifest.xml file, add the Internet permission.
<!-- Required to fetch data from the internet. -->
<uses-permission android:name="android.permission.INTERNET" />
2. 进行网络请求
2. Make a network request
在这里,你可以使用 http.get()
方法从 JSONPlaceholder 上获取到一个样本相册数据。
This recipe covers how to fetch a sample album from the
JSONPlaceholder using the http.get()
method.
Future<http.Response> fetchAlbum() {
return http.get(Uri.https('jsonplaceholder.typicode.com', 'albums/1'));
}
这个 http.get()
方法会返回一个包含 Response
的 Future
。
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.
Album
类
创建一个
Album
class
Create an 首先,创建一个包含网络请求返回数据的 Album
类,而且这个类还需要一个可以利用 json 创建 Album
的工厂构造器。
First, create an Album
class that contains the data from the
network request. It includes a factory constructor that
creates an Album
from JSON.
手动转换 JSON 是我们目前唯一的选项。想了解更多,请查看完整的文档 JSON 和序列化数据。
Converting JSON by hand is only one option. For more information, see the full article on JSON and serialization.
class Album {
final int userId;
final int id;
final String title;
Album({this.userId, this.id, this.title});
factory Album.fromJson(Map<String, dynamic> json) {
return Album(
userId: json['userId'],
id: json['id'],
title: json['title'],
);
}
}
http.Response
转换成 Album
将
http.Response
to an Album
Convert the 现在,我们需要更新 fetchPost()
函数并返回 Future<Album>
,为了实现这个目标,我们需要做以下几步:
Now, use the following steps to update the fetchAlbum()
function to return a Future<Album>
:
-
用
dart:convert
包将响应体转换成一个 jsonMap
。Convert the response body into a JSON
Map
with thedart:convert
package. -
如果服务器返回了一个状态码为 200 的 “OK” 响应,那么就使用
fromJson
工厂方法将 jsonMap
转换成Album
。If the server does return an OK response with a status code of 200, then convert the JSON
Map
into anAlbum
using thefromJson()
factory method. -
如果服务器返回的不是我们预期的响应(返回一个OK,Http Header 是 200),那么就抛出异常。服务器如若返回 404 Not Found 错误,也同样要抛出异常,而不是返回一个
null
,在检查如下所示的snapshot
值的时候,这一点相当重要。If the server does not return an OK response with a status code of 200, then throw an exception. (Even in the case of a “404 Not Found” server response, throw an exception. Do not return
null
. This is important when examining the data insnapshot
, as shown below.)
import 'dart:convert';
Future<Album> fetchAlbum() async {
final response = await http.get('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));
} else {
// If the server did not return a 200 OK response,
// then throw an exception.
throw Exception('Failed to load album');
}
}
太棒了!现在你就拥有了一个可以获取网络数据的完整函数啦。
Hooray! Now you’ve got a function that fetches an album from the internet.
4. 获取数据
4. Fetch the data
在 initState()
或 didChangeDependencies()
方法中调用获取数据的方法 fetch()
。
Call the fetch()
method in either the
initState()
or didChangeDependencies()
methods.
initState()
方法仅会被调用一次。如果你想要响应 InheritedWidget
改变以重新加载 API 的话,请在 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<Album> futureAlbum;
@override
void initState() {
super.initState();
futureAlbum = fetchAlbum();
}
我们将会在下一步中使用这个 Future。
This Future is used in the next step.
5. 显示数据
5. Display the data
为了能够获取数据并在屏幕上展示它,你可以使用 FutureBuilder
widget。这个由 Flutter 提供的 FutureBuilder
组件可以让处理异步数据源变的非常简单。
To display the data on screen, use the
FutureBuilder
widget.
The FutureBuilder
widget comes with Flutter and
makes it easy to work with asynchronous data sources.
此时,你必须要提供两个参数:
You must provide two parameters:
-
你想要处理的
Future
,在这个例子中就是fetchAlbum()
返回的 future。The
Future
you want to work with. In this case, the future returned from thefetchAlbum()
function. -
一个告诉 Flutter 渲染哪些内容的
builder
函数,同时这也依赖于Future
的状态:loading、success 或者是 error。A
builder
function that tells Flutter what to render, depending on the state of theFuture
: loading, success, or error.
需要注意的是:当快照包含非空数据值,snapshot.hasData
将只返回 true
,这就是为什么要在服务端返回 404 状态码的时候要让 fetchAlbum
方法抛出异常。如果 fetchAlbum
返回 null
的话,spinner 会显示不正常。
Note that snapshot.hasData
only returns true
when the snapshot contains a non-null data value.
This is why the fetchAlbum
function should throw an exception
even in the case of a “404 Not Found” server response.
If fetchAlbum
returns null
then the spinner displays indefinitely.
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 CircularProgressIndicator();
},
);
为何要在 initState() 中调用 fetchPost()?
Why is fetchAlbum() called in initState()?
虽然这样会比较方便,但是我们仍然不推荐将 API 调用置于 build()
方法内部。
Although it’s convenient,
it’s not recommended to put an API call in a build()
method.
每当 Flutter 需要改变视图中的一些内容时(这个发生的频率非常高),就会调用 build()
方法。因此,如果你将数据请求置于 build()
内部,就会造成大量的无效调用,同时还会拖慢应用程序的速度。
Flutter calls the build()
method every time it needs
to change anything in the view,
and this happens surprisingly often.
Leaving the fetch
call in your build()
method
floods the API with unnecessary calls and slows down your app.
关于如何在页面初始化的时候,只调用 API,下面有一些更好的选择。
Here are some better options so it only hits the API when the page is initially loaded.
StatelessWidget
传入
StatelessWidget
Pass it into a 使用这种策略的话,相当于父组件负责调用数据获取方法,存储结果并传入你的组件中。
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
状态的生命周期中调用
在
StatefulWidget
’s state
Call it in the lifecycle of a 如果你的组件是有状态的,你可以在
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<Album> fetchAlbum() async {
final response =
await http.get(Uri.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));
} 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;
Album({this.userId, this.id, this.title});
factory Album.fromJson(Map<String, dynamic> json) {
return Album(
userId: json['userId'],
id: json['id'],
title: json['title'],
);
}
}
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
MyApp({Key key}) : super(key: key);
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Future<Album> futureAlbum;
@override
void initState() {
super.initState();
futureAlbum = fetchAlbum();
}
@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<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 CircularProgressIndicator();
},
),
),
),
);
}
}