0

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

Daniel Wilson
  • 18,838
  • 12
  • 85
  • 135
  • 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](https://developer.android.com/guide/fragments/saving-state)? – ianhanniballake Mar 12 '22 at 00:26
  • 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? – Daniel Wilson Mar 12 '22 at 00:59
  • 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). – ianhanniballake Mar 12 '22 at 01:04
  • 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...`. – Daniel Wilson Mar 12 '22 at 01:29
  • 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 not – Daniel Wilson Mar 12 '22 at 01:30

0 Answers0