2017/04/28

DaggerのAndroid拡張を導入する(v2.11-rc1)

Dagger 2.11-rc1

Dagger2.10でdagger.androidモジュールがリリースされました.
本稿ではDagger2.10と2.11でリリースされたdagger.androidモジュールの使い方について簡単に紹介したいと思います.

本題へ入る前に, Dagger2.11では当然, 歴代のバージョンで追加されてきた機能を土台にしています.
Daggerを触ったことがない人は Android: Dagger2 を.
Subcomponentを使ったことがない人はAndroid: Dagger2 - Subcomponent vs. dependenciesを.
マルチバインディングを使ったことがない人はDagger2. MultibindingでComponentを綺麗に仕上げるを一度読んでから本稿に戻ってくると理解しやすいと思います.

また今回紹介するコードのリポジトリは下記に公開してあります.
Dagger2.11正式リリースタイミングでも更新していくので, よろしければ ⭐️ をお願いします :)

YukiMatsumura/AndroidDaggerSample

Dagger(Dependency Injection)を最大限に活かせるのは, 依存オブジェクトをDagger自身が生成して, 依存性を満たすようにデザインすることでしょう. しかし, AndroidはActivityやFragmentといったOSが生成・管理するオブジェクトがあり, Daggerが全てを生成・管理することができません.
そうした場合, 次のようにフィールドインジェクションを使って依存性を満たすことになります.

public class MainActivity extends Activity {
  @Inject Hoge hoge;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // 必ず最初に実行すること!
    ((App) getContext().getApplicationContext())
        .getApplicationComponent()
        .newActivityComponentBuilder()
        .activity(this)
        .build()
        .inject(this);
    // ...
  }
}

これにはいくつかの問題があります.

  1. まず, ActivityやFragment, Service, ContentProviderといったOS管理のクラスへインジェクションする数だけコピペコードが出来上がり, メンテナンス性を悪くします.
  2. そしてなにより, クラスが依存性を注入するオブジェクト(ComponentやModules)のことについてそれぞれのクラスが知っている必要があるため, Dependency Injectionのコア原則を破っています.

今回紹介するdagger.androidモジュールを導入すると, これらの問題を解決することができます.

NOTE:
android.daggerモジュールはまだBetaバージョンのため今後変更される可能性があります.  
今でもクラス名がリネームされるなどしているため, 他でコードを参考にされる場合はdaggerのバージョンに注意する必要があります.  

本稿では現時点で最新のリリースバージョンDagger2.11-rc1を対象にしています.  
StableのDagger2.10からの変更点もありますので, Dagger2.10を使う場合は変更点にご注意ください.  

Dagger2.10 -> 2.11の変更点:
 - New API: @ContributesAndroidInjector simplifies the usage of dagger.android
 - All HasDispatching*Injectors are renamed to Has*Injector. They also return an AndroidInjector instead of a DispatchingAndroidInjector
 - Added DaggerApplication and DaggerContentProvider

リネーム情報はGitHubのリリースページに記載されています.  
https://github.com/google/dagger/releases

依存ライブラリの追加

まずはDagger2.11のライブラリを追加しないとはじまりません.
build.gradleのdependenciesに次のライブラリを追加します.

  // Core dependencies
  compile 'com.google.dagger:dagger:2.11-rc1'
  annotationProcessor 'com.google.dagger:dagger-compiler:2.11-rc1'

  // Android dependencies
  compile 'com.google.dagger:dagger-android:2.11-rc1'
  annotationProcessor 'com.google.dagger:dagger-android-processor:2.11-rc1'

  // Require if use android support libs.
  compile 'com.google.dagger:dagger-android-support:2.11-rc1'

dagger-android-*なモジュールがDaggerのAndroid拡張です.
プロジェクトでサポートライブラリを使用している場合はdagger-android-supportも必要です.

余談ですが, 手元の環境ではfindbugsのdependencyでコンフリクトが起きたので, 合わせて解消しています.

エラー:
Error:Conflict with dependency 'com.google.code.findbugs:jsr305' in project ':app'. Resolved versions for app (3.0.1) and test app (2.0.1) differ. See http://g.co/androidstudio/app-test-app-conflict for details.

解決: espresso-coreの依存モジュールからjsr305をexcludeしておく
  androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
    exclude group: 'com.android.support', module: 'support-annotations'
    exclude group: 'com.google.code.findbugs', module: 'jsr305'
  })

Daggerのライブラリを取得したらComponent, Moduleを作成していきましょう.

ActivityのComponent/Module作成

順を追って必要なオブジェクトを作って行きます. まずはMainActivityに紐づくMainComponentの定義から.

MainComponentはこのあと作るアプリケーションコンポーネントのサブコンポーネントとして定義するので@Subcomponentアノテーションをつけます.
さらに, コンポーネントビルダー@Subcomponent.Builderを同じく宣言します.

package com.yuki312.androiddaggersample;

import dagger.Subcomponent;
import dagger.android.AndroidInjector;

@Subcomponent
public interface MainComponent extends AndroidInjector<MainActivity> {
  @Subcomponent.Builder
  abstract class Builder extends AndroidInjector.Builder<MainActivity> {
  }
}

MainComponentにはAndroidInjectorインタフェースを継承させます.
AndroidInjectorはAndroidのコアコンポーネント(Activity, Fragment, Service, BroadcastReceiver, ContentProvider)に依存性を注入するメソッドinject(T)を定義したインタフェースです.

次にMainModuleを定義します.

import android.app.Activity;
import dagger.Binds;
import dagger.Module;
import dagger.android.ActivityKey;
import dagger.android.AndroidInjector;
import dagger.multibindings.IntoMap;

@Module
public abstract class MainModule {

  @Binds @IntoMap @ActivityKey(MainActivity.class)
  abstract AndroidInjector.Factory<? extends Activity> bindInjectorFactory(
      MainComponent.Builder builder);
}

@ActivityKeyでのMainActivity.class指定は, 後ほど説明する適切なAndroidInjector.Builderを選択するための型情報に必要なものです.
Androidの各コアコンポーネント専用のInjectorを生成するファクトリをここで指定します. AndroidInjectorについては後ほど説明します.

続いてアプリケーションクラス用のAppModule.

package com.yuki312.androiddaggersample;

import dagger.Module;

@Module(subcomponents = { MainComponent.class })
public class AppModule {
}

そしてAppComponent.

package com.yuki312.androiddaggersample;

import dagger.Component;
import dagger.android.AndroidInjector;
import dagger.android.support.AndroidSupportInjectionModule;

@Component(modules = { AndroidSupportInjectionModule.class, AppModule.class, MainModule.class })
public interface AppComponent extends AndroidInjector<App> {

  @Component.Builder
  abstract class Builder extends AndroidInjector.Builder<App> {
  }
}

modules={...}にはインジェクションモジュールを含める必要があります.
インジェクションモジュールには次の種類が用意されています.

  • AndroidInjectionModule.class(サポートライブラリを使わない場合)
  • AndroidSupportInjectionModule.class(サポートライブラリを使う場合)

インジェクションモジュールには, AndroidのコアコンポーネントにinjectするComponent/SubComponentのファクトリクラスであるAndroidInjector.Factoryを値に持つMapがAndroidコアコンポーネント毎に定義されており, それぞれのインスタンスはマルチバイインディングの仕組みで構築されています.

@Module
public abstract class AndroidInjectionModule {
 @Multibinds
  abstract Map<Class<? extends Activity>, AndroidInjector.Factory<? extends Activity>>
      activityInjectorFactories();

  @Multibinds
  abstract Map<Class<? extends Fragment>, AndroidInjector.Factory<? extends Fragment>>
      fragmentInjectorFactories();

  @Multibinds
  abstract Map<Class<? extends Service>, AndroidInjector.Factory<? extends Service>>
      serviceInjectorFactories();
 ...

AndroidInjectionModule, AndroidSupportInjectionModuleAndroidInjector.Factoryの管理に必要であることがわかります.
アプリケーション全体に渡るコアコンポーネントを管理するため, 基本的にはApplicationスコープのコンポーネントで管理することになります.
AppComponentにはビルダーAndroidInjector.Builderも忘れずに定義しておきます.

DaggerApplication

次にApplicationクラスの定義です.
Applicationクラスには各Androidコアコンポーネント用のAndroidInjectorを定義する必要があります.
AndroidInjectorはActivityやFragmentといったコアコンポーネントに依存性を注入するためのインジェクター用のインタフェースです.
コアコンポーネント用のインジェクターには次のものがあります.

  • HasActivityInjector
  • HasFragmentInjector,
  • HasServiceInjector,
  • HasBroadcastReceiverInjector,
  • HasContentProviderInjector
  • HasSupportFragmentInjector(dagger-android-support)

それぞれのインタフェースには各コアコンポーネント専用のインジェクターを返すメソッドが定義されているわけですが, Applicationクラスでこれら全てのインジェクターを実装するのは面倒なので, Dagger2.11ではDaggerApplicationクラスが提供されました.

  • dagger.android.DaggerApplication(サポートライブラリを使わない場合)
  • dagger.android.support.DaggerApplication(サポートライブラリを使う場合)

Dagger2.11-rc1ではサポートライブラリ対応/非対応でクラス名が同じなのでextendsする際には注意が必要です.
また, DaggerApplicationはApplication用のインジェクターを返すapplicationInjectorをabstractメソッドとして定義してあるので, これをオーバーライドしておきます.
これで, Applicationクラスへのフィールドインジェクションもサポートされます.

package com.yuki312.androiddaggersample;

import dagger.android.AndroidInjector;
import dagger.android.support.DaggerApplication;

public class App extends DaggerApplication {

  @Override protected AndroidInjector<? extends DaggerApplication> applicationInjector() {
    return DaggerAppComponent.builder().create(this);
  }
}

仕上げ

最後の仕上げにMainActivityでフィールドインジェクションを実装しましょう.

package com.yuki312.androiddaggersample;

...
import dagger.android.AndroidInjection;

public class MainActivity extends AppCompatActivity {

  ...

  @Override protected void onCreate(Bundle savedInstanceState) {
    AndroidInjection.inject(this);
    super.onCreate(savedInstanceState);
    ...
  }
}

AndroidInjection.inject(this);. たったこれだけです! 簡単ですね:)
従来のComponentやModuleの指定が現れないのでDependency Injectionの原則にも忠実です.

おまけ

dagger-android-support は何者か

dagger.androidの肝はAndroidコアコンポーネントへのインジェクションサポートです.
今回登場した HasSupportFragmentInjector, AndroidSupportInjectionModule, dagger.android.support.DaggerApplicationが主にサポートライブラリ向けのクラスになります.
これらの中身を覗くと, android.support.v4.app.Fragmentのためのバインディングマップであったり, インジェクターであったりの処理が定義されています.
つまり, サポートライブラリのFragmentを使ったinjectionをサポートするためにこれらのライブラリが必要になってきます.
サポートライブラリのFragmentを使わないのであれば必ずしも必要というわけではなさそうですね.

コアコンポーネントのInjectorはどうやって選ばれる?

ActivityやFragmentといったコアコンポーネントのインジェクターはAndroidInjectionModuleに定義されたAndroidInjector.Factoryから生成することができますが, これが設定されているマルチバインディングで構築されたMapからファクトリインスタンスを取り出す操作はDispatchingAndroidInjectorが行なっています.
DispatchingAndroidInjectorはDaggerが生成するオブジェクトであるためアプリケーション側から直接触ることはないと思いますが, dagger.androidの内部動作を把握するには押さえておく必要のあるクラスです.

ContentProviderInjectorとApplicationInjector

Androidの仕組み上, アプリケーションプロセスがCygoteからforkされて開始される際, ContentProviderの初期化はApplicationの初期化より早いです.
つまり, ActivityやBroadcastReceiver, Serviceなど他のコアコンポーネントと唯一異なってContentProviderのonCreate時にはまだApplicationクラスが初期化(onCreate)されていない可能性があります.
DaggerApplicationクラスを覗くとこの辺りをどう解決しているのかをうかがい知ることができます.

  // injectIfNecessary is called here but not on the other *Injector() methods because it is the
  // only one that should be called (in AndroidInjection.inject(ContentProvider)) before
  // Application.onCreate()
  @Override
  public AndroidInjector<ContentProvider> contentProviderInjector() {
  ...


  /**
   * Lazily injects the {@link DaggerApplication}'s members. Injection cannot be performed in {@link
   * Application#onCreate()} since {@link android.content.ContentProvider}s' {@link
   * android.content.ContentProvider#onCreate() onCreate()} method will be called first and might
   * need injected members on the application. Injection is not performed in the the constructor, as
   * that may result in members-injection methods being called before the constructor has completed,
   * allowing for a partially-constructed instance to escape.
   */
  private void injectIfNecessary() {
    if (needToInject) {

この他にも, コアコンポーネントのComponent/Module定義を簡略化できる@ContributesAndroidInjectorや, コアコンポーネントインスタンスをパラメータにとるProviderメソッドの提供方法などもありますが, 本稿では割愛します.

ひとまず, dagger.androidパッケージがどのようなものになる予定なのか, 本稿で大まかにでも掴めたようでしたら幸いです.
rcがとれて, Dagger2.11が正式リリースされたタイミングで俯瞰図なども描きたいと思います.

以上です.

2017/04/27

DataBindingでViewのtagにenumを設定する

DataBindingを使えばViewのtagフィールドに好きなオブジェクトを差し込めるので↓のような実装を試してみました.
固定長リストをlayout.xmlで定義する際にenumを設定すれば, onClickリスナーでそれを取り出して使うことができます.

キャストする箇所がアレですが,,

<layout>
<data>
 <import type="hoge.foo.Type"/>
</data>

<LinearLayout
  ...
  >
    <TextView
      ...
      android:tag="@{Type.A}" />

    <TextView
      ...
      android:tag="@{Type.B}" />

    <TextView
      ...
      android:tag="@{Type.C}" />

    <TextView
      ...
      android:tag="@{Type.D}" />
</LinearLayout>
</layou>
@Override public void onClick(View v) {
  Object tag = v.getTag();
  if (tag == null || !(tag instanceof Type)) return;

  Type type = Type.class.cast(tag);
  ...
}

以上です.