LoginSignup
14
6

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-02-27

DroidKaigi2020公式アプリで使われているInjectableが良さげだったので記事にします。

Injectable

当該のプルリクエストはこちらです。

InjectableはDaggerのInjectを便利にするためのInterfaceです。
元々はGoogleの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があったり、Fragmentのテストを書く際にとても参考になります。

さいごに

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

14
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
6