CompositionLocalProviderを使ったjetpack composeのバケツリレーの回避方法
こんにちは、tkyです。
※Qiitaにも書いてありますが、こちらにも投稿しておきます。
jetpack composeには下層のcomposeに依存を渡す方法があります。
- KoinなどのDIコンテナを使う
- CompositionLocalProviderを使う
今回はFragmentManagerを下層のcomposeに渡すサンプルを通して違いについて観察してみようと思います。
Koinを使ったパターン
こんな感じで下層のComposeにfragmentManagerをDIしてみます。
パット見てこのような印象を持ちます。 - Activityで何をDIしたいか一応わかる - MyScreenで必要な依存がわかる - ReactのContext APIと同じような感じ
val activityModule = { activity: AppCompatActivity -> module { // 本来はFragmentManagerを操作するInterfaceを別で定義したほうが依存が分けられるかと思います single { activity.supportFragmentManager } } } class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) loadKoinModules(activityModule(this)) setContent { JetpackcomposenavigationTheme { Surface(color = MaterialTheme.colors.background) { MyApp() } } } } } @Composable fun MyApp() { Scaffold( topBar = { TopAppBar(title = { Text("タイトル") }) }, content = { MyScreen() } ) } @Composable fun MyScreen(fragmentManager: FragmentManager = get()) { // ↑koinのget()メソッドでDIします↑ AndroidView(factory = { context -> FrameLayout(context).apply { id = R.id.container } }, update = { val fragment = ComposeFragment.newInstance() val transaction = fragmentManager.beginTransaction() transaction.replace(it.id, fragment) transaction.commit() }) }
CompositionLocalProviderを使ったパターン
実はこれと同じことをやっています。
jetpack compose内でContextが欲しい時と、LifecycleOwnerが欲しい時ですね。
// Contextが欲しい時 val context = LocalContext.current Toast.makeText(context, "hello jetpack compose", Toast.LENGTH_SHORT).show() // LifecycleOwnerが欲しい時 val owner = LocalLifecycleOwner.current owner.lifecycle.addObserver(viewModel)
この仕組みがCompositionLocalProviderです。
ポイントはこの部分です。CompositionLocalProviderのスコープ内で提供したインスタンスが利用できるようになります。
CompositionLocalProvider( LocalFragmentManager provides supportFragmentManager ) { MyApp() }
コードをみてこのような印象を持ちます。 - Activityで何を提供したいのかわかる - MyAppはKoinパターンと違いはない - MyScreenの引数だけでは必要なものがわからない - FragmentManagerを使うスコープを限定できる
val LocalFragmentManager = staticCompositionLocalOf<FragmentManager> { noLocalProvidedFor("FragmentManager") } private fun noLocalProvidedFor(name: String): Nothing { error("CompositionLocal $name not present") } class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) loadKoinModules(activityModule(this)) setContent { JetpackcomposenavigationTheme { Surface(color = MaterialTheme.colors.background) { CompositionLocalProvider( LocalFragmentManager provides supportFragmentManager ) { MyApp() } } } } } } @Composable fun MyApp() { Scaffold( topBar = { TopAppBar(title = { Text("タイトル") }) }, content = { MyScreen() } ) } @Composable fun MyScreen() { val fragmentManager = LocalFragmentManager.current // Activityで提供したFragmentManagerをここで使う AndroidView(factory = { context -> FrameLayout(context).apply { id = R.id.container } }, update = { val fragment = ComposeFragment.newInstance() val transaction = fragmentManager.beginTransaction() transaction.replace(it.id, fragment) transaction.commit() }) }
使う分には大きな違いは感じませんが、
fun MyScreen()
だけをみてfragmentManagerが必要だということが判別できず、関数の中を確認する必要がありますね。
とはいえDIコンテナを使わなくてもバケツリレーを回避できるという手段を持っておくだけでも実装の幅は広がりそうですね。
注意
CompositionLocalProvider
は非常に便利に感じますがコードリーディングの観点から見ると
いきなり知らない変数依存ができることにもなるので多用厳禁かなとは思いました。
基本的にはDIで解決できるしDIコンテナを利用したほうがより見やすくなると思います。
contextやlifecycleOwnerがCompositionLocalProviderで提供されているように フレームワークやライブラリなどシステム的にどうしても必要な場合でのみ利用を検討するのが良さそうです。
公式Doc
CompositionLocalに関する情報です。
https://developer.android.com/jetpack/compose/compositionlocal?hl=ja