获取网络数据
对于大部分应用来说,获取网络数据都是必不可少的一个功能。幸运的是,Dart 和 Flutter 就为我们提供了这样的工具。
这个教程包含以下步骤:
-
添加
http
包 -
使用
http
包进行网络请求 -
将返回的响应转换成一个自定义的 Dart 对象
-
使用 Flutter 对数据进行获取和展示
http
包
1. 添加 http
package 为我们提供了获取网络数据最简单的方法。
要将 http
包添加到依赖中,运行 flutter pub add
命令:
$ flutter pub add http
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. 进行网络请求
在这里,你可以使用 http.get()
方法从 JSONPlaceholder 上获取到一个样本相册数据。
Future<http.Response> fetchAlbum() {
return http.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));
}
这个 http.get()
方法会返回一个包含 Response
的 Future
。
-
Future
是 Dart 用来处理异步操作的一个核心类,它通常代表一个可能的值或者将来或许会用到的错误。 -
http.Response
类包含成功的 http 请求接收到的数据。
3. 将返回的响应转换成一个自定义的 Dart 对象
虽然进行网络请求很容易,但是处理 Future<http.Response>
却并不简单,为了后续处理起来更加方便,我们需要将 http.Response
转换成一个 Dart 对象。
Album
类
创建一个 首先,创建一个包含网络请求返回数据的 Album
类,而且这个类还需要一个可以利用 json 创建 Album
的工厂构造器。
手动转换 JSON 是我们目前唯一的选项。想了解更多,请查看完整的文档 JSON 和序列化数据。
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 Album(
userId: json['userId'],
id: json['id'],
title: json['title'],
);
}
}
http.Response
转换成 Album
将 现在,我们需要更新 fetchPost()
函数并返回 Future<Album>
,为了实现这个目标,我们需要做以下几步:
-
用
dart:convert
包将响应体转换成一个 jsonMap
。 -
如果服务器返回了一个状态码为 200 的 “OK” 响应,那么就使用
fromJson
工厂方法将 jsonMap
转换成Album
。 -
如果服务器返回的不是我们预期的响应(返回一个OK,Http Header 是 200),那么就抛出异常。服务器如若返回 404 Not Found 错误,也同样要抛出异常,而不是返回一个
null
,在检查如下所示的snapshot
值的时候,这一点相当重要。
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));
} else {
// If the server did not return a 200 OK response,
// then throw an exception.
throw Exception('Failed to load album');
}
}
太棒了!现在你就拥有了一个可以获取网络数据的完整函数啦。
4. 获取数据
在 initState()
或 didChangeDependencies()
方法中调用获取数据的方法 fetch()
。
initState()
方法仅会被调用一次。如果你想要响应 InheritedWidget
改变以重新加载 API 的话,请在 didChangeDependencies()
方法中进行调用,你可以在 State
文档里了解更多。
class _MyAppState extends State<MyApp> {
late Future<Album> futureAlbum;
@override
void initState() {
super.initState();
futureAlbum = fetchAlbum();
}
// ···
}
我们将会在下一步中使用这个 Future。
5. 显示数据
为了能够获取数据并在屏幕上展示它,你可以使用 FutureBuilder
widget。这个由 Flutter 提供的 FutureBuilder
组件可以让处理异步数据源变的非常简单。
此时,你必须要提供两个参数:
-
你想要处理的
Future
,在这个例子中就是fetchAlbum()
返回的 future。 -
一个告诉 Flutter 渲染哪些内容的
builder
函数,同时这也依赖于Future
的状态:loading、success 或者是 error。
需要注意的是:当快照包含非空数据值,
snapshot.hasData
将只返回 true
。
因为 fetchAlbum
只能返回非空值,在服务器响应
“404 Not Found” 的时候应该引发异常抛出。发生异常的时候会将 snapshot.hasError
设定为 true
,用来显示错误消息。
其他情况下,spinner 就会正常显示。
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() 中调用 fetchPost()?
虽然这样会比较方便,但是我们仍然不推荐将 API 调用置于 build()
方法内部。
Flutter calls the build()
method every time it needs
to change anything in the view,
and this happens surprisingly often.
The fetchAlbum()
method, if placed inside build()
, is repeatedly
called on each rebuild causing the app to slow down.
每当 Flutter 需要改变视图中的一些内容时(这个发生的频率非常高),就会调用 build()
方法。因此,如果你将数据请求置于 build()
内部,就会造成大量的无效调用,同时还会拖慢应用程序的速度。
关于如何在页面初始化的时候,只调用 API,下面有一些更好的选择。
StatelessWidget
传入 使用这种策略的话,相当于父组件负责调用数据获取方法,存储结果并传入你的组件中。
class MyApp extends StatelessWidget {
final Future<Post> post;
MyApp({Key key, this.post}) : super(key: key);
你可以在下面看到一个关于这种策略的完整代码示例。
StatefulWidget
状态的生命周期中调用
在 如果你的组件是有状态的,你可以在
initState()
或者 didChangeDependencies()
方法中调用 fetch 方法。
initState()
只会被调用一次而且再也不会被调用。如果你需要在 InheritedWidget
改变的时候可以重新载入的话,可以把数据调用放在 didChangeDependencies()
方法中。想了解更多详细内容请查看 State
文档。
class _MyAppState extends State<MyApp> {
Future<Post> post;
@override
void initState() {
super.initState();
post = fetchPost();
}
测试
关于如何测试这个功能,请查看下面的说明:
完整样例
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));
} 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 Album(
userId: json['userId'],
id: json['id'],
title: json['title'],
);
}
}
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(
primarySwatch: Colors.blue,
),
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();
},
),
),
),
);
}
}