2017年7月23日日曜日

Android で Dagger を使う(その1)

Dependency Injection

API にアクセスするための interface が用意されているとします。 interface ApiService { ... } この interface の実装クラスが何であるかや、どうインスタンス化するかを利用側(例えば Activity)では意識したくありません。
ApiService をどうインスタンス化するかは Activity 側の責務ではないからです。 public class MainActivity extends AppCompatActivity { // どうやってインスタンス化するかは知りたくない ApiService service; } 必要としているもの(dependency)を内部で生成するのではなく、外部から注入(injection)する手法が dependency injection(DI)です。

外部から注入すると
  • テストなど、状況に応じて注入するインスタンスを切り替えられる
  • インスタンス化の方法が変わっても利用側は影響をうけない
などの利点があります。

外部から注入する一番簡単な方法は、コンストラクタで渡すことです。 しかし Activity はフレームワークが生成するため、この方法はとれません。

↓これはできない public class MainActivity extends AppCompatActivity { ApiService service; public MainActivity(ApiService service) { this.service = service; } } そこで、インスタンス化を責務とするクラス(例えば Factory など)を用意して、Activity からはそれを使ってインスタンスを取得するようにします。 public class MainActivity extends AppCompatActivity { ApiService service; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); service = ApiServiceFactory.create(); setContentView(R.layout.activity_main); } } public class ApiServiceFactory { private static ApiService service; @NonNull public synchronized static ApiService create() { if (service == null) { final Retrofit retrofit = ...; return retrofit.create(ApiService.class); } return service; } } Retrofit では毎回インスタンスを生成するとコストが高いので Singleton にします。

この方法には以下のような欠点があります。
  • 外部から注入したいクラスごとに同じようなFactoryクラスが必要
  • どの Factory を使うかを結局知っていなくてはいけない
Dagger を使うと、これらの欠点を解消して DI を行うことができます。



Dagger

Dagger (https://google.github.io/dagger) は Java で DI を行うことをサポートするライブラリです。

「DI = Dagger を使うこと」ではありません。上記で書いたように Dagger を使わなくても DI はできます。 dependencies { compile 'com.google.dagger:dagger:2.11' annotationProcessor 'com.google.dagger:dagger-compiler:2.11' } Dagger では ComponentModule が主な構成要素です。

Component は injector です。inject される側(例だと Activity)は、Component を経由して必要なインスタンス(例だと ApiService のインスンタンス)をもらいます。

Module の責務はインスタンスの生成です。そのため、Module には inject するもの(例だと ApiService)をどうインスタンス化するかを定義します。

Module 用のクラスには @Module アノテーションをつけます。
inject するもの(ApiService のインスタンス)を返すメソッドを用意し、@Provides アノテーションをつけます。 @Module public class ApiModule { @Provides public ApiService provideApiService() { final Retrofit retrofit = ...; return retrofit.create(ApiService.class); } } 慣例で Module 用のクラスには Module suffix、@Provides をつけるメソッドには provide prefix をつけることが多いです。

Component は abstract class か interface で用意します。
@Component アノテーションをつけ、modules 属性に必要な Module クラスを指定します。 @Component(modules = ApiModule.class) public interface AppComponent { ApiService apiService(); } ビルドすると Component の実装クラスが生成されます。名前は Dagger + Component クラス名になるので、この場合だと DaggerAppComponent クラスが自動生成されます。 public final class DaggerAppComponent implements AppComponent { private Provider<ApiService> provideApiServiceProvider; private DaggerAppComponent(Builder builder) { assert builder != null; initialize(builder); } public static Builder builder() { return new Builder(); } public static AppComponent create() { return new Builder().build(); } @SuppressWarnings("unchecked") private void initialize(final Builder builder) { this.provideApiServiceProvider = ApiModule_ProvideApiServiceFactory.create(builder.apiModule); } @Override public ApiService apiService() { return provideApiServiceProvider.get(); } public static final class Builder { private ApiModule apiModule; private Builder() {} public AppComponent build() { if (apiModule == null) { this.apiModule = new ApiModule(); } return new DaggerAppComponent(this); } public Builder apiModule(ApiModule apiModule) { this.apiModule = Preconditions.checkNotNull(apiModule); return this; } } } (AppComponent や ApiModule の実装を変えると DaggerAppComponent の実装も変わります)


Module の @Provides がついたメソッドからは、対応する Factory が自動生成されます。例えば ApiModule の provideApiService() メソッドに対しては ApiModule_ProvideApiServiceFactory が自動生成されます。 public final class ApiModule_ProvideApiServiceFactory implements Factory<ApiService> { private final ApiModule module; public ApiModule_ProvideApiServiceFactory(ApiModule module) { assert module != null; this.module = module; } @Override public ApiService get() { return Preconditions.checkNotNull( module.provideApiService(), "Cannot return null from a non-@Nullable @Provides method"); } public static Factory<ApiService> create(ApiModule module) { return new ApiModule_ProvideApiServiceFactory(module); } } DaggerAppComponent はこの Factory を介して ApiModule の provideApiService() を呼び出し、ApiService のインスタンスを取得します。



AppComponent のインスタンスは自動生成される DaggerAppComponent.Builder を使って取得します。 final AppComponent appComponent = DaggerAppComponent.builder() .apiModule(new ApiModule()) .build(); 必要な Module が全てデフォルトコンストラクタで生成できる、もしくは @Provides がつけられたメソッドが全て static の場合は create() メソッドも用意されます。 final AppComponent appComponent = DaggerAppComponent.create();

Android では Application クラスで Component を生成して保持しておくことが多いです。 public class MyApplication extends Application { private AppComponent appComponent; @Override public void onCreate() { super.onCreate(); appComponent = DaggerAppComponent.builder() .apiModule(new ApiModule()) .build(); } public AppComponent getAppComponent() { return appComponent; } } これで MainActivity では AppComponent の apiService() から ApiService インスタンスをもらえるようになりました。 public class MainActivity extends AppCompatActivity { ApiService service; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); service = ((MyApplication) getApplication()) .getAppComponent() .apiService(); setContentView(R.layout.activity_main); } } Activity が自分で field に代入する書き方は field が増えると冗長になってきます。 public class MainActivity extends AppCompatActivity { ApiService apiService; HogeService hogeService; FugaService fugaService; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final AppComponent appComponent = ((MyApplication) getApplication()) .getAppComponent(); apiService = appComponent.apiService(); hogeService = appComponent.hogeService(); fugaService = appComponent.fugaService(); setContentView(R.layout.activity_main); } } そこで、field に代入する部分も Dagger に任せるようにしてみましょう。

まず AppComponent に inject される側を引数にとる void メソッドを定義します。 @Component(modules = ApiModule.class) public interface AppComponent { void inject(MainActivity target); } そして、Dagger で注入したい field に @Inject アノテーション をつけます。
field の代入していた部分を上記で定義した inject メソッドを呼ぶように変更します。 public class MainActivity extends AppCompatActivity { @Inject ApiService apiService; @Inject HogeService hogeService; @Inject FugaService fugaService; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ((MyApplication) getApplication()) .getAppComponent() .inject(this); setContentView(R.layout.activity_main); } } AppComponent に void inject(MainActivity target) を定義したので、 MainActivity_MembersInjector が自動生成されます。

DaggerAppComponent の inject() を呼ぶと、MainActivity_MembersInjector を使って MainActivity の @Inject がついた field にインスタンスが代入されます。 public final class DaggerAppComponent implements AppComponent { ... @SuppressWarnings("unchecked") private void initialize(final Builder builder) { this.provideApiServiceProvider = ApiModule_ProvideApiServiceFactory.create(builder.apiModule); this.mainActivityMembersInjector = MainActivity_MembersInjector.create(provideApiServiceProvider); } @Override public void inject(MainActivity target) { mainActivityMembersInjector.injectMembers(target); } ... } public final class MainActivity_MembersInjector implements MembersInjector<MainActivity> { private final Provider<ApiService> apiServiceProvider; public MainActivity_MembersInjector(Provider<ApiService> apiServiceProvider) { assert apiServiceProvider != null; this.apiServiceProvider = apiServiceProvider; } public static MembersInjector<MainActivity> create(Provider<ApiService> apiServiceProvider) { return new MainActivity_MembersInjector(apiServiceProvider); } @Override public void injectMembers(MainActivity instance) { if (instance == null) { throw new NullPointerException("Cannot inject members into a null reference"); } instance.apiService = apiServiceProvider.get(); } public static void injectApiService( MainActivity instance, Provider<ApiService> apiServiceProvider) { instance.apiService = apiServiceProvider.get(); } }



Singleton

Retrofit では毎回インスタンスを生成するとコストが高いので ApiService は Singleton にしたいのでした。

ApiModule の provideApiService() に @Singleton アノテーションをつけると、provideApiService() で生成されたインスタンスは Singleton として保持されます。 @Module public class ApiModule { @Singleton @Provides public ApiService provideApiService() { final Retrofit retrofit = ...; return retrofit.create(ApiService.class); } } 実際に Singleton として管理するのは Component の実装クラスです。
どの Component が管理するかを指定するために、管理する Component にも @Singleton をつけます。 @Component(modules = ApiModule.class) @Singleton public interface AppComponent { ... } DaggerAppComponent では、@Singleton アノテーションがついた @Provides メソッドは、DoubleCheck クラスを使って Singleton として管理するようになります。 public final class DaggerAppComponent implements AppComponent { ... @SuppressWarnings("unchecked") private void initialize(final Builder builder) { this.provideApiServiceProvider = DoubleCheck.provider(ApiModule_ProvideApiServiceFactory.create(builder.apiModule)); ... } } ちなみに @Singleton はスコープです。定義に @Scope がついています。 @Scope @Documented @Retention(RUNTIME) public @interface Singleton {} 独自のスコープを作ることもできます。スコープの詳しい説明はここではしません。



テスト

テスト時に mock 化した ApiService インスタンスを注入することを考えましょう。

まず MyApplication が持つ AppComponent を差し替えられるように、テスト用のメソッドを用意します。 public class MyApplication extends Application { private AppComponent appComponent; ... @VisibleForTesting public void setAppComponent(AppComponent appComponent) { this.appComponent = appComponent; } } 次に ApiModule を継承した MockApiModule を用意します。 public class MockApiModule extends ApiModule { private final ApiService service; public MockApiModule(ApiService service) { this.service = service; } @Override public ApiService provideApiService() { return service; } } DaggerAppComponent.Builder で ApiModule を差し替えることができるので、mock 化した ApiService インスタンス持った MockApiModule に差し替えます。 final AppComponent appComponent = DaggerAppComponent.builder() .apiModule(new MockApiModule(mockApiService)) .build(); テストでは MyApplication が持つ AppComponent を差し替えてから MainActivity を起動します。
こうすることで MainActivity には MockApiModule を介して mock 化された ApiService インスタンスが注入されます。 @RunWith(AndroidJUnit4.class) public class MainActivityUiTest { @Rule ActivityTestRule<MainActivity> activityTestRule = new ActivityTestRule<>(MainActivity.class, false, false); @Test public void test() throws Exception { final Context context = InstrumentationRegistry.getTargetContext(); final ApiService mockApiService = mock(ApiService.class); ... final AppComponent appComponent = DaggerAppComponent.builder() .apiModule(new MockApiModule(mockApiService)) .build(); ((MyApplication) context.getApplicationContext()) .setAppComponent(appComponent); activityTestRule.launchActivity(new Intent(context, MainActivity.class)); ... } }



余談

Subcomponent 使ってません。
Activity ごとに Subcomponent にわけるべきなのか私にはまだわかりません。
Subcomponent に分けるということは Component が1つの場合にくらべて明らかに複雑性は増します。
そのデメリットを上回るメリットがわかりませんでした。
(MVPとかMVVMではメリットあるのかな?Androidの思想に沿った設計が好きでそういうの取り入れてないのでわからないです)


2017年7月14日金曜日

Kotlin メモ : data class で List はいい感じに処理してくれるけど Array おまえはダメだ

data class A と B と C があって、C は A の配列と B を持っています。 data class A(val name: String) data class B(val age: Int) data class C(val names: Array<A>, val age: B) A と B に対する以下のテストは通ります assertEquals(A("hoge"), A("hoge")) assertEquals(B(10), B(10)) まぁ、そうだよね。

C に対する以下のテストは通りません assertEquals(C(arrayOf(A("hoge")), B(10)), C(arrayOf(A("hoge")), B(10))) data class がいい感じにやってくれるのかと思っていたのよ。やってくれなかった。

警告が出るのはそういうことなのね。



override すればいいんだけど... いけてない class C(val names: Array<A>, val age: B) { override fun equals(other: Any?): Boolean { if (this === other) return true if (other?.javaClass != javaClass) return false other as C if (!Arrays.equals(names, other.names)) return false if (age != other.age) return false return true } override fun hashCode(): Int { var result = Arrays.hashCode(names) result = 31 * result + age.hashCode() return result } } Array で持つのやめて List にしたらどうかなと思って試したら data class A(val name: String) data class B(val age: Int) data class C(val names: List<A>, val age: B) assertEquals(C(listOf(A("hoge")), B(10)), C(listOf(A("hoge")), B(10))) 通った


結論: Array はダメだが List ならいい感じにやってくれる


Kotlin メモ : data class を継承できないので interface で実現した話

データクラス的な A と B があります。B は A を継承しています。 public class A { @NonNull public final String name; public A(@NonNull String name) { this.name = name; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; A a = (A) o; return name.equals(a.name); } @Override public int hashCode() { return name.hashCode(); } } public class B extends A { public final int size; public B(String name, int size) { super(name); this.size = size; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; if (!super.equals(o)) return false; B b = (B) o; return size == b.size; } @Override public int hashCode() { int result = super.hashCode(); result = 31 * result + size; return result; } } test はこんな感じです。B が A を継承しているのは、A のリストに B を入れたいからです。 public class ABTest { @Test public void test() { final B b = new B("hoge", 10); assertEquals("hoge", b.name); assertEquals(10, b.size); assertEquals(new B("hoge", 10), new B("hoge", 10)); assertNotEquals(new B("hoge", 10), new B("fuga", 10)); assertNotEquals(new B("hoge", 10), new B("hoge", 11)); List<A> aList = new ArrayList<>(); aList.add(new B("hoge", 10)); assertEquals(new B("hoge", 10), aList.get(0)); } } こういう Java コードがあって、これを Kotlin 化するときに A, B それぞれを data class にしようとしたら、data class は親クラスになれない(final)ので困りました。
このテストが通るように、いい感じに Kotlin 化したいのです。

1. data class にするのを諦める

自分で equals と hasCode を override すれば data class にしなくてもやりたいことはできます。
以下は自動変換しただけのもの。 open class A(val name: String) { override fun equals(o: Any?): Boolean { if (this === o) return true if (o == null || javaClass != o.javaClass) return false val a = o as A? return name == a!!.name } override fun hashCode(): Int { return name.hashCode() } } class B(name: String, val size: Int) : A(name) { override fun equals(o: Any?): Boolean { if (this === o) return true if (o == null || javaClass != o.javaClass) return false if (!super.equals(o)) return false val b = o as B? return size == b!!.size } override fun hashCode(): Int { var result = super.hashCode() result = 31 * result + size return result } } もちろん上のテストは通ります。
しかし、いけてない。

2. A を interface にする

Kotlin では interface に抽象プロパティを持たせることができるので、A を interface に変えてみます。 interface A { val name: String } こうすると、B を data クラスにできます。 data class B(override val name: String, val size: Int) : A めっちゃ短い!
テストもちゃんと通ります。