[読書会]インターネットへのデータ送信

(当頁はdocs.flutter.dev(https://docs.flutter.dev/cookbook/networking/send-data)のリファレンスブログです。)

 ほとんどのアプリケーションでは、インターネットを通じてデータをサーバーに送信する必要があります。
http パッケージを使うことで、このタスクを簡単に実現できます。

1.http パッケージの追加

 ネットワークリクエスト(特にデータの送信)を実現するために、http パッケージをプロジェクトに追加します。

(手順1)http パッケージのインストール

(コマンド)プロジェクトに http パッケージを追加:
flutter pub add http
http パッケージを使用するためにインポートします:
import 'package:http/http.dart' as http;

(手順2.1)Android デプロイの場合

 Android プロジェクトでインターネットを使用するには、Internet パーミッションを明示的に付与する必要があります。

 以下のコードを AndroidManifest.xml に追加します。

(ファイルの場所) android/app/src/main/AndroidManifest.xml
<!-- インターネットからデータを取得するために必要 -->
<uses-permission android:name="android.permission.INTERNET" />

 上記により、アプリがネットワークリクエストを実行できるようになります。

(手順2.2)macOS デプロイの場合

 上記と同様に以下のファイルにコードを追加します。

 ファイルの場所:
 ・ macos/Runner/DebugProfile.entitlements
 ・ macos/Runner/Release.entitlements

macOS デプロイの場合:
<!-- インターネットからデータを取得するために必要 -->
<key>com.apple.security.network.client</key>
<true/>

2.サーバへのデータ送信

 http パッケージを使って、POST リクエストでサーバーにデータを送信します。

 ここでは、http.post() メソッドを使い、( JSONPlaceholder API アルバムタイトルを送信し)、新規アルバムを作成します。

(1)dart:convert のインポート

  • データを JSON にエンコードする為、dart:convert ライブラリをインポートします。
  • jsonEncode を使用して Dart のマップ JSON に変換します。
(1)dart:convert のインポート:
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 を含みます。
(1)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.'),
    };
  }
}

 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 でない場合は、例外をスローしてエラーを処理します。
  • 例外をスローすることで、アプリがエラーを適切に扱えるようになります。

サンプルコード:

(2)http.Response の Album への変換
//サーバーにアルバムデータを作成する関数
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.ユーザ入力からタイトルを取得する

 ユーザーから入力されたデータ(例: タイトル)を取得し、それをサーバーに送信します。

5.応答の画面表示

 サーバーからのレスポンスを取得し、それをアプリ画面に表示します。

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

コメントを残す