When the FragmentPagerAdapter
adds a fragment to the FragmentManager, it uses a special tag based on the particular position that the fragment will be placed. FragmentPagerAdapter.getItem(int position)
is only called when a fragment for that position does not exist. After rotating, Android will notice that it already created/saved a fragment for this particular position and so it simply tries to reconnect with it with FragmentManager.findFragmentByTag()
, instead of creating a new one. All of this comes free when using the FragmentPagerAdapter
and is why it is usual to have your fragment initialisation code inside the getItem(int)
method.
Even if we were not using a FragmentPagerAdapter
, it is not a good idea to create a new fragment every single time in Activity.onCreate(Bundle)
. As you have noticed, when a fragment is added to the FragmentManager, it will be recreated for you after rotating and there is no need to add it again. Doing so is a common cause of errors when working with fragments.
A usual approach when working with fragments is this:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
CustomFragment fragment;
if (savedInstanceState != null) {
fragment = (CustomFragment) getSupportFragmentManager().findFragmentByTag("customtag");
} else {
fragment = new CustomFragment();
getSupportFragmentManager().beginTransaction().add(R.id.container, fragment, "customtag").commit();
}
...
}
When using a FragmentPagerAdapter
, we relinquish fragment management to the adapter, and do not have to perform the above steps. By default, it will only preload one Fragment in front and behind the current position (although it does not destroy them unless you are using FragmentStatePagerAdapter
). This is controlled by ViewPager.setOffscreenPageLimit(int). Because of this, directly calling methods on the fragments outside of the adapter is not guaranteed to be valid, because they may not even be alive.
To cut a long story short, your solution to use putFragment
to be able to get a reference afterwards is not so crazy, and not so unlike the normal way to use fragments anyway (above). It is difficult to obtain a reference otherwise because the fragment is added by the adapter, and not you personally. Just make sure that the offscreenPageLimit
is high enough to load your desired fragments at all times, since you rely on it being present. This bypasses lazy loading capabilities of the ViewPager, but seems to be what you desire for your application.
Another approach is to override FragmentPageAdapter.instantiateItem(View, int)
and save a reference to the fragment returned from the super call before returning it (it has the logic to find the fragment, if already present).
For a fuller picture, have a look at some of the source of FragmentPagerAdapter (short) and ViewPager (long).
Android R Update:
From Android R, this method always returns false. Google says that this is done "to protect goat privacy":
/**
* Used to determine whether the user making this call is subject to
* teleportations.
*
* <p>As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this method can
* now automatically identify goats using advanced goat recognition technology.</p>
*
* <p>As of {@link android.os.Build.VERSION_CODES#R}, this method always returns
* {@code false} in order to protect goat privacy.</p>
*
* @return Returns whether the user making this call is a goat.
*/
public boolean isUserAGoat() {
if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.R) {
return false;
}
return mContext.getPackageManager()
.isPackageAvailable("com.coffeestainstudios.goatsimulator");
}
Previous answer:
From their source, the method used to return false
until it was changed in API 21.
/**
* Used to determine whether the user making this call is subject to
* teleportations.
* @return whether the user making this call is a goat
*/
public boolean isUserAGoat() {
return false;
}
It looks like the method has no real use for us as developers. Someone has previously stated that it might be an Easter egg.
In API 21 the implementation was changed to check if there is an installed app with the package com.coffeestainstudios.goatsimulator
/**
* Used to determine whether the user making this call is subject to
* teleportations.
*
* <p>As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this method can
* now automatically identify goats using advanced goat recognition technology.</p>
*
* @return Returns true if the user making this call is a goat.
*/
public boolean isUserAGoat() {
return mContext.getPackageManager()
.isPackageAvailable("com.coffeestainstudios.goatsimulator");
}
Here is the source and the change.
Best Answer
androidx.lifecycle.ViewModel's are not meant to be used on RecyclerView items by default
Why?
ViewModel
is AAC (Android Architecture Component) whose sole purpose is to survive configuration changes of Android Activity/Fragment lifecycle, so that data can be persisted via ViewModel for such case.This achieved by caching VM instance in storage tied to hosting activity.
That's why it shouldn't be used on RecyclerView (ViewHolder) Items directly as the Item View itself would be part of Activity/Fragment and it (RecyclerView/ViewHolder) doesn't contain any specific API to provide
ViewModelStoreOwner
(From which ViewModels are basically derived for given Activity/Fragment instance).Simplistic syntax to get ViewModel is:
& here
this
would be referred to Activity/Fragment context.So even if you end up using
ViewModel
inRecyclerView
Items, It would give you same instance due to context might be of Activity/Fragment is the same across the RecyclerView which doesn't make sense to me. So ViewModel is useless for RecyclerView or It doesn't contribute to this case much.TL;DR
Solution?
You can directly pass in
LiveData
object that you need to observe from your Activity/Fragment'sViewModel
in yourRecyclerView.Adapter
class. You'll need to provideLifecycleOwner
as well for you adapter to start observing that given live data.So your Adapter class would look something like below:
If this is not the case & you want to have it on
ViewHolder
class then you can pass yourLiveData
object duringonCreateViewHolder
method to your ViewHolder instance along withlifecycleOwner
.Bonus point!
If you're using data-binding on RecyclerView items then you can easily obtain
lifecyclerOwner
object from your binding class. All you need to do is set it duringonCreateViewHolder()
something like below:So yes, you can use wrapper class for your
ViewHolder
instances to provide youLiveData
out of the box but I would discourage it if wrapper class is extendingViewModel
class.As soon as concern about mimicking
onCleared()
method ofViewModel
, you can make a method on your wrapper class that gets called whenViewHolder
gets recycled or detaches from window via methodonViewRecycled()
oronViewDetachedFromWindow()
whatever fits best in your case.Edit for comment of @Mariusz: Concern about using Activity/Fragment as LifecycleOwner is correct. But there would be slightly misunderstanding reading this as POC.
As soon as one is using
lifecycleOwner
to observeLiveData
in givenRecyclerViewHolder
item, it is okay to do so becauseLiveData
is lifecycle aware component and it handles subscription to lifecycle internally thus safe to use. Even if you can explicitly remove observation if wanted to, usingonViewRecycled()
oronViewDetachedFromWindow()
method.About async operation inside
ViewHolder
:If you're using coroutines then you can use
lifecycleScope
fromlifecycleOwner
to call your operation and then provide data back to particular observingLiveData
without explicitly handling clear out case (LifecycleScope
would take care of it for you).If not using Coroutines then you can still make your asyc call and provide data back to observing
LiveData
& not to worry about clearing your async operation duringonViewRecycled()
oronViewDetachedFromWindow()
callbacks. Important thing here isLiveData
which respects lifecycle of givenLifecycleOwner
, not the ongoing async operation.