Viewpager2 con fragmentos y navegación Jetpack: restaure fragmentos en lugar de recrearlos

9 minutos de lectura

tengo un Viewpager2 dentro de una Fragment (Vamos a llamarlo HomeFragment). Que Viewpager en sí mismo también contiene Fragments. Cuando me alejo del HomeFragment su vista se destruirá y cuando navegue hacia atrás, la vista se volverá a crear. Ahora configuro el adaptador del Viewpager2 en el HomeFragment durante onViewCreated(). Por lo tanto, el adaptador se volverá a crear cuando navegue de regreso a la HomeFragmentque también recrea todos Fragments en el Viewpager2 y el elemento actual se restablece a 0. Si trato de reutilizar el adaptador que instalé en la primera creación del HomeFragmentObtengo una excepción, debido a este control dentro del FragmentStateAdapter:

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

¿Alguien tiene una idea de cómo puedo evitar recrear todo cuando navego hacia atrás? De lo contrario, esto es una sobrecarga de rendimiento bastante grande y dificulta mi UX.

  • Tuviste alguna solucion?, tengo el mismo problema

    – Mahmud Ali

    17 de agosto de 2019 a las 8:39

  • No del todo, pero creo que el problema es algo diferente. Recrear el adaptador está bien, acabo de llamar notifyDataSetChanged() dentro de mi adaptador de enlace de enlace de datos y se llamará cada vez que se cree la vista a medida que se vuelvan a adjuntar los datos en vivo del modelo de vista. Debe verificar si los datos han cambiado y solo luego llamar notifyDataSetChanged() ya que esto recreará todos los fragmentos. También tendré que intentar usar DiffUtil para esto

    – sunilson

    17 de agosto de 2019 a las 11:02

  • en mi caso solo comento viewPager.offscreenPageLimit = 3 y el elemento actual guarda su estado

    – Mahmud Ali

    25 de agosto de 2019 a las 15:35

  • encontraste solución para eso? Me encontré con un problema similar. googleando…

    – Georgi Chebotarev

    3 de junio de 2020 a las 20:08

  • ¿Alguna solución? Estoy en el mismo barco. También @extmkv, ¿qué dependencia es, cuando la actualización a beta05 solucionó su problema?

    – Karthik

    14 de agosto de 2020 a las 3:57

Puede tener páginas iniciales de ViewPager como NavHostFragment que tienen sus propias pilas traseras, lo que dará como resultado la implementación en gif a continuación.

ingrese la descripción de la imagen aquí

Cree un fragmento de NavHost para cada pestaña o puede haber generalizado uno lo agregará

/**
 * 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

    }

}

Diseño para este fragmento

<?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>

Y cree un gráfico de navegación para cada página del ViewPager2para el tablero como puede ver arriba, necesitamos nav_graph_dashboard.

El gráfico de esta página es

<?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>

Y fusionemos estos NavHostFragments con FragmentStateAdapter e implementemos la navegación de retroceso que no funciona de forma predeterminada.

/**
 * 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()
        }
    }

}

También debe tener en cuenta las fugas de memoria, así que use viewLifecycleOwner en lugar de lifeycleOwner si tu ViewPager2 mismo dentro de un Fragment.

Puedes ver otras muestras y más en este tenlace tutorial.

  • Creo que es una respuesta demasiado compleja para mi caso, y todavía no la entendí. Tengo viewpager2 en homeFragment (parte del componente de navegación) y este viewpager2 es un carrusel de imágenes. Quiero cuando el usuario haga clic en este carrusel y se alejará (saltar a zoomFragment). En realidad funciona, pero aparece un problema cuando regresa. Es accidente y error dijo Expected the adapter to be 'fresh' while restoring state.

    – Nanda Z.

    28 de diciembre de 2020 a las 16:32


Avatar de usuario de ANDREIRTT
ANDREIRTT

intenté configurar

viewPager2.setOffscreenPageLimit(ViewPager2.OFFSCREEN PAGE LIMIT_DEFAULT);

Y después de eso, el comportamiento de los fragmentos se normalizó.

Más información sobre OffscreenPageLimit aquí

Avatar de usuario de Kraigolas
Kraigolas

He pasado un poco de tiempo con esto y he diagnosticado el problema para cualquiera que necesite escucharlo. Traté de mantener mi solución lo más convencional posible. Si nos fijamos en su declaración:

Por lo tanto, el adaptador se volverá a crear cuando navegue de regreso a HomeFragment, que también recrea todos los fragmentos en Viewpager2 y el elemento actual se restablece a 0.

los problema es que el elemento actual se restablece a 0, porque se recrea la lista en la que se basa su adaptador. Para resolver el problema, no necesitamos guardar el adaptador, solo los datos que contiene. Con eso en mente, resolver el problema no es nada difícil.

Diseñemos algunas definiciones:

  • HomeFragment es, como has dicho, el anfitrión de tu ViewPager2,
  • MainActivity es la actividad en ejecución que alberga HomeFragment y todos los fragmentos creados dentro de él
  • Estamos hojeando instancias de MyFragment. Incluso podría tener más de un tipo de fragmento que hojear, pero eso está más allá del alcance de este ejemplo.
  • PagerAdapter es tuyo FragmentStateAdapterque es el adaptador para HomeFragment‘s ViewPager2.

En este ejemplo, MyFragment tiene el constructor constructor(id : Int). Después, PagerAdapter probablemente va a aparecer de la siguiente manera:

class PagerAdapter(fm : Fragment) : FragmentStateAdapter(fm){
    
    var ids : List<Int> = listOf()

    ...
    
    override fun createFragment(position : Int) : Fragment{
        return MyFragment(ids[position])
    }
    

}

El problema al que nos enfrentamos es cada vez que recreas PagerAdapter se llama al constructor y ese constructor, como podemos ver arriba, establece ids a una lista vacía.

Mi primer pensamiento fue que tal vez podría cambiar fm ser – estar MainActivity. No navego fuera de MainActivity así que no estoy seguro de por qué, pero esta solución no funciona.

En su lugar, lo que debe hacer es abstraer los datos de PagerAdapter. Crea un “modelo de vista”:

    /* 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> 
    )

Entonces, en PagerAdapterrealice los siguientes ajustes:

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])
    }
    

}

Finalmente, en HomeFragmenttendrás algo como:

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

}

  • Esto no funciona para mi caso, bloquea la aplicación al cambiar a la anterior Fragment que contiene el ViewPager. Aunque ya he implementado la solución para el bloqueo (Fragment no longer exists for key f#0) pero eso no funciona con este. Y sí, lo estoy usando para más de un tipo de Fragmento.

    – Lalit Fauzdar

    10 mayo 2021 a las 14:34


yo suelo FragmentActivity en vez de Fragment como argumentos para el constructor del adaptador y funciona. Cuando navegue de regreso a la HomeFragmentel adaptador se vuelve a crear pero los fragmentos secundarios no.

Anterior:

class HomeFragmentAdapter() : FragmentStateAdapter(fragment)

ahora:

class HomeFragmentAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity)

o

// needs FragmentActivity's lifecycle
FragmentStateAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle)

avatar de usuario de Blinker
intermitente

Es un error en un ViewPager2 (o en realidad en un RecyclerView)
https://issuetracker.google.com/issues/151212195

Debe reutilizar un adaptador antiguo cuando regrese (para evitar fragmentos duplicados) y en HomeFragment’s onDestroyView () llame a viewPager.adapter = null

[Updated 04.08.2022]

Es muy extraño que mi respuesta sea negativa o_O. Nuevamente, no tiene que volver a crear adaptadores en onViewCreated, es un error (no solo para un ViewPager, sino también para un RecyclerView y otros que usan el enfoque del adaptador).

  • ¿Cómo exactamente estamos reutilizando el adaptador antiguo?

    – Jimly Assiddiqy

    24 de julio a las 9:14

  • @JimlyAsshiddiqy Simplemente créelo en onCreate, no en onCreateView

    – intermitente

    4 ago a las 7:39

  • ¿Cómo exactamente estamos reutilizando el adaptador antiguo?

    – Jimly Assiddiqy

    24 de julio a las 9:14

  • @JimlyAsshiddiqy Simplemente créelo en onCreate, no en onCreateView

    – intermitente

    4 ago a las 7:39

¿Ha sido útil esta solución?