ViewPager release 1.0.0 version
For a normal RecyclerView, holder.itemView gives me the view currently binding/ rendering.
However, FragmentStateAdapter's holder.itemView only gives me a FrameLayout
My ViewPager adapater:
class MyFragmentStateAdapter(activity: FragmentActivity) :
FragmentStateAdapter(activity) {
var items = mutableListOf<String>()
override fun createFragment(position: Int): MyPageFragment {
val itemText = items[position]
return MyPageFragment.create(itemText)
}
override fun getItemCount(): Int = items.size
override fun onBindViewHolder(
holder: FragmentViewHolder,
position: Int,
payloads: MutableList<Any>
) {
super.onBindViewHolder(holder, position, payloads)
val fragment = ??? as MyRefreshableInterface
fragment.refresh()
}
fun update(mutableListOf: MutableList<String>) {
this.items = mutableListOf
notifyDataSetChanged()
}
}
Context
I have a small and fix number of tabs displaying different information section of a user profile. Upon certain events, I need to refresh AUTOMATICALLY all the tabs ASAP. In other words, I need to refresh the currently-hidden tabs besides the current tab user is looking at.
ASAP = the next time user visits a currently-hidden tab. User goes there, first thing they see is a loading animation. That's good
Why not immediately?
Because hidden fragments could be detached/ destroyed. User is not looking at the right now anyway. Keeping all the fragments in the memory is also expensive
When user navigates to previous hidden tab say Addresses, onBindViewHolder will be triggered (This is great compared to ViewPager 1) However, the gap is that I have no reference of the currently-selected fragment
Other findings
I've already try referencing fragmentActivity.supportFragmentManager.fragments but it seems to have maximum of 4 fragments whereas I have 6 fragments/ pages, for the sake of testing
You seem to be asking 2 different things.
I need to refresh automatically all the tabs. In other words, I need to refresh the currently-hidden tabs besides the current tab user is looking at.
and
When user navigates to previous hidden tab say Addresses, onBindViewHolder will be triggered.
You are trying to do the update when displayed on this second item.
This "Update when displayed" is easy with viewpager2, when a Fragment is displayed the Fragment is brought up to lifecycle state "Resumed" from "Started".
Thus create an update method in each Fragment and then in the Fragments onResume method call the update method.
(This is what I do in my App)
Update:
The docs on this behaviour is bad in viewpager2, the best I can find is the comment in the source code https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-master-dev/viewpager2/viewpager2/src/main/java/androidx/viewpager2/adapter/FragmentStateAdapter.java#634 of
/**
* Pauses (STARTED) all Fragments that are attached and not a primary item.
* Keeps primary item Fragment RESUMED.
*/
Or the release notes https://developer.android.com/jetpack/androidx/releases/viewpager2#1.0.0-alpha06
FragmentStateAdapter: non-primary-item Fragments are capped at STARTED, and their menuVisibility is set to false.
Viewpager 1 in Androidx has been updated with BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT to match the behaviour of Viewpager2
See https://developer.android.com/reference/androidx/fragment/app/FragmentStatePagerAdapter#BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
The "Update all Fragments" is bit harder but I believe you can do the following (sorry my Kotlin is not up to scratch to give code example.
In the Adapter Constructor create and store a List of the Fragments (Not just itemText)
e.g. foreach item in items add to list of Fragments MyPageFragment.create(itemText)
Then in createFragment just return the Fragment from the correct position of the list.
e.g. return fragments[position]
Then in your adapter update you iterate over all items in your List of Fragements calling update on each Fragment.
(This is basically maintaining your own list of Fragments in the adapter instead of creating them on the fly when createFragment is called)
Update:
As noted in the comments this is less than ideal and breaks the Fragment lifecycle concepts and efficiency of Viewpager2, the update in onResume of Fragment is better.
Related
dears.
I'm using ViewPager2 & FragmentStateAdapter to add Fragments dynamically. And I stuck for 1 week already. Read all data here but no topics regarding my question.
I'll simplify real project just to illustrate my issue.
I have HostFragment which fills full screen in MainActivity.
HostFragment has ViewPager2 which fills full screen of HostFragment.
Tapping on the FAB new PageFragment should be created in ViewPager2 and immediately after it some View should be inflated on just created page.
How I'd realize it:
class HostFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_viewPager = binding.viewPager
----------------------------------
// For now viewModel.pages is empty - MutableList<PageFragment>()
----------------------------------
_viewPagerAdapter = ViewPagerHostFragmentAdapter(this, viewModel.pages)
viewPager.adapter = viewPagerAdapter
viewModel.pages.add(PageFragment())
viewPagerAdapter.notifyItemInserted(viewModel.pages.size - 1)
----------------------------------
// Retrieving the PageFragment on which some View will be inflated.
// For now there are 1 Fragment in viewModel.pages
----------------------------------
var page = viewModel.pages.last()
val inflater =
LayoutInflater.from(page.requireContext()).inflate(R.layout.some_view, null, false)
page.binding.addView(inflater)
}
Normally notifyItemInserted triggers fun createFragment in adapter. Adapter checks if fragment isn't already created and creates new instanse of fragment. So after notifyItemInserted immediately should be launched createFragment -> creation methods in PageFragment (onAttach, OnCreate so on...). And only after it the rest of code in HostFragment should be executed.
But this isn't happen. After viewPagerAdapter.notifyItemInserted(viewModel.pages.size - 1) the rest of code is executed:
var page = viewModel.pages.last()
val inflater =
LayoutInflater.from(page.requireContext()).inflate(R.layout.some_view, null, false)
page.binding.addView(inflater)
And when it comes to LayoutInflater.from(page.requireContext())... app crashes with
java.lang.IllegalStateException: Fragment PageFragment{c44e369} (a42d8521-35ae-4bb4-b445-c126e06fe026) not attached to a context.
It's clear. Because PageFragment isn't created yet, and we try to make some actions with it. In this case we try to inflate View on it.
I checked. If to remove
var page = viewModel.pages.last()
val inflater =
LayoutInflater.from(page.requireContext()).inflate(R.layout.some_view, null, false)
page.binding.addView(inflater)
then createFragment -> creation methods in PageFragment (onAttach, OnCreate so on...) is executed after notifyItemInserted
It's weird for me. It brakes foundomental rule of code order execution. Every line of code should be executed only after the previous line is done.
In this case app somehow skips notifyItemInserted and say: "Hey, friend. I'll execute it later". And goes to execute the rest of code. And only after it's done, app says: "Ok. Let's come back and execute all logic connected with notifyItemInserted". Means createFragment -> creation methods in PageFragment (onAttach, OnCreate so on...)
How can I force the app execute all methods sequentially? So no code is executed before notifyItemInserted -> createFragment -> onAttach, OnCreate so on is done?
How can I force the rest of code to wait till notifyItemInserted and all connected methods with it are done?
I need PageFragment to be created before app starts to inflate Views on it.
P.S.
I tried to debug notifyItemInserted and see what is does under the hood. But after few layers of repeats it comes to C++ code which compiler can't reach.
I am sure that notifyItemInserted triggers createFragment. Because if to remove rest of code with View inflation, the fragment is created perfectly. And if then to remove notifyItemInserted, fragment is added to viewModel.pages but is not inflated in the screen. Conclusion: notifyItemInserted triggers createFragment.
setCurrentItemView doesn't matter here. Even without it the notifyItemInserted sets created fragment on the screen. If the rest of code is absent of course.
Yes, I can execute inflations of Views inside onViewCreated of PageFragment. So we'll always be sure that Views are created only after the page in created. But I want to inflate View from HostFragment. Some another reasons for it exist, which are out of my question context.
I Use a Activity that holds a Fragment Inside That Fragment I have an other Fragment that holds ViewPagger2 now when apps open its should display first child of ViewPagger2 and also lode only that data but its not happening its loads two childs that make my app slow how to solve this issue.
It's the default behaviour of the viewPager2 to load adjacent pages for smoother animation. But you can override the default offscreen page limit by using viewpager2 setOffscreenPageLimit method.
In kotlin use
viewPager2.offscreenPageLimit = PAGE_COUNT
This will load that specified number of pages in advance. But, this PAGE_COUNT cannot be less than 1 which means, It'll still load your second fragment. But, It shouldn't slow down your app as you've mentioned.
If you are doing any network request then you can set registerOnPageChangeCallback for viewPager2 and override onPageSelected, then do the network request only when that fragment is selected.
As onPageSelected(position: Int) only gives us the position of the selected page, but not the fragment itself, so we have to retrieve the selected fragment using childFragmentManager and then trigger our network load request.
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
if (childFragmentManager.fragments.size > position) {
if(position == 1) { // for second fragment
val fragment = childFragmentManager.fragments[position] as SecondFragmentClassName
fragment.loadNetworkData()
}
}
}
})
For multiple fragments you can use when statement accordingly.
Put your data loading in the Fragment's onResume method as Viewpager2 only resumes the Fragment when it is displayed.
This maintains the Fragment's encapsulation but achieves the same goal as doing it in a OnPageChangeCallback.
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 want my fragment to load only when the tab is clicked. That is I am calling a webservice on each fragment, so I want that webservice to be called only when user clicks the specific tab; loads the fragment.
My Fragments are attached to the view pager.
I have override the following method in my fragments: setUserVisibleHint
override fun setUserVisibleHint(isFragmentVisible: Boolean) {
super.setUserVisibleHint(true)
if (this.isVisible) {
// we check that the fragment is becoming visible
if (isFragmentVisible && !isLoadOnce) {
callAPI(param)
isLoadOnce = true
}
}
}
the variable is set as: private var isLoadOnce = false in the fragment class.
I have 3 fragments in number the problem is when my activity popsup, the first fragment is visible and if I click the last tab that is the third tab to load the third fragment, nothing happens that is the web service won't call at all.
But when I click the second fragment and then the third fragment, and yes the webservice then only calls
So I want to call the web service whenver the user clicks each fragment (number 2 fragment or number 3 fragment)!
Can somebody please figure out what I am doing wrong?
I think there are at least 2 solutions to your problem:
Call "callApi(param)" on onResume() of the fragment;
Override onPageSelected(int) and call "callApi(param) in it.
Let me know if this worked for you!
viewpager generally loads the side views to provide smooth swipe experience.
you can restrict the viewpager to load the view using
viewpager.setOffScreenLimit(0)
I have 3 Fragments inside my ViewPager.
Inside my Fragments I added static ToggleButtons.(static, because I didn't find a workaround for a nullpointerexception, if I wanted to use findViewById() inside my Fragment classes in custom methods and not in onCreateView() )
Outside the ViewPager in my MainActivity I have a Reset-Button, that should reset all ToggleButtons (on all 3 Fragments) to "unchecked".
Every time i click on the Reset-Button, only the current Page and every neighbor-page gets updated.
E.G.
Current Page : 0 Updated Pages: 0,1
Current Page : 1 Updated Pages: 0,1,2
Current Page : 2 Updated Pages: 1,2
I think the Problem is the FragmentPagerAdapter. The Documentation says :
Implementation of PagerAdapter that represents each page as a Fragment
that is persistently kept in the fragment manager as long as the user
can return to the page.
When I'm on Page 2 i can not directly return to Page 0 , so I think the Views from Page 0 (= Fragment 0 ) are not in memory anymore?!
So how can I access the ToggleButtons inside a Fragment that is not visible at the moment nor is a neighbor of the current Fragment ? Is there any workaround?
EDIT:
I found out, that in deed all checked()-values of my ToggleButtons (on ALL Fragments) get updated, but not the inflated Views of Fragments, that are not visible and not neighbor Fragments. So when I return to those previous Fragments, the checked()-values of the ToggleButtons are reset to the state last time the fragment was visible. strange...
Example:
Page2 active - ToggleButton_Page2.checked() = true
switch to Page 0
press Reset-Button. Toast ToggleButton_Page2.checked() = false
switch back to Page 2 (or already at Page 1)
ToggleButton_Page2.checked() = true //it should be false
SOURCE:
Example Source Code
So how can I access the ToggleButtons inside a Fragment that is not
visible at the moment nor is a neighbor of the current Fragment ? Is
there any workaround?
You have a lot of problems in your code, you should read a bit more about Fragments and using them in a ViewPager.
In the reset and status buttons OnClickListeners listeners you write _adapter.getItem(x); in an attempt to get a handle to the Fragment representing the page at that position. This will not work as simply calling that method will return a new instance of the Fragment at that position and not the actual Fragment the ViewPager used(by calling getItem() at previous moment). That new instance you get after calling getItem() is not tied to the Activity and its onCreateView method wasn't called so it has no view(and you get the NullPointerException when you access it). You then tried to get around this by making the ToggelButton as a static field in the Fragment which will work as the field will be initialized when the ViewPager properly creates the Fragments at start. But you shouldn't do this, static fields that hold references to the Context(like any View does) are dangerous as you risk leaking that Context.
Also, related to what I said above, you don't need to pass a Context to a Fragment as a Fragment which is tied to an Activity has the getActivity() method which returns a reference to that Activity.
You shouldn't access any fragments from the ViewPager which aren't near to the visible fragment(one on the left/right of the visible position, unless you don't play with the setOffscreenPageLimit() method of the ViewPager). The ViewPager has a mechanism to improve performance(like a ListView does) so it only creates what it needs immediately so it can provide a smooth swipe for the user.
I've made some changes to your project, check them out. For further questions please post the relevant code in your question.
Salut,
Thank you for your good information, and the improved code!
"But you shouldn't do this, static fields that hold references to the
Context(like any View does) are dangerous as you risk leaking that
Context."
I'm new to Fragment implementation, and spent hours of finding out how the input String is correctly used for findFragmentByTag().(Now knowing how to use it I never thought that it would be that complex). So I decided to do that static workaround, which wasn't a good idea...
As I understand you, i CAN access more then +-1 Fragments, for example if I use setOffscreenPageLimit(3). I think this is the answer to my question.