FlutterFragmentとViewPagerの組み合わせはonResumeのタイミングで難しかった


ViewPager2を使って2ページ目以降FlutterFragmentを設定すると、完全にページ遷移するまではFlutterFragmentが描画されず真っ白になります。一度表示すると真っ白になりません。

このように遷移途中でもFlutterFragmentが描画されて欲しいです。

なぜそうなるか

作り方

ViewPager2のAdapterはこのように作りました。

MyFragmentStateAdapter.kt
class MyFragmentStateAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
    override fun getItemCount() = 2

    override fun createFragment(position: Int): Fragment {
        return if (position == 0)
        // AndroidネイティブなFragment
            MainFragment()
        else
        // FlutterFragment
            FlutterFragment.withNewEngine()
                    .shouldAttachEngineToActivity(false)
                    .build()
    }
}

ViewPager2はこのように設定しました。

PagerActivity.kt
viewPager.adapter = MyFragmentStateAdapter(this)
// こちらを設定しないとスワイプ開始時に一瞬ひっかかる
viewPager.offscreenPageLimit = 1

Fragmentのライフサイクルを確認する

2ページ目をFlutterFragmentから空のフラグメントにします。

MyFragmentStateAdapter.kt
class MyFragmentStateAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
    override fun getItemCount() = 2

    override fun createFragment(position: Int): Fragment {
        return if (position == 0)
            MainFragment()
        else
            EmptyFragment()
    }
}

空のフラグメントのライフサイクルを確認します。

EmptyFragment.kt
class EmptyFragment : Fragment() {
    override fun onStart() {
        super.onStart()
        Log.d("EmptyFragment", "onStart")
    }

    override fun onResume() {
        super.onResume()
        Log.d("EmptyFragment", "onResume")
    }

    override fun onPause() {
        super.onPause()
        Log.d("EmptyFragment", "onPause")
    }

    override fun onStop() {
        super.onStop()
        Log.d("EmptyFragment", "onStop")
    }
}

logcatを確認した所、onStartは画面を開いたときに呼ばれますが、onResumeは2ページ目に完全遷移するまで呼ばれませんでした。

# 画面を開いた
D EmptyFragment: onStart
# 2ページ目に完全に遷移
D EmptyFragment: onResume

どうやらFlutterFragmentは初回のonResumeで描画されるようです。この挙動を変更する方法は見つけられませんでした。

ViewPager2ではなくViewPagerを使った場合

コンストラクタのbehaviorフィールドにBEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT定数を設定した場合はViewPager2と同じくonResumeは2ページ目に完全遷移するまで呼ばれずFlutterFragmentの描画も完全遷移まで開始されません。

MyFragmentStateAdapter.kt
class MyFragmentStateAdapter(fm: FragmentManager) :
        FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {

    override fun getCount() = 2

    override fun getItem(position: Int): Fragment {
        return if (position == 0)
            MainFragment()
        else
            FlutterFragment.withNewEngine()
                    .shouldAttachEngineToActivity(false)
                    .build()
    }
}

behaviorフィールドをBEHAVIOR_SET_USER_VISIBLE_HINT定数にすれば、画面が開かれると同時にonResumeが呼ばれるので、FlutterFragmentの描画もスワイプ中に行われます。しかしその定数は非推奨扱いなので、後ほど技術的負債になる危険性を感じました。

onResumeが完全遷移まで呼ばれない理由

非推奨になったBEHAVIOR_SET_USER_VISIBLE_HINT定数について調べてみたところ、setMaxLifecycleメソッドが紹介されていて、それについて調べてみたところ、こちらの記事に行き着きました。

setUserVisibleHintのdeprecatedとsetMaxLifecycle

この記事の情報を持ってViewPager, ViewPager2のアダプターのソースコードを見てみたところsetMaxLifecycleメソッドの呼び出しで、完全遷移するまではonStartまでしかライフサイクルを遷移させない設定にしていたようです。どうしてもViewPagerを使いたい場合は、ライブラリをフォークしてsetMaxLifecycle呼び出し部分を書き換える必要がありそうです。

最終的にはUIデザインを変更しました

結局、横スワイプによるページ切り替えは諦めて、ボタンによる画面呼び出しにしました。

今回に関してはそちらの方がUIとしては分かりやすいと思いました。