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

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


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 theHomeFragment, which also recreates all Fragments in the Viewpager2 andthe 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         ...    }        ...}


You can have initial pages of ViewPager as NavHostFragment which have their own back stacks which will result having the implementation in gif below

enter image description here

Create a NavHost fragment for each tab or can have generalized one will add it either

/** * Using [FragmentStateAdapter.registerFragmentTransactionCallback] with [FragmentStateAdapter] solves back navigation instead of using [OnBackPressedCallback.handleOnBackPressed] in every [NavHostFragment] * ### Should set app:defaultNavHost="true" for [NavHostFragment] for this to work */class DashboardNavHostFragment : BaseDataBindingFragment<FragmentNavhostDashboardBinding>() {    override fun getLayoutRes(): Int = R.layout.fragment_navhost_dashboard    private var navController: NavController? = null    private val nestedNavHostFragmentId = R.id.nested_nav_host_fragment_dashboard    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        val nestedNavHostFragment =            childFragmentManager.findFragmentById(nestedNavHostFragmentId) as? NavHostFragment        navController = nestedNavHostFragment?.navController    }}

Layout for this fragment

<?xml version="1.0" encoding="utf-8"?><layout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto">    <androidx.constraintlayout.widget.ConstraintLayout        android:layout_width="match_parent"        android:layout_height="match_parent">        <com.google.android.material.appbar.AppBarLayout            android:id="@+id/appbar"            android:layout_width="match_parent"            android:layout_height="wrap_content"            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"            app:layout_constraintEnd_toEndOf="parent"            android:background="#0D47A1"            app:layout_constraintStart_toStartOf="parent"            app:layout_constraintTop_toTopOf="parent">            <androidx.appcompat.widget.Toolbar                android:id="@+id/toolbar"                android:layout_width="match_parent"                android:layout_height="?attr/actionBarSize"                app:popupTheme="@style/ThemeOverlay.AppCompat.ActionBar" />        </com.google.android.material.appbar.AppBarLayout>        <fragment            android:id="@+id/nested_nav_host_fragment_dashboard"            android:name="androidx.navigation.fragment.NavHostFragment"            android:layout_width="match_parent"            android:layout_height="0dp"            app:layout_constraintBottom_toBottomOf="parent"            app:layout_constraintLeft_toLeftOf="parent"            app:layout_constraintRight_toRightOf="parent"            app:layout_constraintTop_toBottomOf="@+id/appbar"            app:defaultNavHost="true"            app:navGraph="@navigation/nav_graph_dashboard"/>    </androidx.constraintlayout.widget.ConstraintLayout></layout>

And create a navigation graph for each page of the ViewPager2, for dashboard as you can see above we need nav_graph_dashboard.

Graph for this page is

<?xml version="1.0" encoding="utf-8"?><navigation xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    xmlns:tools="http://schemas.android.com/tools"    android:id="@+id/nav_graph_dashboard"    app:startDestination="@id/dashboardFragment1">    <fragment        android:id="@+id/dashboardFragment1"        android:name="com.smarttoolfactory.tutorial6_4_navigationui_viewpager_fragmenttoolbar_nested_navigation.blankfragment.DashboardFragment1"        android:label="DashboardFragment1"        tools:layout="@layout/fragment_dashboard1">        <action            android:id="@+id/action_dashboardFragment1_to_dashboardFragment2"            app:destination="@id/dashboardFragment2" />    </fragment>    <fragment        android:id="@+id/dashboardFragment2"        android:name="com.smarttoolfactory.tutorial6_4_navigationui_viewpager_fragmenttoolbar_nested_navigation.blankfragment.DashboardFragment2"        android:label="DashboardFragment2"        tools:layout="@layout/fragment_dashboard2">        <action            android:id="@+id/action_dashboardFragment2_to_dashboardFragment3"            app:destination="@id/dashboardFragment3" />    </fragment>    <fragment        android:id="@+id/dashboardFragment3"        android:name="com.smarttoolfactory.tutorial6_4_navigationui_viewpager_fragmenttoolbar_nested_navigation.blankfragment.DashboardFragment3"        android:label="DashboardFragment3"        tools:layout="@layout/fragment_dashboard3" >        <action            android:id="@+id/action_dashboardFragment3_to_dashboardFragment1"            app:destination="@id/dashboardFragment1"            app:popUpTo="@id/dashboardFragment1"            app:popUpToInclusive="true" />    </fragment></navigation>

And let's merge these NavHostFragments with FragmentStateAdapter and implement back press navigation which does not work by default.

/** * FragmentStateAdapter to contain ViewPager2 fragments inside another fragment. * * * 🔥 Create FragmentStateAdapter with viewLifeCycleOwner instead of Fragment to make sure * that it lives between [Fragment.onCreateView] and [Fragment.onDestroyView] while [View] is alive * * * https://stackoverflow.com/questions/61779776/leak-canary-detects-memory-leaks-for-tablayout-with-viewpager2 */class ChildFragmentStateAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :    FragmentStateAdapter(fragmentManager, lifecycle) {    init {        // Add a FragmentTransactionCallback to handle changing        // the primary navigation fragment        registerFragmentTransactionCallback(object : FragmentTransactionCallback() {            override fun onFragmentMaxLifecyclePreUpdated(                fragment: Fragment,                maxLifecycleState: Lifecycle.State            ) = if (maxLifecycleState == Lifecycle.State.RESUMED) {                // This fragment is becoming the active Fragment - set it to                // the primary navigation fragment in the OnPostEventListener                OnPostEventListener {                    fragment.parentFragmentManager.commitNow {                        setPrimaryNavigationFragment(fragment)                    }                }            } else {                super.onFragmentMaxLifecyclePreUpdated(fragment, maxLifecycleState)            }        })    }    override fun getItemCount(): Int = 3    override fun createFragment(position: Int): Fragment {        return when (position) {            0 -> HomeNavHostFragment()            1 -> DashboardNavHostFragment()            else -> NotificationHostFragment()        }    }}

You also need to be aware of memory leaks so use viewLifecycleOwner instead of lifeycleOwner if your ViewPager2 itself inside a Fragment.

You can check out other samples and more in this tutorial link.


I tried setting

viewPager2.setOffscreenPageLimit(ViewPager2.OFFSCREEN PAGE LIMIT_DEFAULT);

And after that, the behavior of the fragments became normal.

More information about OffscreenPageLimit here