Edited at
DartDay 17

AngularDartでHTTP(REST)通信

More than 1 year has passed since last update.

Dart Advent Calendar 2017 十七目の記事です。

今回はフロントエンドのアプリとしては必須であるHTTP通信処理に関して書いていきます。

今回のソースはこちらです。


使用するライブラリーとか

AngularDartのバージョンは5.0+alphaです。4.0でも内容は変わらないので最新版にしてます。


  • http

  • json_serializable

  • build_runner


http

dart公式のHTTPライブラリーです。必要な機能は十分揃っているし、シンプルで使いやすいです。

AngularDartで使用する場合はこのライブラリの中のBrowserClientというクラスをDIする形で使用する事になります。

まずはDIの管理下に置くためにエントリーポイントのクラスにてBrowserClientを登録します。


main.dart

import 'package:angular/angular.dart';

import 'package:rest_client_example/app_component.dart';
import 'package:http/browser_client.dart';

void main() {
bootstrap(AppComponent,[
provide(BrowserClient, useFactory: () => new BrowserClient(), deps: [])
]);
}


上記によりAngularでいうServiceにてDI可能になりました。実際の使用例は下記になります。

import 'dart:async';

import 'dart:convert';

import 'package:angular/angular.dart';
import 'package:http/browser_client.dart';

@Injectable()
class SampleService {
BrowserClient _http;
SampleService(this._http);

Future<Null> execute() async {
var response = await _http.get("http://ip.jsontest.com/");
print(JSON.decode(response.body));
}
}

DIに関する説明は省略しますが、main.dart(エントリーポイントのクラス) にて定義していれば何処でもコンストラクタインジェクションが可能になります。

さて、これでhttp通信が可能になったのですが上記だけだと JSON.decode でdecodeしているとはいえ些か扱いづらいです。そこで使用するのが次の json_serializable build_runner になります。


json_serializable

JSONのserialize及びdeserializeを行うためのDart公式ライブラリーです。

DartをWebで使用する場合、いわゆるreflectionの機能は処理速度に大きな問題が出るためあまり推奨されていません(このあたりは最終的にJSへトランスパイルされる事による制約なのだと思います。flutterなどではどうなのか調べていません。)なのでAngularDartでレスポンス情報等をMapではなく普通のクラス、オブジェクトとして扱いたい場合はMapの情報を丁寧にオブジェクトにバインディングしていかないといけないです。しかし、そんな事を一々書くのは面倒、でもクラスとして扱いたい場合にこのjson_serializableを仕様してソースコードを自動生成します。

ちなみにこの自動生成処理は実行する順番とかが重要です。公式サイトには最終形態のソースしか載ってないので安易にソースだけ見て理解しようとすると「???」ってなると思います。

たとえば http://ip.jsontest.com/ にて返却されるレスポンスが下記JSONだとします。

{

"ip": "XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX"
}

その場合、まずは下記のようなクラスを作成します。

library sample_json;

import 'package:json_annotation/json_annotation.dart';

@JsonSerializable()
class SampleJson {
final String ip;
SampleJson(this.ip);
}

SampleJsonクラス自体は特筆する点はないです。JSONのキーとなる属性を持つ普通のクラスです。そしてこのクラスに @JsonSerializable のアノテーションを付けることでSerialize対象のクラスだと宣言します。

そして大事且つ忘れがちなのが先頭の library 宣言です。このlibrary宣言を忘れると後述する自動生成を実行した場合にエラーが出てきちんと自動生成してくれません。なのでこの宣言を忘れないようにしましょう。

そして、この段階で一旦自動生成をします。そうすると library名.g.dart というファイル(今回であればsample_json.g.dart)が生成されるので part を使用してこのライブラリの一部だと宣言します(libraryやpartの詳しい説明は割愛します)。そして自動生成されたソースを利用してSerializeとdeserializeの処理を追加します。

library sample_json;

import 'package:json_annotation/json_annotation.dart';

part 'sample_json.g.dart'; //ここを追加

@JsonSerializable()
class SampleJson extends Object with _$SampleJsonSerializerMixin{ //serialize処理をmixin
final String ip;
SampleJson(this.ip);

//deserialize処理を追加(_$SampleJsonFromJsonは自動生成された処理)
factory SampleJson.fromJson(Map<String, dynamic> json) => _$SampleJsonFromJson(json);
}

んで、これを使ったRepository的なものは下記になります。

@Injectable()

class SampleRepository {
BrowserClient _http;
SampleRepository(this._http);

Future<SampleJson> get() async {
var response = await _http.get("http://ip.jsontest.com/");
return new SampleJson.fromJson(JSON.decode(response.body) as Map<String, dynamic>);
}
}

リフレクションでサクっと書けないのがつらい所です。

ちなみにオブジェクトのネストをする際の最終的なソースは下記のような感じになりますが、必ずネストされる側のソースを書き終えてからでないとエラーが出るので注意して下さい。

今回で言えばItemJsonで自動生成し、MixinとfromJsonの追加をした後にCartJsonで自動生成し、MininとfromJsonの追加する必要があります。

@JsonSerializable()

class ItemJson extends Object with _$ItemJsonSerializerMixin{
final String id;
final String name;
ItemJson(this.id, this.name);
factory ItemJson.fromJson(Map<String, dynamic> json) => _$ItemJsonFromJson(json);
}

@JsonSerializable()
class CartJson extends Object with _$CartJsonSerializerMixin{
final List<ItemJson> items;
CartJson(this.items);
factory CartJson.fromJson(Map<String, dynamic> json) => _$CartJsonFromJson(json);
}

さて、上記だけだと自動生成処理がないので最後に自動生成の説明です。


build_runner

上記で書いた library名.g.dart は自動生成にて作成します。

プロジェクトルート直下にtoolディレクトリを作成し、下記ファイルを作成します。


build_actions.dart

import 'package:build_runner/build_runner.dart';

import 'package:json_serializable/json_serializable.dart';

List<BuildAction> get buildActions => [
new BuildAction(
jsonPartBuilder(),
'rest_client_example',
inputs: const [
'lib/src/json/*.dart',
],
),
];


BuildActionの2つ目の引数が実は重要で、ここには pubspec.yamlname に指定した名前を定義しないといけないです。 inputs には自動生成したいソースの情報を定義します。その後下記ファイルを同じtoolディレクトリに作成します。


build.dart

import 'dart:async';

import 'dart:io';
import 'package:build_runner/build_runner.dart';
import 'build_actions.dart';

Future<Null> main() async {
var result = await build(buildActions, deleteFilesByDefault: true);
if (result.status == BuildStatus.failure) {
exitCode = 1;
}
}


そしてこの build.dart を実行すればソースが自動生成されます。

修正したら自動で検知して自動生成して欲しい場合は下記ファイルを作成して実行しておけばコードの修正をwatchして自動生成をバックグラウンドで実行してくれます。


watch.dart

import 'package:build_runner/build_runner.dart';

import 'build_actions.dart';

void main() {
watch(buildActions, deleteFilesByDefault: true);
}


ここまでくればあとはソースをごりごり書くだけです。

正直面倒くさいですし、もっと簡潔に書きたいからなんとかならないかなぁと思うのですが現状ではこれが一番良さそうです(@proxy などを駆使するライブラリなどもあるのですが、それはそれで現状ではIDEのコード補完が効かないので微妙です・・・)

以上、AngularDartでHttp通信するやり方の紹介でした。