ticktakclockの日記

技術ポエムを綴ったりします。GitHub idも同じです (@ticktakclock)

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