2017年5月22日月曜日

Dagger2を試す


また、だいぶ間が空いてしまった。

個人でツールみたいなアプリ作っていても大規模化することとかは無くて、Dagger2など使う必要など全く無かったりするんですが、そのまま使う必要ないなーと思っているだけなのもつまらないのでちょっと調べてみました。

Dagger2の導入の方法などは公式(https://github.com/google/dagger)を見るのが良いと思います。僕が試したときには最新は2.10でした。

大体の記事がmoduleを作ってcomponentを作って、そこからinjectするのですが、gsonとかアプリ全体で使うやつとrepositoryとか一部のactivityでしか使わなそうやつを分けたいなとか思ったりもするわけでして、moduleを2つに分けたので、componentもそれぞれ作って、2つのcomponentを特定のactivityで利用してinjectしたいなとか思ったので作ってみました。

1:  ├── AppComponent.java  
2:  ├── MainActivity.java  
3:  ├── RepositoryComponent.java  
4:  └── module  
5:    ├── AppModule.java  
6:    └── RepositoryModule.java  

ただ、これコンパイル通りません。
Componentでinject(MainActivity activity)などのmethodがあるとAnnotationProcessorがソースコードをapp/build/generated/source/apt/debug/com/matsuhiro/dagger2sample/MainActivity_MembersInjector.java
に生成します。
inject(MainActivity activity)を持っているComponentが複数あるとそれぞれのComponentがMainActivity_MembersInjector.javaを生成しようとするなどするけど出来ないようです。
複数のComponentをどうやって1つのactivityにinjectしようと調べたりもしたんですが、ActivityComponentみたいに専用のComponent1つ作ればいいかと思いました。
Moduleを複数作って、Componentは、Injectしたいclass毎に作ればよいかと。

でこうしました。

1:  ├── ActivityComponent.java  
2:  ├── MainActivity.java  
3:  └── module  
4:    ├── AppModule.java  
5:    └── RepositoryModule.java  

使い方は、これで良いとしてDagger2をAndroidで試している記事を見るとActivityに何かしらのclassをInjectしていものが多い気がしました。
Dagger2というかDIの場合に、どんなことが嬉しいかっていうと、やはりTestコードを書く時にテストしたいModule以外のModuleを置き換えやすいというのがあると思うのですが、ActivityのメンバーにInjectしたところでTestの時に置き換えやすくない。。。
https://github.com/google/dagger/issues/110#issuecomment-98260225
http://qiita.com/arenahito/items/d9bbca61c8a67cfad226
こんな感じのworkaroundが見つかったりもしますが、ApplicationクラスにComponent持たせて外から差し替えるって、なんだかテストのために無理やりやってる感がすごいなと。まあworkaroundなんでそうなんでしょうけど。

AndroidでDIを使おうと思ったら、ActivityなどAndroid特有の場所以外にロジックを押し込んで、そこでやるのが良さそうです。
そういうので、一番参考になるのってhttps://github.com/android10/Android-CleanArchitecture なわけですが、やっていることがワリといろんなところに飛ぶのと、Testの方がだいぶ素朴なので、自分で最小と思われるものを作ってみました。

ソースコード
1:  public class UseCaseUnitTest {  
2:    final class MockedSettingRepositoryImpl implements SettingRepository {  
3:      @Override  
4:      public String getValue(String key) {  
5:        return "mockrepo";  
6:      }  
7:      @Override  
8:      public void setValue(String key, String value) {}  
9:    }  
10:    @Test  
11:    public void MockedRepository() throws Exception {  
12:      UseCase useCase = new UseCase(new SomeBusinessService(), new MockedSettingRepositoryImpl());  
13:      String message = useCase.doSomething("hoge");  
14:      assertEquals(message, "mockrepo,biz");  
15:    }  
16:    @Test  
17:    public void MockedService() throws Exception {  
18:      SomeBusinessService mockedSomeBusinessService = Mockito.mock(SomeBusinessService.class);  
19:      Mockito.when(mockedSomeBusinessService.doSomething(Mockito.any(String.class))).thenReturn("mockbiz");  
20:      UseCase useCase = new UseCase(mockedSomeBusinessService, RepositoryModule_ProvideSettingRepositoryFactory.create(new RepositoryModule()).get());  
21:      String message = useCase.doSomething("hoge");  
22:      assertEquals(message, "mockbiz");  
23:    }  
24:    @Test  
25:    public void MockedInput() throws Exception {  
26:      SomeBusinessService mockedSomeBusinessService = Mockito.mock(SomeBusinessService.class);  
27:      Mockito.when(mockedSomeBusinessService.doSomething(Mockito.any(String.class))).thenReturn("mockbiz");  
28:      UseCase useCase = new UseCase(mockedSomeBusinessService, new MockedSettingRepositoryImpl());  
29:      String message = useCase.doSomething("hoge");  
30:      assertEquals(message, "mockbiz");  
31:    }  
32:    @Test  
33:    public void MockedInput2() throws Exception {  
34:      SomeBusinessService mockedSomeBusinessService = Mockito.mock(SomeBusinessService.class);  
35:      Mockito.when(mockedSomeBusinessService.doSomething(Mockito.any(String.class))).thenReturn("mockbiz");  
36:      SettingRepository mockedSettingRepository = Mockito.mock(SettingRepository.class);  
37:      Mockito.when(mockedSettingRepository.getValue(Mockito.any(String.class))).thenReturn("mockrepo");  
38:      UseCase useCase = new UseCase(mockedSomeBusinessService, mockedSettingRepository);  
39:      String message = useCase.doSomething("hoge");  
40:      assertEquals(message, "mockbiz");  
41:    }  
42:  }  

テストする時にUseCaseの引数にMockなどを突っ込めばOKなわけです。
MockedRepository()でやっているように引数にinterfaceを持たせる感じにすれば、テスト内で自由に挙動を変更できます。
ただ、めんどくさいですね。Mockito使うのが良いと思います。

MockedService()ではMockito使ってSomeBusinessServiceを差し替えてます。
@Mock使えばよいだけの気もしてます。
ここで、SettingRepositoryのインスタンス化にDagger2が自動生成したコードを使っています。実際にSettingRepositoryがアプリのコードでインスタンス化されるときにはこんな風にされています。
ただ、テストしたいのはUseCaseクラスなので全部Mockにすべきでしょう。
なのでMockedInputとMockedInput2では、違うやり方ですが引数を外部から変更しています。

Dagger2の使い方とか調べてましたが、Activityの中でinject(MainActivity activity)を呼び出したタイミングでactivityのメンバーに対してインスタンスがinjectされるのが自動生成されたコードからわかります。

1:  // Generated by dagger.internal.codegen.ComponentProcessor (https://google.github.io/dagger).  
2:  package com.matsuhiro.dagger2sample;  
3:  import com.matsuhiro.dagger2sample.domain.UseCase;  
4:  import dagger.MembersInjector;  
5:  import javax.inject.Provider;  
6:  public final class MainActivity_MembersInjector implements MembersInjector<MainActivity> {  
7:   private final Provider<UseCase> useCaseProvider;  
8:   public MainActivity_MembersInjector(Provider<UseCase> useCaseProvider) {  
9:    assert useCaseProvider != null;  
10:    this.useCaseProvider = useCaseProvider;  
11:   }  
12:   public static MembersInjector<MainActivity> create(Provider<UseCase> useCaseProvider) {  
13:    return new MainActivity_MembersInjector(useCaseProvider);  
14:   }  
15:   @Override  
16:   public void injectMembers(MainActivity instance) {  
17:    if (instance == null) {  
18:     throw new NullPointerException("Cannot inject members into a null reference");  
19:    }  
20:    instance.useCase = useCaseProvider.get();  
21:   }  
22:   public static void injectUseCase(MainActivity instance, Provider<UseCase> useCaseProvider) {  
23:    instance.useCase = useCaseProvider.get();  
24:   }  
25:  }  

具体的には20行目ですね。

onCreate()の一番最初とかで呼び出してあげなとバグになりそうだなとか、何回呼び出しても大丈夫なんだなーとかわかったのは面白かったです。

Dagger2のアプリのコード部分だけみていると魔法のようも思えますが、自動生成されたコードをみると割合と単純な事をやっているもんですね。

bloggerでソースコード書くのめんどくさくなってきたのでqiitaに移ろうかなとか思います。