Fragments in Composeを実現する方法

公開日: 2024/06/15

はじめに

androidx.fragment:fragment-*:1.8.0がリリースされ、AndroidFragmentcomposableがstableバージョンで使用できるようになりました。

以前までは、Fragments in Composeを実現するために、AndroidViewBindingを使うことが推奨されていました。

しかし、androidx.fragment1.8.0のリリースのノート[1]に以下のような記述がある通り、今後はAndroidFragmentAndroidViewBindingの代替となるようです。

This should be used as a direct replacement for the previously recommended approach of using AndroidViewBinding to inflate a Fragment.

以下のポストにある通り、AndroidFragmentAndroidViewBindingを使用する方法で解決できなかった多くの問題点を解決してくれているとのことです。


https://x.com/ianhlake/status/1775710793897525619

本記事では、Fragments in Composeの新しい実装方法となったAndroidFragmentの使用方法とAndroidFragmentの内部実装について解説します。

AndroidFragmentの使用方法

例えば、以下のようなFragmentとレイアウトファイルがあるとします。

SampleFragment.kt
...
class SampleFragment : Fragment(R.layout.fragment_sample) {
    lateinit var binding: FragmentSampleBinding
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = FragmentSampleBinding.inflate(layoutInflater)
        return binding.root
    }
}
fragment_sample.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

AndroidFragmentを使用して以下のように記述するだけで、ComposeからFragmentを使用することができます。

HogeScreen.kt
@Composable
fun HogeScreen() {    
    AndroidFragment<SampleFragment>(
        onUpdate = { fragment ->
            fragment.binding.textView.text = "Fragment in Compose!!"
        }
    )
    ...
}

実行すると以下のように、TextViewに「Fragment in Compose!!」という文字列が表示されていることがわかると思います。


実行結果

AndroidFragmentの内部実装

AndroidFragmentの内部実装を解説していきます。

1. AndroidFragmentのパラメータについて

AndroidFragmentは以下のように定義されています。

@Composable
<T : Fragment> AndroidFragment(
    clazz: Class<T>,
    modifier: Modifier,
    fragmentState: FragmentState,
    arguments: Bundle,
    onUpdate: (T) -> Unit
)

それぞれのパラメータは以下のような役割になります。

  • clazz: 作成したいFragmentのクラス
  • modifier: レイアウトに適用するModifier
  • fragmentState: Fragmentの状態保持用のState(後述)
  • arguments: Fragmentに渡すarguments(これあるのだいぶ嬉しい)
  • onUpdate: 作成されたFragmentのインスタンスを提供するコールバック

2. どのようにFragmentのインスタンスを作成しているか

AndroidFragmentの内部実装を見ると以下のコードがあります。

AndroidFragment.kt
DisposableEffect(...) {
    val fragment = fragmentManager.findFragmentById(container.id)
        ?: fragmentManager.fragmentFactory.instantiate(
            context.classLoader, clazz.name
        )
        ...
}

すでにFragmentがfragmentManagerに追加されていればそのインスタンスを取得し、追加されていなければ、Fragmentのクラス名(=clazz.name)を指定して新たにFragmentのインスタンスを作成しています。

3. どのようにFragmentをComposeに表示しているか?

以下のようにFragmentContainerViewAndroidViewに渡すことで表示しています。
(ここはAndridViewBindingを使用した方法を踏襲していそうですね!)

FragmentContainerViewは、指定された場所にフラグメントを表示するためのコンテナであり、ここに作成するFragmentのインスタンスをaddしています。

AndroidFragment.kt
lateinit var container: FragmentContainerView
AndroidView({
    container = FragmentContainerView(context)
    container.id = hashKey
    container
}, modifier)

...

DisposableEffect(...) {
    val fragment = fragmentManager.findFragmentById(container.id)
        ?: fragmentManager.fragmentFactory.instantiate(
            context.classLoader, clazz.name
        ).apply {
            ...
            fragmentManager.beginTransaction()
                    .setReorderingAllowed(true)
                    .add(container, this, "$hashKey")
                    .commitNow()
        }
        ...
}

3. どのようにFragmentの状態保持をしているか

Configuration Changedやプロセスのキルが発生したときのために、AndroidFragmentfragmentStateをパラメータとして受け取ります。これはFragmentの状態を保存するために使用されます。

デフォルトではパラメータとしてrememberFragmentState()を使用しており、以下のように定義されています。

FragmentState.kt
@Composable
fun rememberFragmentState(): FragmentState {
    return rememberSaveable(saver = fragmentStateSaver()) {
        FragmentState()
    }
}

@Stable
class FragmentState(
    internal var state: MutableState<Fragment.SavedState?> = mutableStateOf(null)
)

private fun fragmentStateSaver(): Saver<FragmentState, *> = Saver(
    save = { it.state },
    restore = { FragmentState(it) }
)

rememberSaveableを使用することで、Acitivityの破棄に耐えられるようにFragmentStateを保持しています。FragmentStateは、Fragmentの状態情報を管理するクラスであるFragment.SavedState?のMutableStateを保持しています。

さらに、fragmentStateAndroidFragment内で以下のように使用されています。

AndroidFragment
DisposableEffect(...) {
    val fragment = fragmentManager.findFragmentById(container.id)
        ?: fragmentManager.fragmentFactory.instantiate(
            context.classLoader, clazz.name
        ).apply {
            setInitialSavedState(fragmentState.state.value)
            ...
        }
        onDispose {
            val state = fragmentManager.saveFragmentInstanceState(fragment)
            fragmentState.state.value = state
            ...
        }
}

fragmentのインスタンス取得後、setInitialSavedState()を使用して、Fragment.SavedState?の初期値をセットしています。これがFragmentの状態の復元処理になります。

そして、AndroidFragmentがCompositionから退場したときに、fragmentManager経由でFragment.SavedState?をfragmentStateに保存しています。

おわりに

AndroidFragmentについて解説しました。

まだまだFragmentが残っているプロダクトが多いのが現状だと思うので、うまくAndroidFragmentを使用してCompoe化を進めていきましょう!!

参考

https://developer.android.com/jetpack/androidx/releases/fragment

https://developer.android.com/reference/kotlin/androidx/fragment/compose/package-summary

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:fragment/fragment-compose/src/main/java/androidx/fragment/compose/AndroidFragment.kt

脚注
  1. androidx.fragment version 1.8のリリースノート ↩︎