使用分页库和导航架构组件将 recyclerview 的状态保持在片段中
Keeping states of recyclerview in fragment with paging library and navigation architecture component
我正在使用 Jetpack 的 2 个组件:寻呼库和导航。
在我的例子中,我有 2 个片段:ListMoviesFragment 和 MovieDetailFragment
当我滚动一定距离并单击 recyclerview 的电影项目时,会附加 MovieDetailFragment 并且 ListMoviesFragment 在后台。然后我按下后退按钮,ListMoviesFragment 从后台返回。
点是滚动位置,ListMoviesFrament 的项目被重置,就像第一次附加到它的 activity 一样。那么,如何保持 recyclerview 的状态以防止出现这种情况?
换句话说,如何保持整个片段的状态,例如 hide/show 以传统方式使用 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"/>
片段 kotlin 代码:
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()
}
}
在fragment的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));
}
}
您发布的示例片段代码与问题描述不符,我想这只是您在应用程序中所做操作的说明。
在示例代码中,实际的导航(片段交易)隐藏在这一行后面:
findNavController().navigate(SplashFragmentDirections.actionSplashFragmentToSingerFragment2())
关键是细节片段的附加方式。
根据您的描述,您的详细信息片段可能附有 FragmentTransaction.replace(int containerViewId, Fragment fragment)
。这实际上所做的是首先删除当前列表片段,然后将详细信息片段添加到容器中。在这种情况下,不保留列表片段的状态。当您按下后退按钮时,列表片段的 onViewCreated
将再次 运行。
要保持列表片段的状态,您应该使用 FragmentTransaction.add(int containerViewId, Fragment fragment)
而不是 replace
。这样,列表片段将保留在原处,并通过详细信息片段获得 "covered"。当您按下后退按钮时,onViewCreated
不会被调用,因为片段的视图没有被破坏。
由于使用了 NavController,因此在导航时无法保留列表片段的视图。
您可以做的是保留 RecyclerView 的数据,并在返回导航后重新创建视图时使用该数据。
问题是每次创建片段的 view 时都会重新创建适配器和 singersPagination。相反,
移动singersAdapter
到一个字段:
private val singersAdapter = HomeSingersAdapter()
将此部分移至onAttach
getSingersPagination().observe(viewLifecycleOwner, Observer {
singersAdapter.submitList(it)
})
在 onAttach
中调用 retainInstance(true)
。这样即使更改配置也不会重置状态。
处理这些问题时要注意的事项:
为了让OS自动处理视图的状态恢复,您必须提供一个id。我相信情况并非如此(因为您必须识别 RecyclerView 才能绑定数据)。
您必须使用正确的 FragmentManager。我对嵌套片段有同样的问题,我所要做的就是使用 ChildFragmentManager。
SaveInstanceState() 由 activity 触发。只要activity还活着,就不会被调用。
您可以使用 ViewModels 来保持状态并在 onViewCreated() 中恢复它
最后,每次我们导航到目的地时,导航控制器都会创建一个新的片段实例。显然,这不能很好地保持持久状态。在有官方修复之前,您可以检查以下支持 attaching/detaching 的解决方法以及每个选项卡的不同后退堆栈。即使你不使用 BottomNavigationView,你也可以使用它来实现一个 attaching/detaching 机制。
每当片段再次返回时,它会获得新的 PagedList,这会导致适配器呈现新数据,因此您会看到回收器视图移至顶部。
通过将 PagedList 的创建移动到 viewModel 并检查 return 现有的 PagedList 而不是创建新的将解决问题。当然,创建新的 PagedList 可能需要取决于您的应用程序业务,但您可以完全控制它。 (例如:拉动刷新时,用户输入数据进行搜索时...)
抱歉迟到了。我有一个类似的问题,我想出了一个解决方案(也许不是最好的)。就我而言,我调整了方向变化。
您需要保存和检索的是 LayoutManager 状态和适配器的最后一个键。
在显示 RecyclerView 的 activity / 片段中声明 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());
}
}
在创建中:
@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 应该可以正常恢复了。
我在使用分页库的回收视图中遇到了同样的问题。当从我的详细信息片段中单击向上导航按钮时,我的列表总是会重新加载并滚动到顶部。从@Janos Breuer 的观点出发,我将视图模型的初始化和初始列表调用(存储库)移至片段 onCreate() 方法,该方法在片段生命周期中仅调用一次。
onCreate() The system calls this method when creating the fragment. You should initialize essential components of the fragment that you want to retain when the fragment is paused or stopped, then resumed.
嗯...您可以检查视图是否已初始化(对于 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 但不会重新加载您的数据。
请尝试以下步骤:
- 在
onCreate
而不是 onCreateView
中初始化您的适配器。保留一次初始化,附在onCreateView
或onViewCreated
.
- 不要每次都从
getSingersPagination()
方法 return 一个新的 pagedList 实例,而是将其存储在伴随对象或 ViewModel
(首选)中并重新使用它。
检查以下代码以大致了解要做什么:
class SingersViewModel: ViewModel() {
private var paginatedLiveData: MutableLiveData<YourType>? = null;
fun getSingersPagination(): LiveData<YourType> {
if(paginatedLiveData != null)
return paginatedLiveData;
else {
//create a new instance, store it in paginatedLiveData, then return
}
}
}
造成您问题的原因是:
- 您每次都连接一个新的适配器,所以它会跳到顶部。
- 你每次都在创建新的分页列表,所以它跳到顶部,认为数据是全新的。
简单的步骤...
将 id
给所有需要在片段返回堆栈中保持状态 的视图。喜欢 Scrollview, root layout, Recyclerview
...
应保存值的属性,在片段onCreate()
中初始化。与适配器一样,计数...除了视图引用。
如果您要设置观察者,请使用 lifecycleowner 作为 this@yourFragment
而不是 viewLifecycleOwner。
就是这样。片段 onCreate()
仅被调用一次,因此 onCreate()
上的设置属性将一直存在于内存中,直到片段 onDestroy()
被调用(而不是 onDestroyView)。
所有视图引用代码都应该在 onCreateView() 之后和 onDestroyView() 之前 .
顺便说一句,如果可能,您可以将属性保存在 ViewModel 中,即使片段配置发生更改,所有实例变量都将被销毁,这些属性也会存在。
希望这篇 link 对您有所帮助:
我正在使用 Jetpack 的 2 个组件:寻呼库和导航。
在我的例子中,我有 2 个片段:ListMoviesFragment 和 MovieDetailFragment
当我滚动一定距离并单击 recyclerview 的电影项目时,会附加 MovieDetailFragment 并且 ListMoviesFragment 在后台。然后我按下后退按钮,ListMoviesFragment 从后台返回。
点是滚动位置,ListMoviesFrament 的项目被重置,就像第一次附加到它的 activity 一样。那么,如何保持 recyclerview 的状态以防止出现这种情况?
换句话说,如何保持整个片段的状态,例如 hide/show 以传统方式使用 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"/>
片段 kotlin 代码:
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()
}
}
在fragment的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));
}
}
您发布的示例片段代码与问题描述不符,我想这只是您在应用程序中所做操作的说明。
在示例代码中,实际的导航(片段交易)隐藏在这一行后面:
findNavController().navigate(SplashFragmentDirections.actionSplashFragmentToSingerFragment2())
关键是细节片段的附加方式。
根据您的描述,您的详细信息片段可能附有 FragmentTransaction.replace(int containerViewId, Fragment fragment)
。这实际上所做的是首先删除当前列表片段,然后将详细信息片段添加到容器中。在这种情况下,不保留列表片段的状态。当您按下后退按钮时,列表片段的 onViewCreated
将再次 运行。
要保持列表片段的状态,您应该使用 FragmentTransaction.add(int containerViewId, Fragment fragment)
而不是 replace
。这样,列表片段将保留在原处,并通过详细信息片段获得 "covered"。当您按下后退按钮时,onViewCreated
不会被调用,因为片段的视图没有被破坏。
由于使用了 NavController,因此在导航时无法保留列表片段的视图。
您可以做的是保留 RecyclerView 的数据,并在返回导航后重新创建视图时使用该数据。
问题是每次创建片段的 view 时都会重新创建适配器和 singersPagination。相反,
移动
singersAdapter
到一个字段:private val singersAdapter = HomeSingersAdapter()
将此部分移至
onAttach
getSingersPagination().observe(viewLifecycleOwner, Observer { singersAdapter.submitList(it) })
在
onAttach
中调用retainInstance(true)
。这样即使更改配置也不会重置状态。
处理这些问题时要注意的事项:
为了让OS自动处理视图的状态恢复,您必须提供一个id。我相信情况并非如此(因为您必须识别 RecyclerView 才能绑定数据)。
您必须使用正确的 FragmentManager。我对嵌套片段有同样的问题,我所要做的就是使用 ChildFragmentManager。
SaveInstanceState() 由 activity 触发。只要activity还活着,就不会被调用。
您可以使用 ViewModels 来保持状态并在 onViewCreated() 中恢复它
最后,每次我们导航到目的地时,导航控制器都会创建一个新的片段实例。显然,这不能很好地保持持久状态。在有官方修复之前,您可以检查以下支持 attaching/detaching 的解决方法以及每个选项卡的不同后退堆栈。即使你不使用 BottomNavigationView,你也可以使用它来实现一个 attaching/detaching 机制。
每当片段再次返回时,它会获得新的 PagedList,这会导致适配器呈现新数据,因此您会看到回收器视图移至顶部。
通过将 PagedList 的创建移动到 viewModel 并检查 return 现有的 PagedList 而不是创建新的将解决问题。当然,创建新的 PagedList 可能需要取决于您的应用程序业务,但您可以完全控制它。 (例如:拉动刷新时,用户输入数据进行搜索时...)
抱歉迟到了。我有一个类似的问题,我想出了一个解决方案(也许不是最好的)。就我而言,我调整了方向变化。 您需要保存和检索的是 LayoutManager 状态和适配器的最后一个键。
在显示 RecyclerView 的 activity / 片段中声明 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());
}
}
在创建中:
@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 应该可以正常恢复了。
我在使用分页库的回收视图中遇到了同样的问题。当从我的详细信息片段中单击向上导航按钮时,我的列表总是会重新加载并滚动到顶部。从@Janos Breuer 的观点出发,我将视图模型的初始化和初始列表调用(存储库)移至片段 onCreate() 方法,该方法在片段生命周期中仅调用一次。
onCreate() The system calls this method when creating the fragment. You should initialize essential components of the fragment that you want to retain when the fragment is paused or stopped, then resumed.
嗯...您可以检查视图是否已初始化(对于 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 但不会重新加载您的数据。
请尝试以下步骤:
- 在
onCreate
而不是onCreateView
中初始化您的适配器。保留一次初始化,附在onCreateView
或onViewCreated
. - 不要每次都从
getSingersPagination()
方法 return 一个新的 pagedList 实例,而是将其存储在伴随对象或ViewModel
(首选)中并重新使用它。
检查以下代码以大致了解要做什么:
class SingersViewModel: ViewModel() {
private var paginatedLiveData: MutableLiveData<YourType>? = null;
fun getSingersPagination(): LiveData<YourType> {
if(paginatedLiveData != null)
return paginatedLiveData;
else {
//create a new instance, store it in paginatedLiveData, then return
}
}
}
造成您问题的原因是:
- 您每次都连接一个新的适配器,所以它会跳到顶部。
- 你每次都在创建新的分页列表,所以它跳到顶部,认为数据是全新的。
简单的步骤...
将
id
给所有需要在片段返回堆栈中保持状态 的视图。喜欢Scrollview, root layout, Recyclerview
...应保存值的属性,在片段
onCreate()
中初始化。与适配器一样,计数...除了视图引用。如果您要设置观察者,请使用 lifecycleowner 作为
this@yourFragment
而不是 viewLifecycleOwner。
就是这样。片段 onCreate()
仅被调用一次,因此 onCreate()
上的设置属性将一直存在于内存中,直到片段 onDestroy()
被调用(而不是 onDestroyView)。
所有视图引用代码都应该在 onCreateView() 之后和 onDestroyView() 之前 .
顺便说一句,如果可能,您可以将属性保存在 ViewModel 中,即使片段配置发生更改,所有实例变量都将被销毁,这些属性也会存在。
希望这篇 link 对您有所帮助: