この前Dart Meetup Tokyo #5で発表してきました。
その時のスライドはこちら
FlutterとAngularDartを DIとClean Architectureでいい感じにする
このスライドの中ではFlutterのDIに関してgoogleが出しているinject.dartというコンパイル時のDIライブラリーに触れているのですが今回はそれの使い方を書いてみます。
今回のソースコードはこちらです。
inject.dart
ざっくりまとめると
- Daggerにインスパイヤされている
- Google、Dartの公式ではない
- プラットフォーム非依存なのでDartで書いたアプリなら理論的にはなんでも使える
- コンパイル時DI
- pubには上がっていない
flutterチームやDartチームの公式ライブラリーではありません。考え方としてはDaggerと同じです。Spring Frameworkに代表されるような実行時のリフレクション等によってDIするのではなく、コンパイル時にDIするアプローチになります。そのためSpringやGoogle Guiceに慣れている人からすると冗長な書き方に違和感を覚えるかもしれません(自分もそうでした)。スマホアプリ向けだとバイナリサイズだったり実行速度の問題への解決策としてコンパイル時のDIを選択しているようです。
また、現時点ではpubに上がっていないのと、そのままではflutterのlive reloadが使えないのでローカルにソースごとcloneして使わないといけません。Google社内のリポジトリで使用しているライブラリの部分を抜き出してプレビュー版として提供されているという事も注意すべき点です。
使い方
cloneとyamlの設定
inject.dartからソースをcloneしましょう。
その後に別途作成したflutterのプロジェクトにおけるyamlを下記のように修正します。
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.2
# cloneしたディレクトリ直下のpackage/injectを追加
inject:
path: ../inject.dart/package/inject
dev_dependencies:
# DI用ソースコードを自動生成するためbuild_runnerを追加
build_runner: "^0.9.0"
flutter_test:
sdk: flutter
# cloneしたディレクトリ直下のpackage/inject_generatorを追加
inject_generator:
path: ../inject.dart/package/inject_generator
# バージョンの差異でflutter packages getが通らないので依存関係をオーバーライド
dependency_overrides:
quiver: "0.29.0+1"
collection: "1.14.6"
cloneしたinject.dartのpackageディレクトリ直下にinjectとinject_generatorがあります。
injectにはDIの定義に必要なアノテーションのソースが存在し、inject_generatorにはその情報を基にDI用のソースコードを生成するための処理があります。なのでinjectの方をdependenciesに追加し、inject_generatorの方はdev_dependenciesに追加します。このままでは自動生成は行われないのでbuild_runnerも追加します。
inject_generatorの修正
前述したようにbuild_runnerを使用してソースの自動生成を行うのですが、ここで注意が必要です。
build_runnerを使ってソースコードを自動生成する場合に、生成されたコードの出力先は現時点では2つあります。
- プロジェクトディレクトリ直下の
.dart_tool/build/generated
- 自動生成対象ソースが存在するディレクトリ
前者はAngularDart
や今回のinject.dart
等です。後者はjson_serializable
等です。
ただし、flutterは現時点で.dart_tool
に出力されたソースをコンパイルの対象としていません。
このあたりはissueとして挙げられていて、今後解決されるようですが現時点での解決策としては出力先を変更するしかないようです。
なのでinject_generator
の出力先を変更するために。cloneしたソースを修正します。修正対象はinject_generator/build.yaml
です。
builders:
inject_generator:
target: ":inject_generator"
import: "package:inject_generator/inject_generator.dart"
builder_factories:
- "summarizeBuilder"
- "generateBuilder"
build_extensions:
".dart":
- ".inject.summary"
- ".inject.dart"
auto_apply: dependents
## このbuild_toの値をcacheからsourceへ修正
build_to: source
build_to
の値がcacheの場合は.dart_tool
にソースが出力され、source
の場合は生成対象と同じ場所に出力されます。なので上記のようにbuild_to
の値をsource
へ修正します。後述するようにbuilderは自プロジェクト直下に定義したbuild.yamlにて定義のオーバーライドが可能なのですが現時点ではこの部分に関してはオーバーライド出来ないため直接修正する必要があります。
プロジェクト直下にbuild.yamlを追加
自プロジェクトの直下にbuild.yamlを追加して、自動生成対象ファイルを指定します(build_runnerやbuild.yamlに関する詳細な説明は今回は省略します)
targets:
$default:
builders:
inject_generator:
generate_for:
- lib/src/di/**.dart
- lib/src/di/**.summary
DIの定義に関するソースに関して今回はlib/src/di
直下に配置するので今回は上記のように定義します。このパスはプロジェクトのディレクトリ構成に合わせて修正して下さい。build_runner実行時にまず*.summary
というDIメタデータのJsonが出力され、その情報を基にDI用Dartファイルが生成されるため*.summary
の定義を忘れないようにして下さい。
以上で事前準備が終了です。
クラスの定義
今回は単純にName
の依存関係を解決した状態でEmployee
を取得出来るようにします。
import 'package:flutter_app01/src/name.dart';
class Employee {
Name name;
Employee(this.name);
}
class Name {
String nameValue;
Name(this.nameValue);
}
DIの定義
作成するのはModule
とInjector(ServiceLocator)
です。
Moduleにはどのように依存関係を解決するか、ライフサイクルはどうするか等を定義します。
import 'package:flutter_app01/src/employee.dart';
import 'package:flutter_app01/src/name.dart';
import 'package:inject/inject.dart';
@module
class SampleModule {
@provide
Employee provideEmployee(Name name) => new Employee(name);
@provide
Name provideName() => Name("test");
}
@module
のアノテーションをクラスに定義し、依存関係の解決方法を定義するメソッドに@provide
アノテーションを定義します。
@asynchronous
で非同期初期化ができたり@singleton
でシングルトンにしたりQualifier
を使って同一の型に対してアノテーションによるインジェクトを変えたり出来ます。
import 'package:flutter_app01/src/di/sample_module.dart';
import 'package:flutter_app01/src/employee.dart';
import 'package:inject/inject.dart';
@Injector(const [SampleModule])
abstract class SampleLocator {
@provide
Employee getEmployee();
}
abstract
なクラスとしてInjector(ServiceLocator)
を定義します。@Injector
を追加し、その引数として先程定義したModuleのクラスを指定します。
DIのソースを自動生成
プロジェクト直下のディレクトリにて下記コマンドを実行します。
flutter packages pub run build_runner build
そうするとlib/src/di
直下に自動生成されたソースが出力されます。
その後に上記SampleLocator
を下記のように修正します。
import 'package:flutter_app01/src/di/sample_module.dart';
import 'package:flutter_app01/src/employee.dart';
import 'package:inject/inject.dart';
import 'sample_locator.inject.dart' as generated; //自動生成されたソースをimport
@Injector(const [SampleModule])
abstract class SampleLocator {
// ServiceLocator取得用メソッドの追加
static create(SampleModule module) => generated.SampleLocator$Injector.create(module);
@provide
Employee getEmployee();
}
あとはこの処理をflutter側で呼び出すだけです。
Flutter側で呼び出す
import 'package:flutter/material.dart';
import 'package:flutter_app01/src/di/sample_locator.dart';
import 'package:flutter_app01/src/di/sample_module.dart';
SampleLocator locator;
void main() async {
// Service Locator取得
locator = await SampleLocator.create(SampleModule());
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text("DI sample"),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
//EmployeeからName取得し、その値を表示
locator.getEmployee().name.nameValue
)
],
),
),
)
);
}
}
main関数の最初で初期化し、必要な箇所でService Locatorから必要なオブジェクトを取得する感じです。
まとめ
inject.dartはGoogleの社内リポジトリーからオープンソース化されたものです。Daggerと考え方が同じとういう事もあり、今後Flutterにてビルトインされたり、もしくはDartチーム公式のDIフレームワークが出てきた場合でも考え方、使い方はほとんど変わらないと思います。そういう意味でinject.dartを使っておけば今後のマイグレーションには苦労しない可能性は高いのではないでしょうか。
ただ、前述のとおり、pubには上がっていないし、flutterで使うためには修正しないといけません。build_runnerやbuild.yamlに関する知識もある程度必要です。そのあたりのコストをどう捉えるかがこのinject.dart
を使うかの分かれ目かなと。
おそらく後々Flutterにビルトインされる場合は現状のService Locatorのような使い方ではなくきちんとしたDIコンテナとして使えるようになると信じています。現状だと「制御の反転」ではないのでDIというのは個人的に違和感があります。なのでFlutterチームには期待してます。