ViewPager2 how to restore fragments when navigating back
Asked Answered
H

0

1

Hi folks I have a ViewPager2 with single activity architecture. When I click a button, I swap out the ViewPager2 host fragment with another one using the Jetpack Navigation library.

This calls onDestroyView for the host fragment. When I click back, we are back to onCreateView. How can I return to the ViewPager2 I was looking at, seeing as the host fragment itself is not destroyed?

I believe based on this answer that restoring a ViewPager2 is actually impossible, not sure if this is by design or not. So what is the best practice here, assuming each fragment loads a heavy list, am I supposed to reload all the data every time a user pops the backstack into my viewpager? The only thing I can think of is to have an activity scoped ViewModel which maintains the list of data for each fragment, which sounds ridiculous, imagine if my pages were dynamically generated or I had several recycler views on each fragment....

Here is my attempt, I am trying to do the bare minimum when navigating back, however without assigning the view pager adapter again, I am looking at a blank fragment tab. I don't understand this, the binding has not died, so why is the view pager not capable of restoring my fragment?

OrderTabsFragment.kt

var adapter: TabsPagerAdapter? = null
private var _binding: FragmentOrdersTabsBinding? = null
private val binding get() = _binding!!
private var initted = false

override fun onCreate(savedInstanceState: Bundle?) {
    Timber.d("OrderTabsFragment $initted - onCreate $savedInstanceState")
    super.onCreate(savedInstanceState)

    adapter = TabsPagerAdapter(this, Tabs.values().size)
    adapter?.currentTab = Tabs.valueOf(savedInstanceState?.getString(CURRENT_TAB) ?: Tabs.ACTIVE.name)
}

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View {
    Timber.d("OrderTabsFragment $initted - onCreateView $savedInstanceState, _binding=$_binding")
    if(_binding == null)
        _binding = FragmentOrdersTabsBinding.inflate(inflater, container, false)

    return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    Timber.d("OrderTabsFragment $initted - onViewCreated $savedInstanceState")

    super.onViewCreated(view, savedInstanceState)

    if(!initted) {
        initted = true

        val viewpager = binding.viewpager
        viewpager.adapter = adapter
        viewpager.isSaveEnabled = false

        binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
            override fun onTabSelected(tab: TabLayout.Tab?) {}
            override fun onTabUnselected(tab: TabLayout.Tab?) {}
            override fun onTabReselected(tab: TabLayout.Tab?) {
                if (adapter?.currentTab == Tabs.FILTERED) {
                    showFilterBalloon(tab)
                }
            }
        })
        TabLayoutMediator(binding.tabLayout, viewpager) { tab, position ->
            when (position) {
                0 -> tab.text = getString(R.string.title_active).uppercase(Locale.getDefault())
                1 -> tab.text =
                    getString(R.string.title_scheduled).uppercase(Locale.getDefault())
                2 -> tab.text =
                    getString(R.string.title_complete).uppercase(Locale.getDefault())
            }
        }.attach()
    }
    else{
        val viewpager = binding.viewpager
        viewpager.adapter = adapter //Required otherwise we are looking at a blank fragment tab. The adapter rv was detached and can't be reattached?
        viewpager.isSaveEnabled = false //Required otherwise "Expected the adapter to be 'fresh' while restoring state."
    }
}

override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    Timber.d("OrderTabsFragment $initted - onSaveInstanceState")
    outState.putString(CURRENT_TAB, adapter?.currentTab?.name)
}


override fun onDestroy() {
    super.onDestroy()
    Timber.d("OrderTabsFragment $initted - onDestroy")
    binding.viewpager.adapter = null
    _binding = null
    adapter = null
}

enum class Tabs {
    ACTIVE, SCHEDULED, COMPLETE, FILTERED
}

Edit: Here's roughly the same questions coming up in other places 1, 2, 3

Handcraft answered 11/3, 2022 at 23:19 Comment(5)
Well, you specifically told the ViewPager not to save its state when you did isSaveEnabled = false. Why are you doing that if you want each fragment to save its state?Whithersoever
We get a common IllegalStateException "Expected the adapter to be 'fresh' while restoring state." otherwise, which I do not know how to get around I think is the gist of my question (and the others). Assuming that dumb else branch was not there, I understand mFragments in the exception is not empty when I am returning to the suspended fragment - only onDestroyView was hit so the fragments are still in the fm. I can see onViewCreated is called just before it throws this exception. My feeling is this is designed for destroy->create config changes, but not destroyview->createview ones?Handcraft
Why aren't you creating your adapter in onViewCreated() right alongside your TabLayoutMediator? What do you think you gain by keeping your adapter around after onDestroyView()? You don't get any exception when you do that (and if you don't purposefully disable saving the fragment's state, it'll all still be there, in the fragments themselves).Whithersoever
If I move the adapter creation to onViewCreated (and stop turning off isSaveEnabled), it still throws that exception. If I kill the initted boolean as well I assumed it would create a bunch of brand new fragments when I come back (hence I tried to keep the adapter around), but now I think I am understanding if the fragment itself is not destroyed that is not the case? However it now throws an exception in the restoreState: Fragment no longer exists for key f#0....Handcraft
I will keep digging at this if you are saying basically stick to the viewpager2 sample in the doc and it should be capable of restoring everything itself. Adding to my confusion was that navigation 2.3.5 was incorrectly recreating these (nested) fragments whereas 2.4.1 does notHandcraft

© 2022 - 2024 — McMap. All rights reserved.