Reputation: 866
I'm using 2 components of the jetpack: Paging library and Navigation.
In my case, I have 2 fragment: ListMoviesFragment & MovieDetailFragment
when I scroll a certain distance and click a movie item of the recyclerview, MovieDetailFragment is attached and the ListMoviesFragment is in the backstack. Then I press back button, the ListMoviesFragment is bring back from the backstack.
The point is scrolled position and items of the ListMoviesFrament are reset exactly like first time attach to its activity. so, how to keep states of recyclerview to prevent that?
In another way, how to keep states of whole fragment like hide/show a fragment with FragmentTransaction in traditional way but for modern way(navigation)
My sample codes:
fragment layout:
<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"/>
Fragment kotlin code:
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()
}
}
Upvotes: 25
Views: 11561
Reputation: 72
This so simple, only use this method cachedIn()
fun getAllData() {
viewModelScope.launch {
_response.value = repository.getPagingData().cachedIn(viewModelScope)
}
}
Upvotes: 1
Reputation: 1088
Simple steps...
Give id
to all the views which you need to maintain state in fragment back stack. Like Scrollview, root layout, Recyclerview
...
The properties which should hold values, initialize in onCreate()
of the fragment. Like adapter, count...except view references.
If you are setting observers use lifecycleowner as this@yourFragment
instead of viewLifecycleOwner.
That's it. Fragment onCreate()
is called only once so setting properties on onCreate()
will be there in memory till the fragment onDestroy()
is called(not onDestroyView).
All the view referencing code should be there after onCreateView() and before onDestroyView().
BTW If possible you can save the properties in ViewModel which will be there even when fragment configuration changes where all the instance variables will be destroyed.
hope this link will help: PagingDataAdapter.refresh() not working after fragment navigation
Upvotes: 0
Reputation: 1605
Try the following steps:
onCreate
instead of onCreateView
. Keep the initialization one time, and attach it in onCreateView
or onViewCreated
.getSingersPagination()
method everytime, instead store it in a companion object or ViewModel
(preferred) and reuse it.Check the following code to get a rough idea of what to do:
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
}
}
}
The causes of your problem are:
Upvotes: 2
Reputation: 339
Well... You can check if the view is initialized or not (for kotlin)
Initialize view variable
private lateinit var _view: View
In onCreateView check if view is initialized or not
if (!this::_view.isInitialized) {
return inflater.inflate(R.layout.xyz, container, false)
}
return _view
And in onViewCreated just check for it
if (!this::_view.isInitialized) {
_view = view
// ui related methods
}
This solution is for onreplace()....but if data is not changing on back press you may use add() method of fragment.
Here oncreateview will be called but your data won't be reloaded.
Upvotes: 0
Reputation: 31
I had the same problem with my recycle view using the paging library. My list list would always reload and scroll to the top when up navigation button is clicked from my details fragment. Picking up from @Janos Breuer's point I moved the initialisation of my view model and initial list call(repository) to the fragment onCreate() method which is called only once in the fragment lifecycle.
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.
Upvotes: 1
Reputation: 1153
Sorry to be late. I had a similar problem and I figured out a solution (maybe not the best). In my case I adapted the orientation changes. What you need is to save and retrieve is the LayoutManager state AND the last key of the adapter.
In the activity / fragment that is showing your RecyclerView declare 2 variables:
private Parcelable layoutState;
private int lastKey;
In 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());
}
}
In 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;
}
}
}
});
}
And that's it. Now your RecyclerView should restore properly.
Upvotes: 0
Reputation: 1800
Whenever the fragment back again, it get the new PagedList, this cause the adapter present new data thus you will see the recycler view move to top.
By moving the creation of PagedList to viewModel and check to return the exist PagedList instead of create new one will solve the problem. Of course, depends on your app business create new PagedList might require but you can control it completely. (ex: when pull to refresh, when user input data to search...)
Upvotes: 2
Reputation: 567
Things to keep in mind when dealing with these issues:
In order to let OS handle the state restoration of a view automatically, you must provide an id. I believe that this is not the case (because you must identify the RecyclerView in order to bind data).
You must use the correct FragmentManager. I had the same issue with a nested fragment and all that i had to do was to use ChildFragmentManager.
The SaveInstanceState() is triggered by the activity. As long as activity is alive, it won't be called.
You can use ViewModels to keep state and restore it in onViewCreated()
Lastly, navigation controller creates a new instance of a fragment every time we navigate to a destination. Obviously, this does not work well with keeping persistent state. Until there is an official fix you can check the following workaround which supports attaching/detaching as well as different backstacks per tab. Even if you do not use BottomNavigationView, you can use it to implement an attaching/detaching mechanism.
Upvotes: 1
Reputation: 480
Since you use NavController, you cannot keep the view of the list fragment when navigating.
What you could do instead is to keep the data of the RecyclerView, and use that data when the view is recreated after back navigation.
The problem is that your adapter and the singersPagination is created anew every time the view of the fragment is created. Instead,
Move singersAdapter
to a field:
private val singersAdapter = HomeSingersAdapter()
Move this part to onAttach
getSingersPagination().observe(viewLifecycleOwner, Observer {
singersAdapter.submitList(it)
})
Call retainInstance(true)
in onAttach
. This way even configuration changes won't reset the state.
Upvotes: 4
Reputation: 480
The sample fragment code you posted does not correspond to the problem description, I guess it's just an illustration of what you do in your app.
In the sample code, the actual navigation (the fragment transaction) is hidden behind this line:
findNavController().navigate(SplashFragmentDirections.actionSplashFragmentToSingerFragment2())
The key is how the details fragment is attached.
Based on your description, your details fragment is probably attached with FragmentTransaction.replace(int containerViewId, Fragment fragment)
. What this actually does is first remove the current list fragment and then add the detail fragment to the container. In this case, the state of the list fragment is not kept. When you press the back button, the onViewCreated
of the list fragment will run again.
To keep the state of your list fragment, you should use FragmentTransaction.add(int containerViewId, Fragment fragment)
instead of replace
. This way, the list fragment remains where it is and it gets "covered" by the detail fragment. When you press the back button, the onViewCreated
will not be called, since the view of the fragment did not get destroyed.
Upvotes: 2
Reputation: 173
On fragment's onSaveinstanceState save the layout info of the recyclerview:
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(KEY_LAYOUT, myRecyclerView.getLayoutManager().onSaveInstanceState());
}
and on onActivityCreated, restore the scroll position:
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
myRecyclerView.getLayoutManager().onRestoreInstanceState(
savedInstanceState.getParcelable(KEY_LAYOUT));
}
}
Upvotes: 3