Dartの非同期処理とFlutterのHTTP通信の書き方
はじめに
皆さん、こんにちは。
今回はDartの非同期処理についてと、Flutterでの通信の書き方、それに伴いJSONの扱いについて取り上げています。
Future型
概要
- 非同期処理を扱うための型
- 非同期処理を扱う関数の戻り値はFuture型とする
- async/awaitキーワードで非同期処理を同期的に記述可能
Dartの実行環境はシングルスレッドなので非同期処理を活用します。非同期処理を扱う型はFuture
型です。非同期処理を行う関数の戻り値はFuture
型とし、具体的な結果の値は型パラメータで指定します。
// 結果なし(void)の非同期関数の宣言例
Future<void> showMessage() {
// 結果はString型の非同期関数の宣言例
Future<String> showMessage() {
非同期処理を記述する際はasync
とawait
キーワードを利用します。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文字列をList
やMap
に変換
GETリクエストでWebAPIからデータを取得する方法についてまとめます。やり取りするデータ形式はJSONを想定しています。
GETするJSONの形式
[
{
"id": 1,
"title": "abc"
}
]
POSTするJSONの形式
{
"id": 5,
"title": "fivefive"
}
httpパッケージの導入
Flutterで通信を行うにはhttpパッケージを導入します。
flutter pub add 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)
メソッドでList
やMap
を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
クラスの用意とコード生成
コード生成の前に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