I have Fragments which are managed inside a ViewPager and works pretty well. It uses FragmentStatePagerAdapter to manage the fragments and all of them are from the same type.
This ViewPager is working inside of a parent fragment.
However, when the user clicks the back button to leave the parent fragment, the fragments that inside the ViewPager won't go into Pause->Destroy->onDetach etc'
They seems to stay in their onResume state even when they no longer visible, although the parent fragment, which created the PagerAdapter has been destroyed and detached properly.
Imagine that when the user seems to be in another screen already, then when he clicks the home button I can see that the Fragments that was in ViewPager (which are no longer visible) go into their Pause state, and when the user navigate back to the app, I can see that the Fragments that was in the ViewPager resumed again while they not visible.
Is there any explanation for such behaviour?
Here portion of my code which initialise the PagerAdapter in the parent fragment:
private fun configureScreen() {
tabs_view_pager.offscreenPageLimit = TabsPagerAdapter.NUM_PAGES - 1
fragmentManager?.let {
tabsPagerAdapter = TabsPagerAdapter(it)
tabs_view_pager.adapter = tabsPagerAdapter
}
}
And here a portion of the PagerAdapter code:
class TabsPagerAdapter(fm : FragmentManager) : FragmentStatePagerAdapter(fm) {
companion object {
const val NUM_PAGES = 3
private var pagesTitle = arrayOf("" , "", "")
}
override fun getItem(item: Int): Fragment {
return ChildFragment.newInstance()
}
override fun getCount(): Int {
return NUM_PAGES
}
fun updateChildFragments(someData: FragmentData?) {
//process new data
notifyDataSetChanged()
}
}
override fun getItemPosition(fragment: Any): Int {
(fragment as ChildFragment).refresh()
return PagerAdapter.POSITION_UNCHANGED
}
}
When using a viewPager with fragments inside another fragment you should call getChildFragmentManager() instead of getFragmentManager().
Related
I have couple of fragments (lets say A,B and C) replacing each other. In fragment A, there is a recyclerView which should be updated as an observer receives changes in a liveData. But the observer does not receive updates when I move to fragment B because fragment A is already destroyed.
I have changed the the lifeCycle owner from viewLifeCycleOwner to this (fragment) but still the same result.
Here is some snippet of my code:
class MainActivity: AppCompatActivity() {
val fragments = listOf(fragmentA(), fragmentB(), fragmentC())
private fun moveToFragment(index: Int) {
if (fragments.isNotEmpty()) {
viewModel.selectedPage = index
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, fragments[index])
transaction.commit()
}
}
}
class FragmentA(): Fragment{
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
fragmentAViewModel.listOfThings.observe(viewLifecycleOwner) { things ->
things?.let {
when (things.status) {
Status.SUCCESS -> {
thingsAdapter.submitList(things.data)
}
else -> {
thingsAdapter.submitList(emptyList())
}
}
}
}
}
I know that "replace" destroys the fragments but I also tried using HIDE/SHOW fragments but this pattern overlays fragments on top of each other and I wasn't able to get it fixed.
My question is what the best practice is here to make sure that the observer/recyclerView is already updated before going to the fragment. Otherwise the recyclerView will have old values and they change in few milliseconds after I go the fragmentA. The most obvious situation is when the recyclerView is supposed to be empty but when I go to fragmentA items are still there for a moment before they disappear.
I have a viewpager2 with FragmentStateAdapter() adapter inside it. I also have a tab layout with 4 tabs. I use a single fragment for all tab. that is named AllOrdersTab.
in my architecture I just send different value to load different API data to AllOrdersTab fragment.
when each tab layout is selected , a fragment created and works fine for the first time for all 4 fragments. after that if I swipe back to previous tab it is not created or refreshed again.
I want to recreate the fragment or a way to call API again when swiping between tabs. I also read this page.
FragmentStateAdapter not recreating currentFragment after notifyDataSetChanged
I tried to do this. so I decided to create 4 instance of Allorderstab() fragment. but never work for me because I guess hash codes of fragments are same.
ViewPager2 Adapter:
class ViewPagerOrdersAdapter(fm: FragmentManager,val listFragments:MutableList<Fragment>, viewlifecycler: Lifecycle) : FragmentStateAdapter(fm, viewlifecycler)
{
override fun getItemCount(): Int
{
return listFragments.size
}
override fun createFragment(position: Int): Fragment {
val args = Bundle()
when (position) {
1 -> {
args.putString("KEY_ID", "inProgress")
listFragments[position].arguments = args
return listFragments[position]
}
2 -> {
args.putString("KEY_ID", "cancel")
listFragments[position].arguments = args
return listFragments[position]
}
3 ->
{
args.putString("KEY_ID","deliver")
listFragments[position].arguments=args
return listFragments[position]
}
else -> {
args.putString("KEY_ID", "all")
listFragments[position].arguments = args
return listFragments[position]
}
}
}
override fun getItemId(position: Int): Long {
return listFragments[position].hashCode().toLong()
}
override fun containsItem(itemId: Long): Boolean {
return listFragments.find {it.id.hashCode().toLong() == itemId } != null
}
}
Here I created 4 instance of Allorderstab() Fragment.
Set ViewPager2 Adapter
MainFragment:
val fragments:MutableList<Fragment> = mutableListOf(Allorderstab(), Allorderstab(), Allorderstab(), Allorderstab())
vp.setAdapter(ViewPagerOrdersAdapter(this.childFragmentManager,fragments, lifecycle))
AllOrderstab Fragment:
override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?, savedInstanceState: Bundle?): View? {
val bundle = arguments
bundle?.let {
val myStatus = bundle.getString("KEY_ID")
myStatus?.let{
//getting history of each tab orders - calling API
myviewModel.getOrdersHistory(tempTn,myStatus)
}
}
}
All in all I don't know how to refresh while swiping between tabs . if I have a single fragment for all 4 tabs.
For those who wasted a day for this problem like me , wanting to call API each time for refreshing data, Move your code to onResume function. fortunately it is run each time your fragment visible.
AllOrderstab Fragment:
override fun onResume() {
val bundle = arguments
bundle?.let {
val myStatus = bundle.getString("KEY_ID")
myStatus?.let{
//getting history of all orders
myviewModel.getOrdersHistory(tempTn,myStatus)
}
}
super.onResume()
}
You have 4 different instances of the same tab. If you need to refresh the tabs everytime the user navigates to your tab you need to add a PageChangeListener to your tabs view. Then whenever you change the page you need to notify the fragment that it has come to foreground you can do so by calling a method on Allorderstab class and then refreshing the data from this method.
I have one MainActivity with -> FragmentA which contains -> ViewPager with (Fragment1,Fragment2,Fragment3)
Now in FragmentA I have one spinner and any selection must reflect the changes inside viewpager's currently visible fragment.
How can I achieve that? I don't want to follow ViewModel or EventBus approach for now as I am working on very old project. I want to use interface to communicate between them.
Create an interface inside your FragmentA
interface OnSpinnerValue{
fun onSpinnerValueChanged()
}
Create a WeakReference for the current selected fragment
private var _currentPage: WeakReference<OnSpinnerValue>? = null
private val currentPage
get() = _currentPage?.get()
fun setCurrentPage(page: OnSpinnerValue) {
_currentPage = WeakReference(page)
}
Now implement this interface in every child fragment of ViewPager
class Fragment1() : Fragment(), OnAddEvent {
override fun onSpinnerValueChanged() {
// implement your method
}
}
And, update currentPage value of the FragmentA, according to the selected fragment, and update it in onResume() of each child fragment
override fun onResume() {
super.onResume()
(parentFragment as FragmentA).setCurrentPage(this)
}
Now, trigger onSpinnerValueChanged from your spinner's onItemSelected methods
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
currentPage?.onSpinnerValueChanged()
}
It is strange if you don't want to use ViewModel ,
I know may be it is not best solutions but you can :
you can create function itIsYourFunctionToUpdate() inside your fragment and update him on FragmentA() , calling function with her object like fragmentB.itIsYourFunctionToUpdate()
or
if (ViewPager.getCurrentItem() == 0 && page != null) {
((FragmentClass1)page).itIsYourFunctionToUpdate("new item");
}
Also you can update viewPager like this mAdapter.notifyDataSetChanged() and all fragment inside viewPager must be updated
Just curious about the Performance difference between these 2 approaches:
Approach 1: Directly return new Fragment object for each position
class ProductAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
override fun getItemCount() = 5
override fun createFragment(position: Int) = ProductPageFragment() //here
}
Approach 2: Prepare Fragments in advance and return by position
class ProductAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
private val fragments = mutableListOf<ProductPageFragment>()
init {
repeat(5) { fragments.add(ProductPageFragment()) }
}
override fun getItemCount() = 5
override fun createFragment(position: Int) = fragments[position] //here
}
The performance difference will depend on the Fragment and what it does where, but there usually little difference because
When a fragment is instantiated, it begins in the INITIALIZED state. For a fragment to transition through the rest of its lifecycle, it must be added to a FragmentManager. The FragmentManager is responsible for determining what state its fragment should be in and then moving them into that state.
From https://developer.android.com/guide/fragments/lifecycle
Most fragments design do most of their work at higher lifecycle states e.g. at CREATED, therefore there will be little difference in performance as the lifecycle management by viewpager2 will be the same for each approach.
I have a Pager Adapter to load server images as like below:
class ImagePagerAdapter(var imageList: List<ProductImageModel>, var fragmentManager: FragmentManager) :
FragmentPagerAdapter(fragmentManager) {
override fun getItem(position: Int): Fragment {
val fragment = ImagePagerFragment()
fragment.arguments = Bundle().apply {
putString(PAGER_IMAGE_URL, imageList[position].image)
}
return fragment
}
override fun getCount(): Int {
return imageList.size
}
}
I want to know any option to addtoBackStack(null) while creating a new Fragment inside the pagerAdapter.
The current code, automatically adding the fragments to the backstack. So it's making a huge issue while the user presses the back button. I have added another logic to filter the fragments in the back press.