DroidKaigi2020アプリで使われているInjectableについて

[DroidKaigi2020公式アプリ](https://github.com/DroidKaigi/conference-app-2020)で使われている`Injectable`が良さげだったので記事にします。

#Injectable
当該のプルリクエストは[こちら](https://github.com/DroidKaigi/conference-app-2020/pull/618)です。

`Injectable`はDaggerのInjectを便利にするためのInterfaceです。
元々はGoogleの[architecture-components-samples](https://github.com/android/architecture-components-samples)にあるGithubBrowserSampleで使われているコードです。
イメージとしては、`DaggerFragment = Fragment + Injectable`です。


```Injectable.kt
interface Injectable
```

```SearchSessionsFragment.kt
class SearchSessionsFragment : Fragment(R.layout.fragment_search_sessions), Injectable {
    ....
}
```

`Injectable`そのものは空のInterfaceで、実際のInjection周りの処理は`AppInjector`が受け持っています。

```AppInjector.kt
object AppInjector {
    fun initialize(application: Application) {
        application.registerActivityLifecycleCallbacks(
            object : Application.ActivityLifecycleCallbacks {

                override fun onActivityCreated(
                    activity: Activity,
                    savedInstanceState: Bundle?
                ) {
                    handleActivity(activity)
                }

                override fun onActivityStarted(activity: Activity) {
                }

                override fun onActivityResumed(activity: Activity) {
                }

                override fun onActivityPaused(activity: Activity) {
                }

                override fun onActivityStopped(activity: Activity) {
                }

                override fun onActivitySaveInstanceState(
                    activity: Activity,
                    outState: Bundle?
                ) {
                }

                override fun onActivityDestroyed(activity: Activity) {
                }
            })
    }

    private fun handleActivity(activity: Activity) {
        if (activity is HasAndroidInjector) {
            AndroidInjection.inject(activity)
        }
        if (activity is FragmentActivity) {
            activity.supportFragmentManager.registerFragmentLifecycleCallbacks(
                object : FragmentManager.FragmentLifecycleCallbacks() {
                    override fun onFragmentPreAttached(
                        fm: FragmentManager,
                        f: Fragment,
                        context: Context
                    ) {
                        if (f is Injectable || f is HasAndroidInjector) {
                            AndroidSupportInjection.inject(f)
                        }
                    }
                }, true
            )
        }
    }
}
```
これをApplicationクラスで`AppInjector.initialize(this)`することによって、自動でFragmentにInjectしてくれるようになります。
`DaggerFragment`を使った場合と変わらないように見えるかもしれませんが、色々と便利になります。

##1. FragmentのコンストラクタにレイアウトIdを渡せる
上のコードでも書かれていますが、FragmentのコンストラクタにレイアウトIdを渡すことによってレイアウトのViewをinflateできます。
これにより、`onCreateView`でinflateの処理を書く必要がなくなります。

```ExampleFragment.kt
class ExampleFragmentWithDagger : DaggerFragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.fragment_example, container, false)
        return view
    }
}

class ExampleFragmentWithInjectable : Fragment(R.layout.fragment_example), Injectable {
}
```

##2. テストが書きやすくなる
おそらくGithubBrowserSampleの方ではこちらが目的だったのではないかなと思います。
例えば、以下のようにViewModelFactoryをInjectしているFragmentのテストを書くとします。

```ExampleFragment.kt
class ExampleFragmentWithDagger : DaggerFragment() {
    @Inject lateinit var viewModelFactory: ViewModelProvider.Factory
    ...
}
```

テストコードは以下の通り。

```ExampleFragmentWithDaggerTest.kt
@RunWith(AndroidJUnit4::class)
class ExampleFragmentWithDaggerTest {
    @Test
    fun test() {
        val scenario = launchFragmentInContainer<ExampleFragmentWithDagger>()
        scenario.onFragment { fragment ->
            // write fragment test
        }
    }
}
```

このテストを実行するのは結構面倒です。
特に、ViewModelFactoryをモックにする場合はModuleやComponentを別に作成する必要が出てきそうです。
また、マルチモジュールのアプリだとモジュール間の依存関係がネックになったりしてさらに面倒です。

そこで、`Injectable`を使用したらどうでしょうか。

```ExampleFragment.kt
class ExampleFragmentWithInjectable : Fragment(R.layout.fragment_example), Injectable {
    @Inject lateinit var viewModelFactory: ViewModelProvider.Factory
    ...
}
```

```ExampleFragmentWithInjectableTest.kt
@RunWith(AndroidJUnit4::class)
class ExampleFragmentWithInjectableTest {
    @Test
    fun test() {
        val scenario = launchFragmentInContainer<ExampleFragmentWithInjectable>() {
            ExampleFragmentWithInjectable().apply {
                // Inject here
                viewModelFactory = mockViewModelFactory
            }
        }
        scenario.onFragment { fragment ->
            // write fragment test
        }
    }
}
```

`Injectable`を使うとテストではDaggerのdependencyを定義する必要がなくなります。（`AndroidSupportInjection.inject`を実行するのはApplicationクラスなので）
したがって、FragmentScenarioをlaunchする際に自分でInjectすることができます。
これならDIしているものをモックするのも簡単ですし、テストが書きやすいです。
ちなみに、GithubBrowserSampleではテスト用のViewModelFactoryを作成する[Util](https://github.com/android/architecture-components-samples/blob/master/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/ViewModelUtil.kt)があったり、Fragmentのテストを書く際にとても参考になります。


#さいごに
Fragmentテストの解説はrnakanoさんの「Jetpack時代のFragment再入門」が非常に分かりやすくおすすめです。
参考にさせていただきました。
https://youtu.be/IwHw7vrFwSE