I have three fragments
MainFragment // Fragment with ViewPager2
ItemFragment // Fragment of the item of the adapter ViewPager2 that contains the RecyclerView
PreviewFragment // Fragment to which I navigate after clicking on any RecyclerView item in the ItemFragment
When I go MainFragment -> PreviewFragment (exactly like that, not from ItemFragment) then on return back the ItemFragment is re-created from scratch. It is understandable because in OnCreateView I do this:
viewPager.adapter = MyAdapter(this)
// Add items
Tried saving MyAdapter like this:
private val adapter: MyAdapter by lazy {
MyAdapter(this)
}
But it leads to a crash, all the solutions described there did not help me.
I also tried to save fragments anywhere, in any repository and cache them there and then add them, but this leads to a bunch of leaks in the LeakCanary of ViewPager2.
In general, I would like to hear how this problem is solved, namely, you need to somehow save the state of the ViewPager2 items, perhaps they really need to be cached somewhere
P.S The main reason why the re-creation of fragments bothers me is that the scroll position of the RecyclerView gets lost
Related
I am using Navigation component in my App, using google Advanced Sample(here).
my problem is when going back to a fragment, the scrolling position does not lost but it rearranges items and moves highest visible items so that top of those item align to top of recyclerview. please see this:
before going to next fragment:
and after back to fragment:
this problem is matter because some times clicked item goes down and not seen until scroll down.
how to prevent this behavior?
please consider:
this problem exist if using navigation component to change fragment. if start fragment using supportFragmentManager.beginTransaction() or start another activity and then go to this fragment it is OK. but if I navigate to another fragment using navigation component this problem is exist.(maybe because of recreating fragment)
also this problem exist if using fragment in ViewPager. i.e recyclerView is in a fragment that handle with ViewPagerAdapter and viewPager is in HomeFragment that opened with Navigation component. if recyclerView is in HomeFragment there is no problem.
no problem with LinearLayoutManager. only with StaggeredGridLayoutManager.
there is not difference if using ViewPager2 and also FragmentStatePagerAdapter
I try to prevent recreate of fragment(by this solution) but not solved.
UPDATE:
you can clone project with this problem from here
When using Navigation Component + ViewPager + StaggeredGridLayoutManager, wrong recyclerView.computeVerticalScrollOffset() has been returned during Fragment recreate.
In general, all layout managers bundled in the support library already know how to save and restore scroll position, but in this case, we had to take responsibility for this.
class TestFragment : Fragment(R.layout.fragment_test) {
private val testListAdapter: TestListAdapter by lazy {
TestListAdapter()
}
private var layoutManagerState: Parcelable? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
postListView.apply {
layoutManager = StaggeredGridLayoutManager(
2, StaggeredGridLayoutManager.VERTICAL
).apply {
gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
}
setHasFixedSize(true)
adapter = testListAdapter
}
testListAdapter.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT
}
override fun onPause() {
saveLayoutManagerState()
super.onPause()
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
restoreLayoutManagerState()
}
private fun restoreLayoutManagerState () {
layoutManagerState?.let { postListView.layoutManager?.onRestoreInstanceState(it) }
}
private fun saveLayoutManagerState () {
layoutManagerState = postListView.layoutManager?.onSaveInstanceState()
}
}
Source code: https://github.com/dautovicharis/MyStaggeredListSample/tree/q_65539771
The Navigation Component behavior is normal when you navigate from one fragment to another. I mean, onDestroyView() method from the previous fragment is executed, so it means that your view is destroyed, but not the fragment. Remembering that fragment has two lifecycles one for the fragment and another one for the view, There was a video about it.
Also, there were issues registered in issue tracker in order to avoid this behavior in some cases and the GitHub issues:
https://issuetracker.google.com/issues/127932815
https://github.com/android/architecture-components-samples/issues/530
The problem is that when you have fragment that is heavy to recreate, is easier to do not destroy it and just add one fragment. So, when you go back it is not recreated. But, for this behavior is not part of navigation component.
Solutions
The easiest solution is to not use navigation component and work with the tradicional way, as you can see this works perfectly in you use case.
You can use the traditional way just for this use case, and use the navigation component for other cases.
You can inflate this view in an activity. So you are adding un activity
But if the previous tree options is not possible. You can try the following:
If you are using viewModel, you can use SaveState. Basically, it can save the data from your fragment, it is like a map data structure, so you can save positions from your list or recycler view. When go back to this fragment, get the position from this saveState object and use scrollToPosition method in order to add the real position.
Recycler view have methods for restore positions. You can see the uses cases for that, because first you need the data and then add the real position, for more details you can visit this link. This configuration for recycler view is useful also when you lose memory and you need to recreate the recycler view with asynchronous data.
Finally, if you want to understand more about how fragment works with navigation component, you can see this link
i have activity with viewpager where in 4 fragment inside first fragment viewpager too have, if i open third> fragment inside activity, first fragment where viewpager was deleting and dastroy his children fragment doesnt destroy this my problem in gif
Code creating viewpager
private fun initViewPagerFragment() {
val adapter = ViewPagerAdapter((activity as AppCompatActivity).supportFragmentManager)
adapter.addFragment(BasicInformationFragment(), "Основна інформація")
adapter.addFragment(ContactPersonsFragment(), "Контактні особи")
view_pager_fragment.adapter = adapter
tabs_fragment.setupWithViewPager(view_pager_fragment)
}
how to delete children or doesnt destroy first fragment?
set viewPager.offscreenPageLimit(4) for keeping 4 pages in memory. Otherwise, refresh the list or request for a list item from onVisible of a fragment.
I have added a ViewPager in RecyclerView. The issue is that, if i press Home Button, or set activity to pause state, and resume activity again, it does not load fragments inside the view pager.
ViewPager tabs are there, but fragments loads blank.
When i debugged, i find out that, onCreateView does not get called in viewPager's fragment, when onResume() is called for activity,
Following is my code in RecyclerAdapter.
private fun bindTabViewHolder(viewHolder: HomeRecyclerAdapter.TabViewHolder, position: Int) {
viewHolder.tabLayout.setupWithViewPager(viewHolder.viewPager)
//Creating View Pager Adapter Instance
viewPagerAdapter = BaseViewPagerAdapter(fragmentManager)
viewPagerAdapter?.addFragment(homeFragment, dataList?.get(position)?.teamName) // this line can crash
viewPagerAdapter?.addFragment(awayFragment, dataList?.get(1)?.teamName) // this line can crash
viewHolder.viewPager.adapter = viewPagerAdapter
}
I think if it starts calling onCreateView() for fragments, issue will be resolved, Also if i try to get fresh data from the feed, by pull to refresh, if does not load fragments inside viewPager.
You should generally never put a ViewPager that hosts Fragments inside a RecyclerView, as the IDs / tags in the FragmentManager will get confused.
You will need to use a ViewPager in the RecyclerView that hosts Views (by subclassing PagerAdapter, instead of FragmentPagerAdapter) instead.
I have encountered some problems in "recreating" an adapter from the recyclerView in the onResume method.
Basically, this is my scheme.
MainActivit -ViewPager
- fragment A (has a recyclerView)
- fragment B (nop)
- fragment C (nop)
- fragment D (it has the same recyclerView as the framgnet A)
onResume I always call for
> myCustomAdapter adapter = new myCustomAdapter (
> mListItems,getContext(),ParseUser.getCurrentUser().getObjectId(),
> "type");
> recyclerView.setAdapter(adapter);
but this causes some problems, like when I go back to MainActivity from another activity. For example.
Activityt A
- click on item in recylerView
- start Activity B
Activity B
user does some actions, sees some news, and returns to Activity A.
Activity A
recreates the adapter and set recyclerView.setAdapter (new Adapter);
this is slow, causes a delay of 2 seconds after onBackPressed is pressed in Activity B.
I also have a setMenuVisibility method that also does the same thing as onResume, because as informed in fragment D, I have the recyclerView in fragment A, so if a user makes some change in the recyclerView that is in fragment D, I need update the recyclerView of fragment A when the user returns to it.
Why the same recyclerView in Fragment D is in Fragment A?
We can consider the following, in fragment A, I have a recyclerView that contains only the "user interests", and in fragment D, I have user information such as user name, photo, etc ... and also the "user interests".
Conclusion: The problem is when I return from Activity B to Activity A, and when I alternate between fragment A and fragment D in viewPager, this causes a delay for the re-creation of the adapter.
What should I do in this situation?
I apologize for this horrible English, I'm using google translator.
It is a Scope Problem. You need to define adapter in somewhere class member.
If you want the data presented in Fragment A and Fragment D different, then make adapter as a member of each other, and init it in each Fragment.onCreate(). So it won't recreate adapter evert time. (Make sure ViewPager has enough cache number, or it will still recreate the whole non-cached Fragment)
If you want the FragmentA and FragmentD display identical data or part of the same data, create adapter in MainActivity.onCreate and pass it as parameters into FragmentA, FragmentD.
It is not need to recreate adapter every time onResume because it's really a heavy job. No need to change any view layer attributes. Make sure the data(adapter) which you want to share is in the same scope or can pass to it.
For example:
//MainActivity
private Adapter adapter;
void onCreate(Bundle savedInstanceState){
//...other stuff
adapter = new myCustomAdapter (mListItems,getContext(),ParseUser.getCurrentUser().getObjectId(), "type");
}
public Adapter getAdapter(){ return adapter;}
//FragmentA or FragmentD
void onCreateView(){
MainActivity activity = (MainActivity) getActivity(); //This fragment should be created with `MainActivity`
recyclerView.setAdapter(activity.getAdapter());
}
Do not set set image using "src" attribute of ImageView when dealing with large images or multiple small images
Use an image loading library like Glide or Picasso.
The following snippet is for Glide v4
Glide.with(context).load(<url_or_res_id>).into(imageView)
You can pass url as string. Or if you are loading images from drawable, you can pass its resource id (eg. R.drawable.image_id)
I'm trying to create a ViewPager with six fragments but only 2nd fragment to 5th fragment contain data that I want to show and the first fragment and the last fragment I want to be used to reload the data and set the position to the 2nd fragment again. The overall flow is like this :
1st (reload and go back to 2nd) <- 2nd fragment <-> 5th fragment -> 6th fragment (same with 1st)
what I've tried is I create a callback from the 1st fragment and 6th fragment like this
public static class callbackFragmentLoading implements callbackFragmentLoad {
#Override
public void onLoading() {
mPager.setAdapter(mAdapter);
mPager.setCurrentItem(2,false);
}
}
and I passed the callback to the fragment constructor so I can called the onLoading function in the onActivityCreated. But I everytime I do it the application will be force closed and the logcat shows
recursive entry to executependingtransactions
is there any way to do this? or my method for doing it is wrong?
Thank You
is there any way to do this? or my method for doing it is wrong?
Messing with callbacks between Fragments of a ViewPager isn't probably such a good idea. Instead I would do it like this:
Don't load any data(like with a Loader) in the Fragments from the ViewPager, instead let the FragmentActivity do it(and the Fragments will get it through methods from the Activity).
Your two loading fragments(position 0 and 5) will call in their onResume method a reload action on the parent Activity(like a Loader restart)
At this moment the Activity will load/reload the data and when that finishes it will set the ViewPager to the correct items(either 1 or 4)
in the onResume method of the data fragments you'll refresh the fragment's data(here you may need to use some sort of signaling system because you'll need to duplicate the refresh code in the onCreateView(some fragments may have their view destroyed if they are far apart from the current visible position)).
As I don't know many things about the inner data fragment I've written a basic skeleton sample(without the data loading in the activity).