ticktakclockの日記

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

jetpack composeでcompose上にFragmentをのせる

こんにちは、tkyです。

※Qiitaにも書いてありますが、こちらにも投稿しておきます。

jetpack composeでFragmentの上にjetpack composeを乗せるパターンはよく見ますが、逆はなかったので記事にしてみました。

Fragmentからjetpack composeを扱うこともできれば、jetpack composeからFragmentを扱うこともできるので便利ですね。

jetpack compose on fragment

よく見るFragmentでjetpack composeを扱う方法です。 ComposeViewというViewが提供されているのでこれをonCreateViewに返すようにします。

class ComposeFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return ComposeView(requireContext()).apply {
            setContent {
                // この中でjetpack composeが扱えるようになります
                Text(text = "compose on fragment.")
            }
        }
    }
}

fragment on jetpack compose

AndroidViewというViewを扱うことができるComposableが提供されているのでこれを使います。

Fragmentに限った話ではなく、既存の資産を一時的に使いまわしたい時などに便利かもしれません。

部分

AndroidView(factory = { context ->
    FrameLayout(context).apply {
        id = R.id.container
    }
}, update = {
    val fragment = ComposeFragment.newInstance()
    val transaction = supportFragmentManager.beginTransaction()
    transaction.replace(it.id, fragment)
    transaction.commit()
})

全体

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            JetpackcomposenavigationTheme {
                Surface(color = MaterialTheme.colors.background) {
                    Scaffold(
                        topBar = {
                            TopAppBar(title = {
                                Text("タイトル")
                            })
                        },
                        content = {
                            AndroidView(factory = { context ->
                                FrameLayout(context).apply {
                                    id = R.id.container
                                }
                            }, update = {
                                val fragment = LegacyFragment.newInstance()
                                val transaction = supportFragmentManager.beginTransaction()
                                transaction.replace(it.id, fragment)
                                transaction.commit()
                            })
                        }
                    )
                }
            }
        }
    }
}

supportFragmentManagerをバケツリレーしなければならないのか?

上記サンプルコードはすべてActivityに書いていたのでComposeの中で supportFragmentManager にアクセスできました。

通常Activityの中に全て書き切ることはしないためまともにやろうとするとこうなります。 バケツリレーの嵐でこの先の地獄が見えますね。。

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            JetpackcomposenavigationTheme {
                Surface(color = MaterialTheme.colors.background) {
                    MyApp(supportFragmentManager)
                }
            }
        }
    }
}

@Composable
fun MyApp(fragmentManager: FragmentManager) {
    Scaffold(
        topBar = {
            TopAppBar(title = {
                Text("タイトル")
             })
        },
        content = {
            MyScreen(fragmentManager)
        }
    )
}

@Composable
fun MyScreen(fragmentManager: 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()
    })
}

こういった場合はDIコンテナを利用することが理想系かなと思います。

Koinで解説しようと思います。 下記サンプルとしていきなりFragmentManagerをDIしていますが、実際は直接DIしないでFragmentを操作するインターフェースとしてDIしてあげるのが良さそうです。

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()
    })
}

まとめ

  • composeの上にFragment(=AndroidのView)をのせることができた
  • 既存の資産を一時的に使いまわしたい時などに便利かもしれない
  • jetpack composeにはDIが不可欠