はじめに
2017年に入ってDagger2もバージョン2.9を迎えました.
Androidでも使われることが多いDIフレームワークも, バージョンを重ねるごとに便利なAPIが増えています.
本稿はAndroidアプリを例に, Activity
に依存するComponent
からインジェクションする方法について, 過去のAPIを使用した方法と, 新しいAPIを使用した方法とで比較を行い, より綺麗なインジェクションを実現していきます.
今回紹介する内容+αとソースコードはGitHubにもアップしていますのでそちらもあわせてご覧ください :)
Subcomponent. 親と子の密結合問題
Androidでは, コンポーネントをアクティビティの単位で分割することがよくあります(e.g. MainActivityComponent
, SettingActivityComponent
, etc.)
そのようなコンポーネントが依存性の解決にアプリケーションスコープのオブジェクトを必要とする場合や, 他のコンポーネントからも参照される共有オブジェクトを持つ場合にSubcomponent
として定義されることがあります.
サブコンポーネントの仕組みは, それぞれのいくつかのコンポーネントが持つ共通部分をまとめて定義したり, スコープの観点からみた”親-子”を定義したりするのに大変便利です.
ただ, 古いバージョンのDaggerではサブコンポーネントから依存される親コンポーネントは, サブコンポーネントのクラスを知っている必要があり, 親と子の結合度が高くなってしまう問題がありました.
@Component(...)
public interface AppComponent {
// 親であるAppComponentは子にあたるサブコンポーネントを全て知っておく必要がある :(
MainActivityComponent plus(MainActivityModule module);
SettingActivityComponent plus(SettingActivityModule module);
この問題はDagger2.7で追加された@Module
のsubcomponent
属性を使うことで解決できます.
Module.subcomponents
@Module
のsubcomponent
属性はサブコンポーネントの親を指定するための新しい手段を提供します.
subcomponent
属性を持つモジュールは親コンポーネントと子コンポーネントの関係を築く橋渡し役になります.
このモジュールは親コンポーネントの@Component(modules=...)
に定義されることで, 親コンポーネントに属することになります.
// subcomponent属性には対象となるサブコンポーネントクラスを定義する
@Module(subcomponents = {MainComponent.class, SubComponent.class})
public abstract class ActivityBindingModule {
}
// 親コンポーネントのmodulesに上記モジュールを追加する
@Component(modules = {AppModule.class, ActivityBindingModule.class})
public interface AppComponent { … }
この方法を使う場合は@Module(subcomponents={...})
で指定したサブコンポーネントをどのように構築するのかを定義するコンポーネントビルダーが必要になります.
サブコンポーネントの内部インタフェースとしてビルダーを新たに宣言し, これに@Component.Builder
アノテーションをつけます.
また, (サブ)コンポーネントビルダーにはいくつかの実装ルールがあるのでそれに従います.
@Subcomponent(modules = MainModule.class)
public interface MainComponent {
@Subcomponent.Builder interface Builder {
Builder activityModule(MainModule module);
MainComponent build();
}
サブコンポーネントのビルダーはDaggerによってプライベートなインナークラスとして自動生成されます.
必要なビルダーはActivityBindingModule
をインストールした親コンポーネントから提供されます.
@Component(modules = { AppModule.class, ActivityBindingModule.class })
public interface AppComponent {
MainComponent.Builder mainComponentBuilder();
SettingComponent.Builder settingComponentBuilder();
ここまでの変更によって, 親コンポーネントが抱えていたサブコンポーネントとの密結合関係がコンポーネントビルダーとの密結合関係に変わりました.
まだ, 親コンポーネントはサブコンポーネントのことを知っている状態で, サブコンポーネントが増えると親コンポーネントもあわせて修正しないといけません.
次はマルチバインディングの機能を使ってこの問題に対処します.
Multibinding. コンポーネントマップの自動生成
Dagger2.4から, 生成したオブジェクトをコレクションにバインドするマルチバインディング機能が追加されました.
これにより, 値がプリセットされたSet
やMap
をインジェクションできるようになりました.
次はこのマルチバインディングを使ってサブコンポーネントビルダーのマップコレクションを作り, 親コンポーネントとコンポーネントビルダーの関係を疎にして, サブコンポーネントを柔軟に追加できる仕組みを作っていきます.
まずはじめに, マルチバインディングでマップに格納するKey
とValue
を決めておきます.
Androidではコンポーネットをアクティビティ単位で分割することが多いのでKey
にはアクティビティのクラスオブジェクトを格納し, Value
にはコンポーネントビルダーを格納することにします.
アプリケーションクラスが各アクティビティの詳細を知らなくてもいいように, アクティビティに直接関わるコンポーネントを抽象化したActivityComponent
インタフェースとモジュールを抽象化したActivityModule
インタフェースを定義します. これらはまだマーカーインタフェース扱いですが後々メソッドを定義していきます.
public interface ActivityComponent {
}
public interface ActivityModule {
}
// Activityと関連するコンポーネントとモジュールはこれを実装する
@Subcomponent(modules = MainComponent.MainModule.class)
public interface MainComponent extends ActivityComponent<MainActivity> {
...
@Module
class MainModule extends ActivityModule {
...
}
}
コンポーネントビルダーも抽象化します.
上記で定義したインタフェースActivityModule
をパラメータとして受け取り, ActivityComponent
を構築して返すビルダーパターンのインタフェースです.
public interface ActivityComponentBuilder<M extends ActivityModule, C extends ActivityComponent> {
ActivityComponentBuilder<M, C> activityModule(M activityModule);
C build();
}
// Activityと関連するサブコンポーネントビルダはこれを実装する
@Subcomponent.Builder
interface Builder extends ActivityComponentBuilder<MainModule, MainComponent> {
}
アクティビティがアクティビティコンポーネントを取得したい場合の手順は次の通りです.
ActivityComponentBuilder
の実装クラスにあたるビルダーインスタンスを取得- 必要な
ActivityModule
をビルダーに指定する build
メソッドでActivityComponent
を構築してコンポーネントインスタンスを取得
今時点ではまだコンポーネントビルダーを取得するために親コンポーネントは具体的なクラス名を指定する必要がある状態です.
@Component(modules = { AppModule.class, ActivityBindingModule.class })
public interface AppComponent {
MainComponent.Builder mainComponentBuilder();
SettingComponent.Builder settingComponentBuilder();
アクティビティがコンポーネントを取得する時にはAppComponent
経由で必要なコンポーネントビルダーを取得することになります.
現状でもまだ親コンポーネントからサブコンポーネントビルダーへの依存を分離できていません.
次はいよいよ, 用意したActivityComponent
, ActivityModule
, ActivityComponentBuilder
とマルチバインディング機能を使って依存関係を取り除いていきます.
マルチバインディングでマップに格納するオブジェクトを提供するにはキーにする型をはっきりさせる必要があります.
今回は, アクティビティのクラス情報をキーとするので専用のアノテーションを作成します.
キーとしてClass
を受け取り, その型をActivity
のサブクラスに制限するジェネリクスを指定しておくのがポイントです.
これで, マルチバインディングが行われるマップのキーにはActivity
のサブクラスだけが許可されるようになります.
@MapKey
public @interface ActivityMapKey {
Class<? extends Activity> value();
}
それでは, マルチバインディングの対象となるマップを定義しましょう.
マップを提供する方法は他のオブジェクトと同じくモジュールのメソッドとして定義します.
今回はサブコンポーネントを柔軟に追加できるように改良することが目的なので, これを定義するモジュールは@Module(subcomponents=...)
を宣言しているActivityBindingModule
が妥当でしょう.
ActivityBindingModule
にマルチバインディングされるマップの定義を追加したものが下記です.
@Module(subcomponents = {MainComponent.class, SubComponent.class})
public abstract class ActivityBindingModule {
@Provides @IntoMap @ActivityMapKey(MainActivity.class)
public ActivityComponentBuilder mainComponentBuilder(
MainComponent.Builder builder) {
return builder;
}
@Provides @IntoMap @ActivityMapKey(SettingActivity.class)
public ActivityComponentBuilder settingComponentBuilder(
SettingComponent.Builder builder) {
return builder;
}
}
ここでさらに, Dagger2.4から導入された@Binds
を使えば, こういったボイラープレートなプロバイダーメソッドの定義を簡略化できます.
Dagger2.5からはマルチバインディングに対しても使えるようになったので, これを使ってシンプルに定義したものが下記です.
@Module(subcomponents = {MainComponent.class, SettingComponent.class})
public abstract class ActivityBindingModule {
@Binds @IntoMap @ActivityMapKey(MainActivity.class)
public abstract ActivityComponentBuilder mainComponentBuilder(
MainComponent.Builder builder);
@Binds @IntoMap @ActivityMapKey(SettingActivity.class)
public abstract ActivityComponentBuilder settingComponentBuilder(
SettingComponent.Builder builder);
}
さて, これでClass<? extends Activity>
型をキーに持ち, 値にはActivityComponentBuilder
型が格納されるMap
を自動生成できるようになりました.
これを提供するプロバイダーメソッドは親コンポーネントにあたるAppComponent
に定義します.
@Component(modules = { AppComponent.AppModule.class, ActivityBindingModule.class })
public interface AppComponent {
Map<Class<? extends Activity>, ActivityComponentBuilder> activityComponentBuilders();
これによって, Daggerがマルチバインディングによってコンポーネントビルダーのマップを構築し, それをactivityComponentBuilders()
メソッド経由で提供するようになりました.
マルチバインディング適用前と異なる点は, もはやAppComponent
がサブコンポーネント(MainComponentBuilder
etc.)について知らなくてよくなったという点です.
今やAppComponent
は抽象化されたビルダーActivityComponentBuilder
のことだけ知っていればよいのです.
これでついに親コンポーネントがサブコンポーネントから独立しました Yay!
アクティビティとサブコンポーネントが追加されてもアプリケーションクラスを変更する理由はもはやありません.
アクティビティに関係するサブコンポーネントに追加・変更がある場合の修正はActivityBindingModule
に閉じています.
plus one
これで一通りの実装は完了ですが, もう一歩進めましょう.
アクティビティのコンポーネントにはinject
メソッドを定義することがよくあります.
なのでActivityComponent
にこれを定義します.
public interface ActivityComponent<T extends Activity> extends MembersInjector<T> {
}
もうひとつ, ActivityModule
にはアクティビティインスタンスを保持させることがよくあるので, その定義を追加しておきます.
@Module
public abstract class ActivityModule<T extends Activity> {
protected final T activity;
public ActivityModule(T activity) {
this.activity = activity;
}
@Provides public T provideActivity() {
return activity;
}
}
アクティビティコンポーネントを取得するときは, 下記の手順です.
ActivityComponentBuilder
の実装クラスにあたるビルダーインスタンスを取得- 必要な
ActivityModule
をビルダーに指定する build
メソッドでActivityComponent
を構築してコンポーネントインスタンスを取得
ActivityComponentBuilder
を取得するメソッドは以前と同様, 親コンポーネントにあたるAppComponent
に定義されているので, 下記の要領で取得します.
あとはビルダーに必要なモジュールを指定してコンポーネントを構築します.
// コンポーネントビルダーのマップを取得
Map<Class<? extends Activity>, ActivityComponentBuilder> map =
((AppComponent) context.getApplicationContext().activityComponentBuilders();
// コンポーネントビルダーのインスタンスを取得
MainComponent.Builder builder = (MainComponent.Builder)map.get(MainActivity.class);
// コンポーネントビルダーでコンポーネントを構築
MainComponent component = builder.activityModule(new MainModule(activity)).build();
いい感じですね. コンポーネントビルダーが取得できればコンポーネントを構築すれば目的のコンポーネントが取得できます.
コンポーネントが取得できれば, injectMembers(activity)
で依存性を注入できます.
それでは最後に主要なクラス達を整理しておきます.
// アクティビティのコンポーネントを表現するインタフェース
public interface ActivityComponent<T extends Activity> extends MembersInjector<T> {...}
// アクティビティのモジュールを表現する基底クラス
public abstract class ActivityModule<T extends Activity> {...}
// 具体的なアクティビティコンポーネントとそのビルダとモジュール
@Subcomponent(modules = MainComponent.MainModule.class)
public interface MainComponent extends ActivityComponent<MainActivity> {
@Subcomponent.Builder
interface Builder extends ActivityComponentBuilder<MainModule, MainComponent> {
}
@Module
class MainModule extends ActivityModule<MainActivity> {
...
}
}
// アクティビティモジュールのバインドを定義するクラス
@Module(subcomponents = { MainComponent.class })
public abstract class ActivityBindingModule {
@Singleton @Binds @IntoMap @ActivityMapKey(MainActivity.class)
public abstract ActivityComponentBuilder mainActivityComponentBuilder(
MainComponent.Builder builder);
}
// 親コンポーネントにあたるアプリケーションコンポーネント
@Component(modules = { AppModule.class, ActivityBindingModule.class })
public interface AppComponent {
Map<Class<? extends Activity>, ActivityComponentBuilder> activityComponentBuilders();
@Module
class AppModule { ... }
}
// アクティビティコンポーネントを取得するコード
Map<Class<? extends Activity>, ActivityComponentBuilder> map =
((AppComponent) context.getApplicationContext().activityComponentBuilders();
MainComponent.Builder builder = (MainComponent.Builder)map.get(MainActivity.class);
MainComponent component = builder.activityModule(new MainModule(activity)).build();
// 依存性を注入
component.injectMembers(activity);
Daggerの新しいAPIを使ってアクティビティコンポーネントを取得することができました :)
さらに, もう一歩進めてDaggerによってスコープ制御されているコンポーネントをMortarライブラリで更に使いやすくする方法がありますが, これは次の機会にします.
今回紹介した内容+αと動くソースコードをGitHubにもアップしています. そちらもあわせてご覧ください :)
以上です.