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
isSaveEnabled = false
. Why are you doing that if you want each fragment to save its state? – WhithersoeverIllegalStateException
"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 understandmFragments
in the exception is not empty when I am returning to the suspended fragment - onlyonDestroyView
was hit so the fragments are still in the fm. I can seeonViewCreated
is called just before it throws this exception. My feeling is this is designed for destroy->create config changes, but not destroyview->createview ones? – HandcraftonViewCreated()
right alongside yourTabLayoutMediator
? What do you think you gain by keeping your adapter around afteronDestroyView()
? 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). – WhithersoeveronViewCreated
(and stop turning offisSaveEnabled
), it still throws that exception. If I kill theinitted
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 therestoreState
:Fragment no longer exists for key f#0...
. – Handcraft