🐙

Dartの非同期処理とFlutterのHTTP通信の書き方

2024/11/13に公開

はじめに

皆さん、こんにちは。

今回はDartの非同期処理についてと、Flutterでの通信の書き方、それに伴いJSONの扱いについて取り上げています。

Future型

概要

  • 非同期処理を扱うための型
  • 非同期処理を扱う関数の戻り値はFuture型とする
  • async/awaitキーワードで非同期処理を同期的に記述可能

Dartの実行環境はシングルスレッドなので非同期処理を活用します。非同期処理を扱う型はFuture型です。非同期処理を行う関数の戻り値はFuture型とし、具体的な結果の値は型パラメータで指定します。

// 結果なし(void)の非同期関数の宣言例
Future<void> showMessage() {

// 結果はString型の非同期関数の宣言例
Future<String> showMessage() {

非同期処理を記述する際はasyncawaitキーワードを利用します。awaitキーワードを付与することで戻り値がFuture型の関数から目的の値を直接戻り値として受け取ることができます。

// 非同期関数(asyncキーワードを付与)
Future<void> showMessage() async {
  const seconds = 5;
  
  // 非同期処理にawaitキーワードを付与
  // delayedは指定時間待機する機能
  await Future.delayed(const Duration(seconds: seconds));
  
  // 指定秒後に実行される
  print('after $seconds seconds');
}

Stream

概要

  • 細切れにしたデータを連続で受け取るための型
  • データを受け取るたびに処理を行うことができる
  • 分割して少しずつ処理するため、メモリ消費を抑えつつデータを効率的に扱える
  • 巨大なデータを扱う際に有効

巨大なファイルの逐次読み取りや、スマホの位置情報やジャイロセンサーからの定期的なデータの受け取りなどで利用されるようです。センサーのデータを随時処理することで受け取ったデータに対応したリアルタイムな処理が可能です。用途が限定的なので概要のみ。

アイソレート

概要

  • Dartの並列実行の仕組み
  • 負荷の高い作業をバックグラウンドで実行できる

Flutter開発ではアイソレートを意識することはあまりないようです。ものによってアイソレートが自動的に起動し処理が行われているようです。普段使いの知識ではないので概要のみ。

通信:GETリクエスト

概要

  • httpパッケージをインポート
  • 非同期関数を作成
  • Uri.parse()でURLの準備
  • http**.get**(url);でGETリクエストを送信
  • response.bodyからレスポンスのJSON文字列を取得
  • jsonDecode(JSON)メソッドでJSON文字列をListMapに変換

GETリクエストでWebAPIからデータを取得する方法についてまとめます。やり取りするデータ形式はJSONを想定しています。

GETするJSONの形式

[
    {
      "id": 1,
      "title": "abc"
    }
]

POSTするJSONの形式

{
  "id": 5,
  "title": "fivefive"
}

httpパッケージの導入

Flutterで通信を行うにはhttpパッケージを導入します。

flutter pub add http

https://pub.dev/packages/http

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

非同期関数の作成

通信は非同期処理であるため、非同期関数を作成しその中で処理を組み立てます。作成した非同期関数の戻り値はFuture<List<モデルクラス>>としています。今回は複数件のデータを受け取りListとして返却する想定のためです。

Future<List<Data>> sendGetReqest() async {

URLの準備

送信先のURLを用意します。UriクラスのparseメソッドでURL文字列から用意することができます。生成したUriオブジェクトをGETリクエスト送信の際に利用します。

// 送信先URLを準備
final url = Uri.parse('http://localhost:3000/sample');

GETリクエストを送信

リクエストの送信はhttpパッケージのgetメソッドを利用します。非同期で動作するため呼び出し時にawaitキーワードを付与します。引数には前段で用意したUriオブジェクトを渡します。戻り値はResponseオブジェクトなので、後続の処理でbodyを取り出す必要があります。

// GETリクエストを送信(非同期)
final response = await http.get(url);

ボディを取り出してJSONをデコード

Responseオブジェクトからbodyプロパティを指定しJSON文字列を取り出します。プログラム内でオブジェクトとして扱えるようにdart:convertライブラリのjsonDecodeメソッドでデコードします。戻り値はList<dynamic>型です。

// JSONの変換メソッドを持つライブラリをインポート
import 'dart:convert';
final json = response.body; // String型
final rawList = jsonDecode(json); // List<dynamic>型

dynamic型をクラス型に変換

型安全に扱うためdynamic型をJSONの構造に合わせて用意したクラス型に変換します。今回はDataという名前のクラスを用意しました。クラス名が抽象的すぎますが、今回はあくまで例なので。

Dataクラス内にはデコードしたJSONを受け取ってインスタンスを返却するfactoryコンストラクタを用意しました。通常のコンストラクタを呼び出してもいいのですが、JSONをそのまま渡せた方が楽なので、factoryコンストラクタでラップしています。

// JSONに対応するクラス
class Data {
  final int id;
  final String title;

  // 通常のコンストラクタ
  Data({required this.id, required this.title});

  // デコードしたJSONからインスタンスを生成するコンストラクタ
  factory Data.fromJson(Map<String, dynamic> json) {
    return Data(
      id: json['id'],
      title: json['title'],
    );
  }
}

jsonDecodeメソッドの戻り値のListからmapメソッドを呼び出してdynamic型の要素をDataクラス型に変換していきます。この時mapメソッドの型パラメータでData型を指定しないと型がList<dynamic>のままになるので注意です。

final dataList =
    rawList.map<Data>((json) => Data.fromJson(json)).toList(); // List<Data>型

サンプルの全体像はこちらです。

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

// JSONに対応するクラス
class Data {
  final int id;
  final String title;

  // 通常のコンストラクタ
  Data({required this.id, required this.title});

  // デコードしたJSONからインスタンスを生成するコンストラクタ
  factory Data.fromJson(Map<String, dynamic> json) {
    return Data(
      id: json['id'],
      title: json['title'],
    );
  }
}

// GETリクエストを送信して結果をData型リストで返却する
Future<List<Data>> sendGetReqest() async {
  // 送信先URLを準備
  final url = Uri.parse('http://localhost:3000/sample'); // その他
  // final url = Uri.parse('http://10.0.2.2:3000/sample'); // Android

  // GETリクエストを送信(非同期)
  final response = await http.get(url);
  
  // // ステータスコードを確認
  // if (response.statusCode == 200) {
  final json = response.body; // String型
  final rawList = jsonDecode(json); // List<dynamic>型
  final dataList =
      rawList.map<Data>((json) => Data.fromJson(json)).toList(); // List<Data>型
  // } else {
  //   通信失敗時の処理;
  // }
  return dataList;
}

通信:POSTリクエスト

概要

  • httpパッケージをインポート
  • 非同期関数を作成
  • Uri.parse()でURLの準備
  • response.bodyからレスポンスのJSON文字列を取得
  • jsonEncode(ListやMap)メソッドでListMapをJSON文字列に変換
  • http.post(url, headers, body);でPOSTリクエストを送信

POSTリクエストを送信する際はhttp.postメソッドを利用します。引数には送信先URL、ヘッダー、送信するJSONを指定します。

http.postメソッドは非同期処理なのでawaitキーワードを付与して実行します。戻り値はResponseオブジェクトです。

// 送信先URLを準備
final url = Uri.parse('http://localhost:3000/sample');
// Content-Typeヘッダーを準備
final headers = {'Content-Type': 'application/json'};
// 送信データの元になるオブジェクトを用意
final data = Data(id: 5, title: 'fivefive');

// POSTリクエストを送信(非同期)
final response = await http.post(
  url,
  headers: headers,
  // JSONにエンコードしてボディに指定
  body: jsonEncode(data.toJson()),
);

今回はリクエストボディの指定をbody: jsonEncode(data.toJson()),と記述しています。jsonEncodeメソッドは引数のオブジェクトをJSON文字列に変換するメソッドです。引数のdata.toJson()は今回用意したDataクラスに用意したメソッドで、インスタンスをMap<String, dynamic>型に変換するためのものです。

// JSONに対応するクラス
class Data {
  final int id;
  final String title;

  Data({required this.id, required this.title});

  factory Data.fromJson(Map<String, dynamic> json) {
    return Data(
      id: json['id'],
      title: json['title'],
    );
  }

  // これを追加:インスタンスをMap<String, dynamic>型に変換
  Map<String, dynamic> toJson() {
    return {'id': id, 'title': title};
  }
}

サンプルの全体像はこちらです。

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

// JSONに対応するクラス
class Data {
  final int id;
  final String title;

  // 通常のコンストラクタ
  Data({required this.id, required this.title});

  // デコードしたJSONからインスタンスを生成するコンストラクタ
  factory Data.fromJson(Map<String, dynamic> json) {
    return Data(
      id: json['id'],
      title: json['title'],
    );
  }

  // インスタンスをMap<String, dynamic>型に変換
  Map<String, dynamic> toJson() {
    return {'id': id, 'title': title};
  }
}

// POSTリクエストを送信
Future<void> sendPostReqest() async {
  // 送信先URLを準備
  final url = Uri.parse('http://localhost:3000/sample');
  // Content-Typeヘッダーを準備
  final headers = {'Content-Type': 'application/json'};
  // 送信データの元になるオブジェクトを用意
  final data = Data(id: 5, title: 'fivefive');

  // POSTリクエストを送信(非同期)
  final response = await http.post(
    url,
    headers: headers,
    // JSONにエンコードしてボディに指定
    body: jsonEncode(data.toJson()),
  );
}

JSONの扱いではjson_serializableパッケージを利用すると便利

概要

  • 通信で扱うJSONをクラス型のオブジェクトに変換するメソッドを自動生成できる
  • クラスを用意し、それを元にコード生成を行う

オブジェクトとJSONの変換メソッドを自作せず、コード生成に頼ることもできます。

これらのパッケージ自体はJSONとオブジェクトの変換メソッドを用意するためのものであり、通信の機能ではありません。JSONは通信でよく使われる形式なので勘違いしやすいので注意です。

必要なパッケージの導入

まずは必要なパッケージを導入します。

  • json_annotation:オブジェクトとJSONの変換に必要なアノテーションを提供
  • json_serializable:コードからJSON変換ロジックを自動生成する
  • build_runner:コード生成ツール
flutter pub add json_annotation
flutter pub add --dev json_serializable
flutter pub add --dev build_runner

https://pub.dev/packages/json_serializable
https://pub.dev/packages/json_annotation/install
https://pub.dev/packages/build_runner

クラスの用意とコード生成

コード生成の前にJSONに対応するクラスを用意します。基本は通常のクラスと同じようにフィールドやコンストラクタを用意します。

特徴的なのは次の点です。

  • part 'ファイル名.g.dart'; で生成したコードを取り込む
  • クラスに@JsonSerializable()を付加する
  • コード生成により_$クラス名FromJsonメソッドが生成される
  • コード生成により_$クラス名ToJsonメソッドが生成される

コード生成の対象とするにはpart 'ファイル名.g.dart';@JsonSerializable()の記述が必要です。part 'ファイル名.g.dart'; はコード生成によって生成されるファイルなので、記述時点では存在せず赤波線が出てしまいますが、気にしないで記述します。

partで取り込む生成されたコード内には_$クラス名FromJsonメソッドと_$クラス名ToJsonメソッドが含まれています。これを自前のメソッド内で呼び出すようにします。

_$クラス名FromJsonメソッドは引数のMap<String, dynamic>型オブジェクトを元にクラスのインスタンスを生成するメソッドです。fromJsonというfactoryコンストラクタを用意し、その中で呼び出すことが多いようです。

_$クラス名ToJsonメソッドはインスタンスを元にMap<String, dynamic>型オブジェクトを生成するメソッドです。toJsonというメソッドを用意し、その中で呼び出すことが多いようです。

item.dart

import 'package:json_annotation/json_annotation.dart';

// 生成されたコードを取り込む
// この記述をコード生成前に記述しないとコード生成されない
part 'item.g.dart';

// コード生成の対象とするためのアノテーション
()
class Item {
  final int id;
  final String title;

  Item({required this.id, required this.title});

  factory Item.fromJson(Map<String, dynamic> json) => _$ItemFromJson(json);

  Map<String, dynamic> toJson() => _$ItemToJson(this);
}

コード生成を行うにはこちらのコマンドを実行します。

flutter pub run build_runner build

変更が反映されない場合は一度クリーンアップして再実行します

flutter pub run build_runner clean
flutter pub run build_runner build

通信処理に組み合わせる

クラスの用意ができたら通信処理に組み合わせて利用します。こちらのサンプルは上述した通信のサンプルとほぼ同じです。違いはクラス名のみです。今回はItemクラスを使っていますが、以前はDataクラスという名前でした。

with_package_fetch.dart(Data → Itemに変更)

import 'dart:convert';
import 'package:async_sample/item.dart';
import 'package:http/http.dart' as http;

// GETリクエストを送信して結果をData型リストで返却する
Future<List<Item>> sendGetReqest2() async {
  // 送信先URLを準備
  final url = Uri.parse('http://localhost:3000/sample'); // その他
  // final url = Uri.parse('http://10.0.2.2:3000/sample'); // Android

  // GETリクエストを送信(非同期)
  final response = await http.get(url);

  // // ステータスコードを確認
  // if (response.statusCode == 200) {
  final json = response.body; // String型
  final rawList = jsonDecode(json); // List<dynamic>型
  final itemList =
      rawList.map<Item>((json) => Item.fromJson(json)).toList(); // List<Data>型
  // } else {
  //   通信失敗時の処理;
  // }
  print(itemList);
  return itemList;
}

// POSTリクエストを送信
Future<void> sendPostReqest2() async {
  // 送信先URLを準備
  final url = Uri.parse('http://localhost:3000/sample');
  // Content-Typeヘッダーを準備
  final headers = {'Content-Type': 'application/json'};
  // 送信データの元になるオブジェクトを用意
  // final data = Data(id: '5', title: 'fivefive');
  final item = Item(id: 5, title: 'fivefive');

  // POSTリクエストを送信(非同期)
  final response = await http.post(
    url,
    headers: headers,
    // JSONにエンコードしてボディに指定
    body: jsonEncode(item.toJson()),
  );
}

おわりに

非同期処理ではFuture型が利用され、async/awaitキーワードを付与した書き方をします。

通信はhttpパッケージを利用し、JSONとオブジェクトの相互変換を行うjson_serializableの導入も選択肢に上がります。

一見難しそうでしたが、JSONとオブジェクトの変換部分が少しややこしいだけで他は割と素直な印象でした。

Discussion