(当頁はdocs.flutter.dev(https://docs.flutter.dev/cookbook/networking/send-data)のリファレンスブログです。)
 ほとんどのアプリケーションでは、インターネットを通じてデータをサーバーに送信する必要があります。
http パッケージを使うことで、このタスクを簡単に実現できます。
1.http パッケージの追加
ネットワークリクエスト(特にデータの送信)を実現するために、http パッケージをプロジェクトに追加します。
(手順1)http パッケージのインストール
flutter pub add httpimport 'package:http/http.dart' as http;(手順2.1)Android デプロイの場合
Android プロジェクトでインターネットを使用するには、Internet パーミッションを明示的に付与する必要があります。
以下のコードを AndroidManifest.xml に追加します。
<!-- インターネットからデータを取得するために必要 -->
<uses-permission android:name="android.permission.INTERNET" />上記により、アプリがネットワークリクエストを実行できるようになります。
(手順2.2)macOS デプロイの場合
上記と同様に以下のファイルにコードを追加します。
 ファイルの場所:
 ・ macos/Runner/DebugProfile.entitlements
 ・ macos/Runner/Release.entitlements
<!-- インターネットからデータを取得するために必要 -->
<key>com.apple.security.network.client</key>
<true/>2.サーバへのデータ送信
http パッケージを使って、POST リクエストでサーバーにデータを送信します。
ここでは、http.post() メソッドを使い、( JSONPlaceholder API にアルバムタイトルを送信し)、新規アルバムを作成します。
(1)dart:convert のインポート
- データを JSON にエンコードする為、dart:convert ライブラリをインポートします。
- jsonEncode を使用して Dart のマップを JSON に変換します。
import 'dart:convert';(2)http.post() メソッド
- HTTP POST リクエストを送信する為、http.post() を使用します。
- サーバに送信するデータを JSON 形式に変換し、リクエストのボディに含めます。
//サーバーにアルバムデータを作成する関数
Future<Album> createAlbum(String title) async {
  //HTTP POST リクエストを送信
  final response = await http.post(
    //サーバーのエンドポイント
    Uri.parse('https://jsonplaceholder.typicode.com/albums'),
    headers: <String, String>{
      //JSON データ送信を指定
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, String>{
      'title': title, //POST ボディに "title" を送信
    }),
  );
  //ステータスコードが 201 (CREATED) の場合
  if (response.statusCode == 201) {
    //サーバーからのレスポンスを解析して Album オブジェクトに変換
    return Album.fromJson(
      jsonDecode(response.body) as Map<String, dynamic>
    );
  } else {
    //201 以外のステータスコードの場合、例外をスロー
    throw Exception('Failed to create album.');
  }
}ちなみに、上記でしているサーバ JSONPlaceholder は、開発者がテスト用に利用できる「モック API サービス」です。
 このサービスは、「テスト用モック API」であり、開発や学習目的で使用されるサービスです。
 データの永続性(保存)は提供せず、代わりに即座に想定されるレスポンスを返すことで、API の動作やクライアントのリクエスト処理を確認できるようになっています。
 リクエスト(GET、POST、PUT など)をシミュレートし、想定されるレスポンスを返しますが、実際にはデータベースに変更を加えたりデータを保存したりすることはありません。
3.http.Response をカスタムDart オブジェクトに変換する
サーバーからのレスポンス(通常は JSON 形式)を取得し、Dart オブジェクトに変換します。
(1)Album クラスの作成
 ネットワークリクエストから取得したデータを表現する Album クラス を作成する方法について説明します。
このクラスには、JSON データを Album オブジェクトに変換するためのファクトリコンストラクタが含まれています。
- Album クラスとは
- ネットワークリクエストで取得したアルバムデータを格納するための Dart クラスです。
- サーバーから取得する JSON データを扱いやすい形式に変換します。
- クラスのプロパティとして id と title を含みます。
 
//アルバムデータを表現するクラス
class Album {
  final int id; //アルバムの ID
  final String title; // アルバムのタイトル
  //コンストラクタ
  const Album({required this.id, required this.title});
  //JSON データから Album オブジェクトを生成するファクトリコンストラクタ
  factory Album.fromJson(Map<String, dynamic> json) {
    return switch (json) {
      {
        'id': int id, //JSON の "id" フィールドを取得
        'title': String title, //JSON の "title" フィールドを取得
      } =>
        Album(
          id: id,
          title: title,
        ),
      //JSON データが期待する形式でない場合、例外をスロー
      _ => throw const FormatException('Failed to load album.'),
    };
  }
}JSON を Dart オブジェクトに変換する方法には、 json_serializable などのパッケージを使う自動生成のアプローチ等もあります。
(2)http.Response の Album への変換
このセクションでは、createAlbum() 関数を拡張し、サーバのレスポンス(http.Response)を Dart の Album オブジェクトに変換する方法を解説します。これにより、サーバからのデータをより簡単に扱えるようになります。
手順1 レスポンスボディを JSON に変換
- サーバからのレスポンスのボディを Dart の Map 型に変換します。
- その為には、dart:convert パッケージの jsonDecode 関数を使用します。
手順2 ステータスコードを確認
- サーバーが 201 (CREATED) ステータスコードを返す場合、データの作成が成功したと判断します。
- ステータスコードが 201 でない場合は例外をスローします。
手順3 JSON を Dart オブジェクトに変換
- 取得した JSON データを Album.fromJson ファクトリコンストラクタを使用して Album オブジェクトに変換します。
手順4 例外のスロー
- ステータスコードが 201 でない場合は、例外をスローしてエラーを処理します。
- 例外をスローすることで、アプリがエラーを適切に扱えるようになります。
サンプルコード:
//サーバーにアルバムデータを作成する関数
Future<Album> createAlbum(String title) async {
  //HTTP POST リクエストを送信
  final response = await http.post(
    //サーバーのエンドポイント
    Uri.parse('https://jsonplaceholder.typicode.com/albums'),
    headers: <String, String>{
      //JSON データ送信を指定
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, String>{
      'title': title, //POST ボディに "title" を送信
    }),
  );
  //ステータスコードが 201 (CREATED) の場合
  if (response.statusCode == 201) {
    //JSON を Dart オブジェクトに変換:
    //( サーバーからのレスポンスを解析して Album オブジェクトに変換 )
    return Album.fromJson(
      //レスポンスボディを JSON に変換:
      //( サーバーのレスポンスボディ(文字列)を Map<String, dynamic> に変換。
      //  Dart の標準ライブラリである dart:convert を使用。)
      jsonDecode(response.body) as Map<String, dynamic>
    );
  } else {
    //201 以外のステータスコードの場合、例外をスロー
    throw Exception('Failed to create album.');
  }
}4.ユーザ入力からタイトルを取得する
ユーザーから入力されたデータ(例: タイトル)を取得し、それをサーバーに送信します。
このセクションでは、以下の機能を実装する方法について説明します。
- ユーザがタイトルを入力するための TextField を作成。
- ユーザがタイトルを入力するための UI コンポーネントを作成します。
- TextEditingController を使用して入力データを管理します。
 
- サーバにデータを送信するための ElevatedButton を作成。
- ユーザーが入力したタイトルをサーバに送信するためのボタンを作成します。
- ボタンが押されると、createAlbum() メソッドを呼び出し、POST リクエストを送信します。
 
- ユーザ入力を管理する TextEditingController を定義。
- ユーザが入力したテキストデータ(タイトル)を取得するために使用されます。
- controller.text を使って TextField に入力された値を読み取ります。
 
  //入力画面を構築するウィジェット
  Column buildColumn() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        //⓵ ユーザーがタイトルを入力するテキストフィールド
        TextField(
          //⓷ 入力データを取得するためのコントローラー
          controller: _controller,
          //プレースホルダー
          decoration: const InputDecoration(hintText: 'Enter Title'),
        ),
        //⓶ "Create Data" ボタン
        ElevatedButton(
          onPressed: () {
            //ボタンを押したときに POST リクエストを送信
            setState(() {
              //入力されたタイトルを送信
              _futureAlbum = createAlbum(_controller.text);
            });
          },
          child: const Text('Create Data'), //ボタンのテキスト
        ),
      ],
    );
  }5.応答の画面表示
サーバからのレスポンスを取得し、それをアプリ画面に表示します。
 ここでは、サーバからのレスポンスを画面に表示するために FutureBuilder ウィジェット を使用する方法を解説します。
このウィジェットは、非同期データソースを扱うのに便利な Flutter 標準のツールです。
(1)FutureBuilder の基本構造
⓵ FutureBuilder の役割
非同期操作(Future)の結果を監視し、その状態(読み込み中、成功、エラー)に応じて適切な UI を表示します。
⓶ 必要なパラメータ
(ⅰ)future
 処理対象の Future を指定します。
(この例では createAlbum() 関数から返される Future を設定します。)
(ⅱ)builder
Future の状態(読み込み中、成功、エラー)に基づいて、どのように UI を描画するかを定義する関数です。
(2)サンプルコード
  //サーバのレスポンスを表示する FutureBuilder
  FutureBuilder<Album> buildFutureBuilder() {
    return FutureBuilder<Album>(
      future: _futureAlbum, // サーバからのレスポンス(Future)
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          //データが取得できた場合
          return Text(snapshot.data!.title); //アルバムのタイトルを表示
        } else if (snapshot.hasError) {
          //エラーが発生した場合
          return Text('${snapshot.error}'); //エラーメッセージを表示
        }
        //データ取得中はローディングスピナ(インジケータ)を表示
        return const CircularProgressIndicator();
      },
    );
  }6.サンプルコード



import 'dart:async';
import 'dart:convert'; //JSON のエンコードとデコードをサポート
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
//サーバーにアルバムデータを作成する関数
Future<Album> createAlbum(String title) async {
  //HTTP POST リクエストを送信
  final response = await http.post(
    //サーバーのエンドポイント
    Uri.parse('https://jsonplaceholder.typicode.com/albums'),
    headers: <String, String>{
      //JSON データ送信を指定
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, String>{
      'title': title, //POST ボディに "title" を送信
    }),
  );
  //ステータスコードが 201 (CREATED) の場合
  if (response.statusCode == 201) {
    //サーバーからのレスポンスを解析して Album オブジェクトに変換
    return Album.fromJson(
      jsonDecode(response.body) as Map<String, dynamic>
    );
  } else {
    //201 以外のステータスコードの場合、例外をスロー
    throw Exception('Failed to create album.');
  }
}
//アルバムデータを表現するクラス
class Album {
  final int id; //アルバムの ID
  final String title; //アルバムのタイトル
  //コンストラクタ
  const Album({required this.id, required this.title});
  //JSON データから Album オブジェクトを生成するファクトリコンストラクタ
  factory Album.fromJson(Map<String, dynamic> json) {
    return switch (json) {
      {
        'id': int id, //JSON の "id" フィールドを取得
        'title': String title, //JSON の "title" フィールドを取得
      } =>
        Album(
          id: id,
          title: title,
        ),
      //JSON データが期待する形式でない場合、例外をスロー
      _ => 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();
  }
}
//StatefulWidget の状態クラス
class _MyAppState extends State<MyApp> {
  //テキスト入力用コントローラー
  final TextEditingController _controller = TextEditingController(); 
  Future<Album>? _futureAlbum; //サーバーからのレスポンスを保持する Future
  @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),
          //Future が null の場合は入力画面、それ以外の場合は結果画面を表示
          child: (_futureAlbum == null) ? buildColumn() : buildFutureBuilder(),
        ),
      ),
    );
  }
  //入力画面を構築するウィジェット
  Column buildColumn() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        //ユーザーがタイトルを入力するテキストフィールド
        TextField(
          //入力データを取得するためのコントローラー
          controller: _controller,
          //プレースホルダー
          decoration: const InputDecoration(hintText: 'Enter Title'),
        ),
        //"Create Data" ボタン
        ElevatedButton(
          onPressed: () {
            //ボタンを押したときに POST リクエストを送信
            setState(() {
              //入力されたタイトルを送信
              _futureAlbum = createAlbum(_controller.text);
            });
          },
          child: const Text('Create Data'), //ボタンのテキスト
        ),
      ],
    );
  }
  //サーバーのレスポンスを表示する FutureBuilder
  FutureBuilder<Album> buildFutureBuilder() {
    return FutureBuilder<Album>(
      future: _futureAlbum, //サーバーからのレスポンス(Future)
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          //データが取得できた場合
          return Text(snapshot.data!.title); //アルバムのタイトルを表示
          
        } else if (snapshot.hasError) {
          //エラーが発生した場合
          return Text('${snapshot.error}'); //エラーメッセージを表示
        }
        //データ取得中はローディングスピナーを表示
        return const CircularProgressIndicator();
      },
    );
  }
}