带有片段和 Jetpack 导航的 Viewpager2:恢复片段而不是重新创建它们
Posted
技术标签:
【中文标题】带有片段和 Jetpack 导航的 Viewpager2:恢复片段而不是重新创建它们【英文标题】:Viewpager2 with fragments and Jetpack navigation: Restore fragments instead of recreating them 【发布时间】:2019-12-07 20:08:48 【问题描述】:我在Fragment
中有一个Viewpager2
(我们称之为HomeFragment
)。 Viewpager
本身也包含 Fragments
。当我离开HomeFragment
时,它的视图将被破坏,当我返回时,视图将被重新创建。现在我在onViewCreated()
期间将Viewpager2
的适配器设置在HomeFragment
中。因此,当我导航回HomeFragment
时,将重新创建适配器,这也会重新创建Viewpager2
中的所有Fragments
,并且当前项目重置为0。如果我尝试重新使用我实例化的适配器第一次创建HomeFragment
i 得到一个异常,因为FragmentStateAdapter
内部的这个检查:
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView)
checkArgument(mFragmentMaxLifecycleEnforcer == null);
有没有人知道我在返回时如何防止重新创建所有内容?否则,这是一个相当大的性能开销,会阻碍我的用户体验。
【问题讨论】:
你有什么解决办法吗?我也有同样的问题 不完全,但我认为问题有所不同。重新创建适配器很好,我只是在我的数据绑定绑定适配器中调用了notifyDataSetChanged()
,并且每次创建视图时都会调用它,因为视图模型的实时数据被重新附加。您需要检查数据是否已更改,然后才调用notifyDataSetChanged()
,因为这将重新创建所有片段。我也将不得不尝试为此使用DiffUtil
就我而言,我只是评论viewPager.offscreenPageLimit = 3
并且当前项目保存状态
您找到解决方案了吗?我遇到了类似的问题。谷歌搜索...
有什么解决办法吗?我在同一条船上。 @extmkv,在升级到 beta05 时,它是哪个依赖项解决了您的问题?
【参考方案1】:
我已经花了一些时间来解决这个问题,并且我已经为任何需要听到它的人诊断了这个问题。我试图让我的解决方案尽可能传统。如果我们看看你的陈述:
因此,当我导航回 HomeFragment,它还会重新创建 Viewpager2 中的所有 Fragment 和 当前项目被重置为 0。
问题是当前项目被重置为 0,因为您的适配器所基于的列表被重新创建。为了解决这个问题,我们不需要保存适配器,只需要保存其中的数据。考虑到这一点,解决问题一点也不难。
让我们来布局一些定义:
正如你所说,HomeFragment
是你的ViewPager2
的主机,
MainActivity
是正在运行的 Activity,它承载 HomeFragment
以及其中所有创建的片段
我们正在对MyFragment
的实例进行分页。您甚至可以拥有不止一种类型的片段供您翻页,但这超出了本示例的范围。
PagerAdapter
是你的FragmentStateAdapter
,它是HomeFragment
的ViewPager2
的适配器。
在此示例中,MyFragment
具有构造函数 constructor(id : Int)
。那么PagerAdapter
大概会出现如下:
class PagerAdapter(fm : Fragment) : FragmentStateAdapter(fm)
var ids : List<Int> = listOf()
...
override fun createFragment(position : Int) : Fragment
return MyFragment(ids[position])
我们面临的问题是每次重新创建 PagerAdapter
时都会调用构造函数,而正如我们在上面看到的那样,该构造函数会将 ids
设置为一个空列表。
我的第一个想法是也许我可以将fm
切换为MainActivity
。我没有离开MainActivity
,所以我不知道为什么,但这个解决方案不起作用。
相反,您需要做的是从PagerAdapter
中提取数据。创建一个“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>
)
然后,在PagerAdapter
中进行以下调整:
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])
最后,在HomeFragment
中,你会得到类似的东西:
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
...
...
【讨论】:
这不适用于my case,当切换到包含ViewPager
的先前Fragment
时应用程序崩溃。虽然,我已经实现了崩溃的修复(Fragment no longer exists for key f#0
),但这不适用于这个。是的,我将它用于不止一种类型的 Fragment。【参考方案2】:
您可以将 ViewPager 的初始页面作为 NavHostFragment,它们有自己的后台堆栈,这将导致在下面的 gif 中实现
为每个选项卡创建一个 NavHost 片段,或者可以使用通用的片段添加它
/**
* 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
此片段的布局
<?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_
android:layout_>
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_
android:layout_
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_
android:layout_
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_
android:layout_
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>
并为ViewPager2
的每个页面创建一个导航图,如您在上面看到的仪表板,我们需要nav_graph_dashboard。
此页面的图表是
<?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>
让我们将这些 NavHostFragments 与 FragmentStateAdapter 合并,并实现默认情况下不起作用的后按导航。
/**
* 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://***.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()
您还需要注意内存泄漏,因此如果您的 ViewPager2
本身位于 Fragment
中,请使用 viewLifecycleOwner 而不是 lifeycleOwner
。
您可以在此 tutorial link 查看其他示例和更多内容。
【讨论】:
我认为这对我的情况来说太复杂了,我仍然没有得到它。我在 homeFragment(导航组件的一部分)中有 viewpager2,这个 viewpager2 是图像轮播。我希望每当用户单击此轮播时,它都会缩小(跳转到 zoomFragment)。它实际上是工作,但返回时出现问题。它的崩溃和错误说Expected the adapter to be 'fresh' while restoring state.
【参考方案3】:
我试过设置
viewPager2.setOffscreenPageLimit(ViewPager2.OFFSCREEN PAGE LIMIT_DEFAULT);
然后,片段的行为就变得正常了。
关于 OffscreenPageLimit 的更多信息here
【讨论】:
【参考方案4】:我使用FragmentActivity
而不是Fragment
作为适配器构造函数的参数,它可以工作。当导航回HomeFragment
时,会重新创建适配器,但不会重新创建子片段。
上一个:
class HomeFragmentAdapter() : FragmentStateAdapter(fragment)
现在:
class HomeFragmentAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity)
或
// needs FragmentActivity's lifecycle
FragmentStateAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle)
【讨论】:
这总是错误的解决方案,并且会导致片段在配置更改或进程死亡/重新创建后无法正确恢复其状态。您需要使用带有 Fragment 的构造函数来正确嵌套 Fragment。 嗨@ianhanniballake 我在这里问了基本相同的问题:***.com/questions/71445630/…。这是一个没有答案的常见问题,而且非常令人困惑。我们是否正确地说 FragmentStateAdapters 只为配置更改保存和恢复片段,并且当我们“返回”到 viewpager2 时无法这样做?非常感谢您对此或我的问题的回答,谢谢。 @DanielWilson - 我不知道你在说什么。只要您使用正确的构造函数,每个 Fragment 都会完美地保存和恢复其state,无论是在后台堆栈、配置更改还是进程死亡。跨度>以上是关于带有片段和 Jetpack 导航的 Viewpager2:恢复片段而不是重新创建它们的主要内容,如果未能解决你的问题,请参考以下文章
使用jetpack导航组件从一个底页导航到另一个底页时如何弹出底页对话框