Flutterでinject.dartを使う

この前[Dart Meetup Tokyo #5](https://dartisans-jp.connpass.com/event/91133/)で発表してきました。

その時のスライドはこちら
[FlutterとAngularDartを DIとClean Architectureでいい感じにする](https://speakerdeck.com/takayukihayashi/fluttertoangulardartwo-ditoclean-architecturede-iigan-zinisuru)

このスライドの中ではFlutterのDIに関してgoogleが出している[inject.dart](https://github.com/google/inject.dart)というコンパイル時のDIライブラリーに触れているのですが今回はそれの使い方を書いてみます。

今回のソースコードは[こちら](https://github.com/takayuki-hayashi/flutter-with-inject.dart)です。

# inject.dart

ざっくりまとめると

 - [Dagger](https://google.github.io/dagger/)にインスパイヤされている
 - Google、Dartの公式ではない
 - プラットフォーム非依存なのでDartで書いたアプリなら理論的にはなんでも使える
 - コンパイル時DI
 - pubには上がっていない

　flutterチームやDartチームの公式ライブラリーではありません。考え方としては[Dagger](https://google.github.io/dagger/)と同じです。Spring Frameworkに代表されるような実行時のリフレクション等によってDIするのではなく、コンパイル時にDIするアプローチになります。そのためSpringやGoogle Guiceに慣れている人からすると冗長な書き方に違和感を覚えるかもしれません（自分もそうでした）。スマホアプリ向けだとバイナリサイズだったり実行速度の問題への解決策としてコンパイル時のDIを選択しているようです。
　また、現時点ではpubに上がっていないのと、そのままではflutterのlive reloadが使えないのでローカルにソースごとcloneして使わないといけません。Google社内のリポジトリで使用しているライブラリの部分を抜き出してプレビュー版として提供されているという事も注意すべき点です。

# 使い方

## cloneとyamlの設定
[inject.dart](https://github.com/google/inject.dart)からソースをcloneしましょう。
その後に別途作成したflutterのプロジェクトにおけるyamlを下記のように修正します。

```yaml:pubspec.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として挙げられていて](https://github.com/flutter/flutter/issues/16603)、今後解決されるようですが現時点での解決策としては出力先を変更するしかないようです。
なので`inject_generator`の出力先を変更するために。cloneしたソースを修正します。修正対象は`inject_generator/build.yaml`です。

```yaml:inject.dart/package/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に関する詳細な説明は今回は省略します）

```dart: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`を取得出来るようにします。

```dart:lib/src/employee.dart
import 'package:flutter_app01/src/name.dart';

class Employee {
  Name name;
  Employee(this.name);
}
```

```dart:lib/src/name.dart
class Name {
  String nameValue;
  Name(this.nameValue);
}
```

## DIの定義
　作成するのは`Module`と`Injector(ServiceLocator)`です。
Moduleにはどのように依存関係を解決するか、ライフサイクルはどうするか等を定義します。

```dart:lib/src/di/sample_module.dart
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`を使って同一の型に対してアノテーションによるインジェクトを変えたり出来ます。


```dart:lib/src/di/sample_locator.dart
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`を下記のように修正します。

```dart:lib/src/di/sample_locator.dart
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側で呼び出す

```dart
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チームには期待してます。