跳至主要內容

從網路擷取資料

從網路獲取資料對於大多數應用程式來說是必要的。幸運的是,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 檔案以新增網際網路權限。

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. 發出網路請求

#

此範例說明如何使用 http.get() 方法從 JSONPlaceholder 獲取範例專輯。

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 Not Found」伺服器回應的情況下,也要擲回例外狀況。不要傳回 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 函式,根據 Future 的狀態(載入中、成功或錯誤)告訴 Flutter 要呈現什麼。

請注意,只有當快照包含非空資料值時,snapshot.hasData 才會傳回 true

因為 fetchAlbum 只能傳回非空值,所以即使在「404 Not Found」伺服器回應的情況下,該函式也應該擲回例外狀況。擲回例外狀況會將 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()?

#

雖然很方便,但不建議將 API 呼叫放入 build() 方法中。

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();
            },
          ),
        ),
      ),
    );
  }
}