Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Viewpager2 with fragments and Jetpack navigation: Restore fragments instead of recreating them

I have a Viewpager2 inside a Fragment (lets call it HomeFragment). That Viewpager itself also contains Fragments. When I navigate away from the HomeFragment its view will be destroyed and when I navigate back the view will be recreated. Now I set the adapter of the Viewpager2 in the HomeFragment during onViewCreated(). Therefore the adapter will be recreated when I navigate back to the HomeFragment, which also recreates all Fragments in the Viewpager2 and the current item is reset to 0. If i try to re-use the adapter that I instantiated on the first creation of the HomeFragmenti get an exception, because of this check inside of the FragmentStateAdapter:

public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {         checkArgument(mFragmentMaxLifecycleEnforcer == null); 

Does anybody have an idea how I can prevent recreating everything when navigating back? Otherwise this is a pretty big performance overhead and hinders my UX.

like image 921
sunilson Avatar asked Jul 30 '19 12:07

sunilson


1 Answers

I've spent a bit of time with this, and I've diagnosed the problem for anyone who needs to hear it. I tried to keep my solution as conventional as possible. If we look at your statement:

Therefore the adapter will be recreated when I navigate back to the HomeFragment, which also recreates all Fragments in the Viewpager2 and the current item is reset to 0.

The problem is that the current item is reset to 0, because the list that your adapter is based off-of is recreated. To resolve the issue, we don't need to save the adapter, just the data inside of it. With that in mind, solving the problem is not difficult at all.

Let's layout some definitions:

  • HomeFragment is, as you've said, the host of your ViewPager2,
  • MainActivity is the running activity which hosts HomeFragment and all created fragments inside of it
  • We are paging through instances of MyFragment. You could even have more than one type of fragment that you page through, but that's beyond the scope of this example.
  • PagerAdapter is your FragmentStateAdapter, which is the adapter for HomeFragment's ViewPager2.

In this example, MyFragment has the constructor constructor(id : Int). Then, PagerAdapter is probably going to appear as follows:

class PagerAdapter(fm : Fragment) : FragmentStateAdapter(fm){          var ids : List<Int> = listOf()      ...          override fun createFragment(position : Int) : Fragment{         return MyFragment(ids[position])     }       } 

The problem that we are facing is every time you recreate PagerAdapter the constructor is called and that constructor, as we can see above, sets ids to an empty list.

My first thought was that maybe I could switch fm to be MainActivity. I don't navigate out of MainActivity so I'm not sure why, but this solution doesn't work.

Instead, what you need to do is abstract the data out of PagerAdapter. Create a "viewModel":

    /* We do NOT extend ViewModel. This naming just indicates that this is your data-      storage vehicle for PagerAdapter*/     data class PagerAdapterViewModel(     var ids : List<Int>      ) 

Then, in PagerAdapter, make the following adjustments:

class PagerAdapter(     fm : Fragment,     private val viewModel : PagerAdapterViewModel  ) : FragmentStateAdapter(fm){          // by creating custom getters and setters, you are migrating your code to this      // implementation without needing to adjust any code outside of the adapter      var ids : List<Int>         get() = viewModel.ids          set(value) {viewModel.ids = value}           override fun createFragment(position : Int) : Fragment{         return MyFragment(ids[position])     }       } 

Finally, in HomeFragment, you'll have something like:

class HomeFragment : Fragment(){       ...       /** Calling "by lazy" ensures that this object is only created once, and hence     we retain the data stored in it, even when navigating away. */     private val pagerAdapterViewModel : PagerAdapterViewModel by lazy{         PagerAdapterViewModel(listOf())     }      private lateinit var pagerAdapter : PagerAdapter      ...      override fun onViewCreated(view: View, savedInstanceState: Bundle?) {         super.onViewCreated(view, savedInstanceState)         ...         pagerAdapter = PagerAdapter(this, pagerAdapterViewModel)         pager.adapter = pagerAdapter          ...     }          ...  } 
like image 196
Kraigolas Avatar answered Oct 06 '22 00:10

Kraigolas