跳到主要內容

將資料傳送到網際網路

將資料傳送到網際網路是大多數應用程式的必要功能。http 套件也涵蓋了這一點。

此食譜使用以下步驟

  1. 新增 http 套件。
  2. 使用 http 套件將資料傳送到伺服器。
  3. 將回應轉換為自訂 Dart 物件。
  4. 從使用者輸入取得 title
  5. 在螢幕上顯示回應。

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.post() 方法,將專輯標題傳送至 JSONPlaceholder 來建立 Album

導入 dart:convert 以存取 jsonEncode 來編碼資料

dart
import 'dart:convert';

使用 http.post() 方法傳送已編碼的資料

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

http.post() 方法會回傳一個包含 ResponseFuture

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

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

#

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

建立 Album 類別

#

首先,建立一個 Album 類別,其中包含來自網路請求的資料。它包含一個工廠建構子,可從 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

#

使用下列步驟來更新 createAlbum() 函式,使其回傳 Future<Album>

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

  if (response.statusCode == 201) {
    // If the server did return a 201 CREATED response,
    // then parse the JSON.
    return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
  } else {
    // If the server did not return a 201 CREATED response,
    // then throw an exception.
    throw Exception('Failed to create album.');
  }
}

萬歲!現在您有了一個將標題傳送至伺服器以建立專輯的函式。

4. 從使用者輸入取得標題

#

接下來,建立一個 TextField 以輸入標題,以及一個 ElevatedButton 以將資料傳送至伺服器。同時定義一個 TextEditingController 以從 TextField 讀取使用者輸入。

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

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

按下「建立資料」按鈕時,會發出網路請求,將 TextField 中的資料以 POST 請求傳送至伺服器。接下來的步驟會用到 Future, _futureAlbum

5. 在螢幕上顯示回應

#

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

  1. 您要處理的 Future。在此範例中,是從 createAlbum() 函式回傳的 future。
  2. 一個 builder 函式,用於告訴 Flutter 根據 Future 的狀態 (載入中、成功或錯誤) 轉譯的內容。

請注意,只有當快照包含非 null 資料值時,snapshot.hasData 才會回傳 true。這就是為什麼即使在「404 Not Found」伺服器回應的情況下,createAlbum() 函式也應該拋出例外的原因。如果 createAlbum() 回傳 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> createAlbum(String title) async {
  final response = await http.post(
    Uri.parse('https://jsonplaceholder.typicode.com/albums'),
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, String>{
      'title': title,
    }),
  );

  if (response.statusCode == 201) {
    // If the server did return a 201 CREATED response,
    // then parse the JSON.
    return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
  } else {
    // If the server did not return a 201 CREATED response,
    // then throw an exception.
    throw Exception('Failed to create 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();
  Future<Album>? _futureAlbum;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Create Data Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Create Data Example'),
        ),
        body: Container(
          alignment: Alignment.center,
          padding: const EdgeInsets.all(8),
          child: (_futureAlbum == null) ? buildColumn() : buildFutureBuilder(),
        ),
      ),
    );
  }

  Column buildColumn() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        TextField(
          controller: _controller,
          decoration: const InputDecoration(hintText: 'Enter Title'),
        ),
        ElevatedButton(
          onPressed: () {
            setState(() {
              _futureAlbum = createAlbum(_controller.text);
            });
          },
          child: const Text('Create Data'),
        ),
      ],
    );
  }

  FutureBuilder<Album> buildFutureBuilder() {
    return 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();
      },
    );
  }
}