使用分页库和导航架构组件将recyclerview的状态保持在片段中

时间:2019-01-10 09:55:56

标签: android android-recyclerview android-paging android-architecture-navigation

我正在使用jetpack的两个组件:分页库和导航。

就我而言,我有2个片段:ListMoviesFragment和MovieDetailFragment

当我滚动一定距离并单击recyclerview的电影项目时,MovieDetailFragment被附加,并且ListMoviesFragment在后堆栈中。然后,我按下“后退”按钮,将ListMoviesFragment从堆栈中移回。

该点是滚动位置,并且ListMoviesFrament的项被重置,就像第一次附加到其活动一样。那么,如何保持recyclerview状态以防止这种情况发生?

以另一种方式,如何使用FragmentTransaction以传统方式但以现代方式(导航)保持整个片段的状态(例如隐藏/显示片段)

我的示例代码:

片段布局:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
         xmlns:tools="http://schemas.android.com/tools"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         tools:context="net.karaokestar.app.SplashFragment">

<TextView
        android:id="@+id/singer_label"
        android:layout_width="wrap_content" android:layout_height="wrap_content"
        android:text="Ca sĩ"
        android:textColor="@android:color/white"
        android:layout_alignParentLeft="true"
        android:layout_toLeftOf="@+id/btn_game_more"
        android:layout_centerVertical="true"
        android:background="@drawable/shape_label"
        android:layout_marginTop="10dp"
        android:layout_marginBottom="@dimen/header_margin_bottom_list"
        android:textStyle="bold"
        android:padding="@dimen/header_padding_size"
        android:textAllCaps="true"/>


<androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list_singers"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

科特林片段代码:

    package net.karaokestar.app
    import android.os.Bundle
    import android.view.LayoutInflater
    import android.view.View
    import android.view.ViewGroup
    import androidx.fragment.app.Fragment
    import androidx.lifecycle.LiveData
    import androidx.lifecycle.Observer
    import androidx.navigation.fragment.findNavController
    import androidx.paging.LivePagedListBuilder
    import androidx.paging.PagedList
    import androidx.recyclerview.widget.LinearLayoutManager
    import kotlinx.android.synthetic.main.fragment_splash.*
    import net.karaokestar.app.home.HomeSingersAdapter
    import net.karaokestar.app.home.HomeSingersRepository

    class SplashFragment : Fragment() {

        override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            // Inflate the layout for this fragment
            return inflater.inflate(R.layout.fragment_splash, container, false)
        }

        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            val singersAdapter = HomeSingersAdapter()
            singersAdapter.setOnItemClickListener{
findNavController().navigate(SplashFragmentDirections.actionSplashFragmentToSingerFragment2())
}
            list_singers.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
            list_singers.setHasFixedSize(true)
            list_singers.adapter = singersAdapter

            getSingersPagination().observe(viewLifecycleOwner, Observer {
                singersAdapter.submitList(it)
            })
        }

        fun getSingersPagination() : LiveData<PagedList<Singer>> {
            val repository = HomeSingersRepository()
            val pagedListConfig = PagedList.Config.Builder().setEnablePlaceholders(true)
                .setPageSize(Configurations.SINGERS_PAGE_SIZE).setPrefetchDistance(Configurations.SINGERS_PAGE_SIZE).build()

            return LivePagedListBuilder(repository, pagedListConfig).build()
        }
    }

8 个答案:

答案 0 :(得分:1)

在片段的onSaveinstanceState上保存recyclerview的布局信息:

    @Override
    public void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putParcelable(KEY_LAYOUT, myRecyclerView.getLayoutManager().onSaveInstanceState());
    }

并在onActivityCreated上恢复滚动位置:

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        if (savedInstanceState != null) {
           myRecyclerView.getLayoutManager().onRestoreInstanceState(
                    savedInstanceState.getParcelable(KEY_LAYOUT));
        }
    }

答案 1 :(得分:1)

您发布的示例片段代码与问题描述不符,我想这只是您在应用程序中所做的一个说明。

在示例代码中,实际的导航(片段事务)隐藏在此行的后面:

findNavController().navigate(SplashFragmentDirections.actionSplashFragmentToSingerFragment2())

关键是细节片段的附加方式。

根据您的描述,您的详细信息片段可能附有FragmentTransaction.replace(int containerViewId, Fragment fragment)。这实际上是先删除当前列表片段,然后将详细信息片段添加到容器中。在这种情况下,列表片段的状态不会保留。当您按下返回按钮时,列表片段的onViewCreated将再次运行。

要保持列表片段的状态,应使用FragmentTransaction.add(int containerViewId, Fragment fragment)而不是replace。这样,列表片段将保留在原处,并被细节片段“覆盖”。当您按下后退按钮时,onViewCreated将不会被调用,因为片段的视图没有被破坏。

答案 2 :(得分:1)

由于使用了NavController,因此导航时无法保留列表片段的视图。

您可以做的是保留RecyclerView的数据,并在向后导航后重新创建视图时使用该数据。

问题在于,每次创建片段的 view 时,都会重新创建您的适配器和singersPagination。

  1. singersAdapter移至字段:

    private val singersAdapter = HomeSingersAdapter()
    
  2. 将此部分移至onAttach

    getSingersPagination().observe(viewLifecycleOwner, Observer {
        singersAdapter.submitList(it)
    })
    
  3. retainInstance(true)中呼叫onAttach。这样,即使配置更改也不会重置状态。

答案 3 :(得分:1)

每当片段再次返回时,它将获得新的PagedList,这将导致适配器显示新数据,因此您将看到回收站视图移至顶部。

通过将PagedList的创建移动到viewModel并检查返回存在的PagedList而不是创建新的PagedList可以解决此问题。当然,取决于您的应用业务,创建新的PagedList可能需要,但是您可以完全控制它。 (例如:拉动刷新时,用户输入数据进行搜索时...)

答案 4 :(得分:0)

处理这些问题时要记住的事情:

  1. 为了让OS自动处理视图的状态恢复,必须提供一个ID。我相信情况并非如此(因为必须绑定RecyclerView才能绑定数据)。

  2. 您必须使用正确的FragmentManager。我对嵌套片段有同样的问题,而我要做的就是使用ChildFragmentManager。

  3. SaveInstanceState()由活动触发。只要活动仍然存在,就不会调用它。

  4. 您可以使用ViewModels保持状态并将其还原到onViewCreated()

最后,每次我们导航到目的地时,导航控制器都会创建一个片段的新实例。显然,这不适用于保持持久状态。在没有正式修复之前,您可以检查以下变通方法,该方法支持附加/分离以及每个选项卡使用不同的后台堆栈。即使您不使用BottomNavigationView,也可以使用它来实现附加/分离机制。

https://github.com/googlesamples/android-architecture-components/blob/27c4045aa0e40d402bbbde16d7ae0c9822a34447/NavigationAdvancedSample/app/src/main/java/com/example/android/navigationadvancedsample/NavigationExtensions.kt

答案 5 :(得分:0)

抱歉,迟到了。我有一个类似的问题,我想出了一个解决方案(也许不是最好的)。就我而言,我适应了方向变化。 您需要保存和检索的是LayoutManager状态以及适配器的最后一个键。

在显示您的RecyclerView的活动/片段中,声明2个变量:

private Parcelable layoutState;
private int lastKey;

在onSaveInstanceState中:

@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
    super.onSaveInstanceState(outState);
    if(adapter != null) {
        lastKey = (Integer)adapter.getCurrentList().getLastKey();
        outState.putInt("lastKey",lastKey);
        outState.putParcelable("layoutState",dataBinding.customRecyclerView
        .getLayoutManager().onSaveInstanceState());
    }      
 }

在onCreate中:

 @Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //.... setContentView etc...

    if(savedInstanceState != null) {
        layoutState = savedInstanceState.getParcelable("layoutState");
        lastKey = savedInstanceState.getInt("lastKey",0);
    }

    dataBinding.customRecyclerView
   .addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(@NonNull RecyclerView   recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
        }

        @Override
        public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            if(lastKey != 0) {
                dataBinding.customRecyclerView.scrollToPosition(lastKey);
                Log.d(LOG_TAG, "list restored; last list position is: " + 
                ((Integer)adapter.getCurrentList().getLastKey()));
                lastKey = 0;
                if(layoutState != null) {
                    dataBinding.customRecyclerView.getLayoutManager()
                    .onRestoreInstanceState(layoutState);
                    layoutState = null;
                }
            }
        }


    });
}

就是这样。现在您的RecyclerView应该可以正确还原了。

答案 6 :(得分:0)

我使用分页库的回收视图也遇到了同样的问题。从我的详细信息片段中单击向上导航按钮时,我的列表列表将始终重新加载并滚动到顶部。从@Janos Breuer的观点出发,我将视图模型的初始化和初始列表调用(存储库)移至片段onCreate()方法,该方法在片段生命周期中仅被调用一次。

onCreate()创建片段时,系统将调用此方法。您应该初始化在暂停或停止然后恢复片段时要保留的片段的基本组件。

答案 7 :(得分:0)

好吧...您可以检查视图是否已初始化(对于 kotlin)

初始化视图变量

  private lateinit var _view: View

在 onCreateView 检查视图是否初始化

    if (!this::_view.isInitialized) {
        return inflater.inflate(R.layout.xyz, container, false)
    }
    return _view

在 onViewCreated 中只需检查它

  if (!this::_view.isInitialized) {
        _view = view
       // ui related methods
    }

此解决方案适用于 onreplace()....但如果数据在后退时没有发生变化,您可以使用片段的 add() 方法。

此处将调用 oncreateview,但不会重新加载您的数据。