20
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Flutterでinject.dartを使う

Last updated at Posted at 2018-07-22

この前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を下記のように修正します。

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として挙げられていて、今後解決されるようですが現時点での解決策としては出力先を変更するしかないようです。
なのでinject_generatorの出力先を変更するために。cloneしたソースを修正します。修正対象はinject_generator/build.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に関する詳細な説明は今回は省略します)

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

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

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

DIの定義

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

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

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を下記のように修正します。

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側で呼び出す

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

20
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?