DroidKaigi2020公式アプリで使われているInjectable
が良さげだったので記事にします。
Injectable
当該のプルリクエストはこちらです。
Injectable
はDaggerのInjectを便利にするためのInterfaceです。
元々はGoogleのarchitecture-components-samplesにあるGithubBrowserSampleで使われているコードです。
イメージとしては、DaggerFragment = Fragment + Injectable
です。
interface Injectable
class SearchSessionsFragment : Fragment(R.layout.fragment_search_sessions), Injectable {
....
}
Injectable
そのものは空のInterfaceで、実際のInjection周りの処理はAppInjector
が受け持っています。
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の処理を書く必要がなくなります。
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のテストを書くとします。
class ExampleFragmentWithDagger : DaggerFragment() {
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
...
}
テストコードは以下の通り。
@RunWith(AndroidJUnit4::class)
class ExampleFragmentWithDaggerTest {
@Test
fun test() {
val scenario = launchFragmentInContainer<ExampleFragmentWithDagger>()
scenario.onFragment { fragment ->
// write fragment test
}
}
}
このテストを実行するのは結構面倒です。
特に、ViewModelFactoryをモックにする場合はModuleやComponentを別に作成する必要が出てきそうです。
また、マルチモジュールのアプリだとモジュール間の依存関係がネックになったりしてさらに面倒です。
そこで、Injectable
を使用したらどうでしょうか。
class ExampleFragmentWithInjectable : Fragment(R.layout.fragment_example), Injectable {
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
...
}
@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があったり、Fragmentのテストを書く際にとても参考になります。
さいごに
Fragmentテストの解説はrnakanoさんの「Jetpack時代のFragment再入門」が非常に分かりやすくおすすめです。
参考にさせていただきました。
https://youtu.be/IwHw7vrFwSE