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 HomeFragment
que 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 HomeFragment
Obtengo 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.
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.
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 ViewPager2
para 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
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í
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 tuViewPager2
,MainActivity
es la actividad en ejecución que albergaHomeFragment
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 tuyoFragmentStateAdapter
que es el adaptador paraHomeFragment
‘sViewPager2
.
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 PagerAdapter
realice 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 HomeFragment
tendrá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 elViewPager
. 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 HomeFragment
el 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)
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
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 llamarnotifyDataSetChanged()
ya que esto recreará todos los fragmentos. También tendré que intentar usarDiffUtil
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