跳至主要內容

透過網路更新資料

對於大多數應用程式來說,透過網路更新資料是必要的。http 套件涵蓋了這一點!

此食譜使用以下步驟

  1. 新增 http 套件。
  2. 使用 http 套件透過網路更新資料。
  3. 將回應轉換為自訂 Dart 物件。
  4. 從網際網路取得資料。
  5. 從使用者輸入更新現有的 title
  6. 更新並在螢幕上顯示回應。

1. 新增 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 套件透過網路更新資料

#

本食譜涵蓋如何使用 http.put() 方法,將專輯標題更新至 JSONPlaceholder

dart
Future<http.Response> updateAlbum(String title) {
  return http.put(
    Uri.parse('https://jsonplaceholder.typicode.com/albums/1'),
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, String>{
      'title': title,
    }),
  );
}

http.put() 方法會傳回包含 ResponseFuture

  • Future 是一個核心 Dart 類別,用於處理非同步操作。Future 物件代表未來某個時間點可用的潛在值或錯誤。
  • http.Response 類別包含從成功的 http 呼叫接收到的資料。
  • updateAlbum() 方法會接收一個引數 title,該引數會傳送至伺服器以更新 Album

3. 將 http.Response 轉換為自訂 Dart 物件

#

雖然發出網路請求很容易,但使用原始的 Future<http.Response> 並不是很方便。為了讓您的生活更輕鬆,請將 http.Response 轉換為 Dart 物件。

建立 Album 類別

#

首先,建立一個 Album 類別,其中包含來自網路請求的資料。它包含一個 factory 建構函式,可從 JSON 建立 Album

使用 模式匹配 轉換 JSON 只是其中一個選項。如需更多資訊,請參閱關於 JSON 和序列化 的完整文章。

dart
class Album {
  final int id;
  final String title;

  const Album({required this.id, required this.title});

  factory Album.fromJson(Map<String, dynamic> json) {
    return switch (json) {
      {
        'id': int id,
        'title': String title,
      } =>
        Album(
          id: id,
          title: title,
        ),
      _ => throw const FormatException('Failed to load album.'),
    };
  }
}

http.Response 轉換為 Album

#

現在,請使用以下步驟更新 updateAlbum() 函式,以傳回 Future<Album>

  1. 使用 dart:convert 套件將回應主體轉換為 JSON Map
  2. 如果伺服器傳回狀態碼為 200 的 UPDATED 回應,則使用 fromJson() factory 方法將 JSON Map 轉換為 Album
  3. 如果伺服器沒有傳回狀態碼為 200 的 UPDATED 回應,則擲回例外。(即使在伺服器回應「404 Not Found」的情況下,也要擲回例外。不要傳回 null。這在檢查 snapshot 中的資料時非常重要,如下所示。)
dart
Future<Album> updateAlbum(String title) async {
  final response = await http.put(
    Uri.parse('https://jsonplaceholder.typicode.com/albums/1'),
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, String>{
      'title': title,
    }),
  );

  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 update album.');
  }
}

萬歲!現在您有一個可以更新專輯標題的函式了。

從網際網路取得資料

#

在您可以更新資料之前,請先從網際網路取得資料。如需完整範例,請參閱「取得資料」食譜。

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');
  }
}

理想情況下,您會在 initState 期間使用此方法來設定 _futureAlbum,以從網際網路取得資料。

4. 從使用者輸入更新現有的標題

#

建立一個 TextField 以輸入標題,以及一個 ElevatedButton 以更新伺服器上的資料。同時定義一個 TextEditingController 以從 TextField 讀取使用者輸入。

當按下 ElevatedButton 時,_futureAlbum 會設定為 updateAlbum() 方法傳回的值。

dart
Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    Padding(
      padding: const EdgeInsets.all(8),
      child: TextField(
        controller: _controller,
        decoration: const InputDecoration(hintText: 'Enter Title'),
      ),
    ),
    ElevatedButton(
      onPressed: () {
        setState(() {
          _futureAlbum = updateAlbum(_controller.text);
        });
      },
      child: const Text('Update Data'),
    ),
  ],
);

按下「更新資料」按鈕時,網路請求會將 TextField 中的資料以 PUT 請求的形式傳送至伺服器。下一步會使用 _futureAlbum 變數。

5. 在螢幕上顯示回應

#

若要在螢幕上顯示資料,請使用 FutureBuilder 小工具。FutureBuilder 小工具隨附於 Flutter,可輕鬆處理非同步資料來源。您必須提供兩個參數

  1. 您要處理的 Future。在此案例中,為 updateAlbum() 函式傳回的 future。
  2. 一個 builder 函式,可根據 Future 的狀態 (載入中、成功或錯誤) 告知 Flutter 要呈現的內容。

請注意,當 snapshot 包含非空資料值時,snapshot.hasData 只會傳回 true。這就是為什麼即使在伺服器回應「404 Not Found」的情況下,updateAlbum 函式也應該擲回例外的原因。如果 updateAlbum 傳回 null,則 CircularProgressIndicator 會無限期顯示。

dart
FutureBuilder<Album>(
  future: _futureAlbum,
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      return Text(snapshot.data!.title);
    } else if (snapshot.hasError) {
      return Text('${snapshot.error}');
    }

    return const CircularProgressIndicator();
  },
);

完整範例

#
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');
  }
}

Future<Album> updateAlbum(String title) async {
  final response = await http.put(
    Uri.parse('https://jsonplaceholder.typicode.com/albums/1'),
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, String>{
      'title': title,
    }),
  );

  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 update album.');
  }
}

class Album {
  final int id;
  final String title;

  const Album({required this.id, required this.title});

  factory Album.fromJson(Map<String, dynamic> json) {
    return switch (json) {
      {
        'id': int id,
        'title': String title,
      } =>
        Album(
          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() {
    return _MyAppState();
  }
}

class _MyAppState extends State<MyApp> {
  final TextEditingController _controller = TextEditingController();
  late Future<Album> _futureAlbum;

  @override
  void initState() {
    super.initState();
    _futureAlbum = fetchAlbum();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Update Data Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Update Data Example'),
        ),
        body: Container(
          alignment: Alignment.center,
          padding: const EdgeInsets.all(8),
          child: FutureBuilder<Album>(
            future: _futureAlbum,
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.done) {
                if (snapshot.hasData) {
                  return Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      Text(snapshot.data!.title),
                      TextField(
                        controller: _controller,
                        decoration: const InputDecoration(
                          hintText: 'Enter Title',
                        ),
                      ),
                      ElevatedButton(
                        onPressed: () {
                          setState(() {
                            _futureAlbum = updateAlbum(_controller.text);
                          });
                        },
                        child: const Text('Update Data'),
                      ),
                    ],
                  );
                } else if (snapshot.hasError) {
                  return Text('${snapshot.error}');
                }
              }

              return const CircularProgressIndicator();
            },
          ),
        ),
      ),
    );
  }
}