iT邦幫忙

2024 iThome 鐵人賽

DAY 23
0

大多數應用程式都需要從網路上讀取資料。Dart 和 Flutter 支援 http 套件協助我們實現這類功能。

⚠️ 注意:你應避免直接使用 dart:io 或者 dart:html 來發送 HTTP 請求。這些函式庫相依於特定平台,只能在單一平台上執行,例如 dart:io 用於非網頁平台如 Android,而 dart:html 則使用於網頁。

安裝 http 套件

http 套件提供了最簡易的方式來讀取網路上的資料。首先我們使用下面指令安裝:

$ flutter pub add http

接著,匯入套件

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

如果要支援 Android,那麼請編輯 /android/app/src/main/AndroidManifest.xml 檔案加入網路權限。

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
  	<!-- 加入這行 -->
    <uses-permission android:name="android.permission.INTERNET" />
		<!-- ... -->
</manifest>

同樣的如果需支援 macOS 則須編輯 macos/Runner/DebugProfile.entitlementsmacos/Runner/Release.entitlements 檔案

<key>com.apple.security.network.client</key>
<true/>

發送請求

下面的範例為使用 http.get() 方法 從 JSONPlaceholder 讀取相簿。

Future<http.Response> fetch() {
  return http.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));
}

http.get() 方法回傳 Future 型別的物件且包含了一個 Response

  • Future 是 Dart 的核心類別之一,用於實作非同步操作。一個 Future 物件代表一個尚未完成的操作,這個操作會在未來的某個時間點產生一個值或一個錯誤。它的概念類似於 JavaScript 中的 Promise。
  • http.Response 類別包含收到的資料。

轉換 Response 為自訂 Dart 物件

雖然從上面看來,發送一個請求並不困難,但使用原始的 Future<http.Response> 整體上並不方便。為了後續更簡易的使用通常我們會把 http.Response 轉換為一個符合用途的 Dart 物件。

建立 Album 類別

首先,建立 Album 類別,使其涵蓋從請求取得且我們需要的資料。這個類別包含了一個工廠建構子,可以從 JSON 建立我們的物件。

其中一種作法就是使用 pattern matching 來轉換 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) {
    // Dart 3.0 匹配模式新語法
    return swtich (json) {
      // 對於 Map 的匹配
      {
        'userId': int userId,
        'id': int id,
        'title': String title
      } => Album(
      	userId: userId,
        id: id,
        title: title,
      ),
      _=> throw const FromatException("載入失敗")
    }
  }
}

switch 匹配模式(Pattern Matching)

我們都知道 switch 一般用於控制流程,可以根據變數進行匹配,通過 case 進行流程上的控制。Dart 3.0 之前有一個重點,那就是 case 後面只能接「常量」就是編譯時已經固定的「值」如 const int n = 1
日常開發一般來說常常用於匹配 int double String enum

void run(int v) {
switch (v) {
 case 0: // 物件也可以匹配
   // ...
   break;
 case 1:
   // ...
   break;
}
}

而 3.0 有了新的匹配模式之後,我們可以:

DateTime date = DateTime(2024, 7, 30);
DateTime now = DateTime.now();
Duration diff = date.difference(DateTime(now.year, now.month, now.day));
String durationText = switch (diff) {
 Duration(inDays: -1) => '昨天',
 Duration(inDays: 0) => '今天',
 Duration(inDays: 1 ) => '明天',
 _ => DateFormat('yyyy年MM月dd日').format(date) // 無法匹配的選項
}

除了一般物件,還可以對 Map 進行匹配,這就是上面 fromJson 的語法。

轉換 http.ResponseAlbum

現在,我們可以來更新我們的 fetch() 函式:

  1. 使用 dart:convert 專案回傳的 Response Body 為 JSON Map
  2. 如果伺服端回傳狀態為 200 成功,則進行轉換 MapAlbum
  3. 若伺服端回傳錯誤訊息,則拋出例外。就算回傳的是 404 Not Found 也應該拋出例外,不要回傳 null
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;

// ...

Future<Album> fetch() async {
  final response = await http.get(Uri.parse("https://jsonplaceholder.typicode.com/albums/1"));
  
  if (response.statusCode == 200) {
    return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
  } else {
    throw Exception('載入失敗');
  }
}

讀取資料

現在我們可以在 initState()didChangeDependencies() 方法中呼叫我們的 fetch

initState() 方法只會在初始化的時候呼叫一次,之後就不會觸發了。如果希望在 InheritedWidget 發生變更時重新呼叫 API 載入,可以在 didChangeDependencies() 方法中呼叫。

class _MyAppState extends State<MyApp> {
  late Future<Album> future;
  
  @override
  void initState() {
    super.initState();
    future = fetch();
  }
  
  // ...
}

顯示資料

要顯示資料可以使用 FutureBuilder 組件。FutureBuilder 組件的用途就是為了簡化顯示非同步取得的資料。

我們需要提供 2 個參數;Futurefetch() 函式。builder 函式會負責根據 Future 的狀態來渲染。

注意:snapshot.hasData 只有在取得資料時且不為 null 才會為 true

由於 fetch 可以只回傳非 null 資料,在遇到 404 的情況會拋出例外。例外會將 snapshot.hasError 設為 true 此時可以顯示錯誤訊息。

FutureBuilder<Album>(
	future: future,
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      return Text(snpashot.data!.title);
    } else if (snapshot.hasError) {
      return Text(snapshot.error);
    }
    // 預設顯示一個載入的效果
    return const CircularProgressIndicator();
  }
);

雖然可以在 build() 方法中呼叫 fetch() 但實務上不推薦。因為 Flutter 隨時可能調用 build() 且這種頻率還蠻高的。如果將 fetch() 放到 build() 中很容易導致效能降低。

參考資源


上一篇
Day 22 多國語系支援
下一篇
Day 24 使用 Google Map
系列文
Flutter 開發實戰 - 30 天逃離新手村38
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言