獲取網路資料
對於大部分應用來說,獲取網路資料都是必不可少的一個功能。幸運的是,Dart 和 Flutter 就為我們提供了這樣的工具。
這個教程包含以下步驟:
-
新增
http
package。 -
使用
http
package 進行網路請求。 -
將回傳的回應轉換成一個自定義的 Dart 物件。
-
使用 Flutter 對資料進行獲取和展示。
http
package
1. 新增 http
package 為我們提供了獲取網路資料最簡單的方法。
要將 http
package 新增到依賴中,執行 flutter pub add
指令:
$ flutter pub add http
匯入 http package。
import 'package:http/http.dart' as http;
如果你要部署 Android,請編輯 AndroidManifest.xml
檔案,新增 Internet 許可權。
<!-- Required to fetch data from the internet. -->
<uses-permission android:name="android.permission.INTERNET" />
同樣,如果你要部署 macOS,請編輯 macos/Runner/DebugProfile.entitlements
和
macos/Runner/Release.entitlements
檔案,新增 network client 許可權。
<!-- Required to fetch data from the internet. -->
<key>com.apple.security.network.client</key>
<true/>
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 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
將 現在,我們需要更新 fetchPost()
函式並回傳 Future<Album>
,為了實現這個目標,我們需要做以下幾步:
-
用
dart:convert
package 將回應體轉換成一個 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) 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()
方法中呼叫獲取資料的方法 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 需要更改檢視中的任何內容時(並且這種更改出現的頻率非常高),就會呼叫 build()
方法。因此,如果你將 fetchAlbum()
方法放在 build()
內,該方法會在每次重建應用時重複呼叫,同時還會拖慢應用程式的速度。
將 fetchAlbum()
的結果儲存在狀態變數中,可確保 Future
只執行一次,然後快取(得到的資料)以備後續重新建立應用。
測試
關於如何測試這個功能,請檢視下面的說明:
完整範例
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();
},
),
),
),
);
}
}