Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
442 views
in Technique[技术] by (71.8m points)

android - 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.

question from:https://stackoverflow.com/questions/57271553/viewpager2-with-fragments-and-jetpack-navigation-restore-fragments-instead-of-r

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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 
        ...
    }
    
    ...

}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...