android-fragments 带有片段和Jetpack导航的Viewpager2:还原片段而不是重新创建片段

jrcvhitl  于 2022-11-14  发布在  Android
关注(0)|答案(5)|浏览(203)

我在一个Fragment里面有一个Viewpager2(我们称之为HomeFragment)。Viewpager本身也包含Fragments。当我导航离开HomeFragment时,它的视图将被破坏,当我导航回来时,视图将被重新创建。现在我在onViewCreated()期间在HomeFragment中设置了Viewpager2的适配器。因此,当我导航回HomeFragment时,将重新创建适配器,这也将重新创建Viewpager2中的所有Fragments,并且当前项将重置为0。如果我尝试重用在第一次创建HomeFragment时示例化的适配器,则会出现异常,由于FragmentStateAdapter

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

有没有人知道我如何才能防止在导航回来时重新创建所有内容?否则,这将是一个相当大的性能开销,并阻碍我的UX。

7ivaypg9

7ivaypg91#

您可以将ViewPager的初始页面作为NavHostFragment,这些页面具有自己的后台堆栈,这将导致实现下面的gif

为每个选项卡创建一个NavHost片段,或者可以将其添加到通用的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_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>

ViewPager2的每个页面创建一个导航图,对于上面所示的 Jmeter 板,我们需要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://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()
        }
    }

}

您还需要注意内存泄漏,因此如果您的ViewPager2本身位于Fragment中,请使用viewLifecycleOwner而不是lifeycleOwner
您可以在此t utorial link中查看其他示例和更多内容。

dgenwo3n

dgenwo3n2#

我试过设置

viewPager2.setOffscreenPageLimit(ViewPager2.OFFSCREEN PAGE LIMIT_DEFAULT);

而在那之后,碎片的行为就变得正常了。
有关OffscreenPageLimit的详细信息,请单击此处

myss37ts

myss37ts3#

我花了一点时间来分析这个问题,我已经为任何需要了解这个问题的人诊断出了问题所在。我试图尽可能地保持我的解决方案的常规性。如果我们看看你的陈述:
因此,当我导航回HomeFragment时,将重新创建适配器,这也将重新创建Viewpager2中的所有Fragments,并且当前项重置为0。

  • 问题 * 是当前项被重置为0,因为您的适配器所基于的列表被重新创建。要解决这个问题,**我们不需要保存适配器,只需要保存其中的数据。**考虑到这一点,解决这个问题一点也不难。

让我们来设计一些定义:

  • HomeFragmentViewPager2的主机,
  • MainActivity是正在运行的Activity,它托管HomeFragment以及在其中创建的所有片段
  • 我们正在对MyFragment的示例进行分页,您甚至可以对多种类型的片段进行分页,但这超出了本示例的范围。
  • PagerAdapter是你的FragmentStateAdapter,它是HomeFragmentViewPager2的适配器。

在本例中,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 
        ...
    }
    
    ...

}
7xllpg7q

7xllpg7q4#

我使用FragmentActivity而不是Fragment作为适配器的构造函数的参数,并且它工作了。当导航回HomeFragment时,适配器被重新创建,但是子片段没有被重新创建。
上一页:

class HomeFragmentAdapter() : FragmentStateAdapter(fragment)

现在:

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

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

yi0zb3m45#

这是ViewPager2(或者实际上是RecyclerView)https://issuetracker.google.com/issues/151212195中的一个错误
返回时必须重用旧适配器(以避免片段重复),并在HomeFragment的onDestroyView()中调用viewPager。adapter = null
[更新日期:04.08.2022]很奇怪,我的答案是downvoted o_O。同样,您不必在onViewCreated中重新创建适配器,这是一个错误(不仅对于ViewPager,而且对于RecyclerView和其他使用适配器方法的人)。

相关问题