FragmentStateAdapter crashes, throwing java.lang.IllegalStateException - android

In my app, I have a PlaylistMenuFragment with a button that replaces itself in the ViewPager with PlaylistContentFragment, and vice-versa, through the following methods:
fun goToPlaylistContent() {
pagerAdapter.replaceLastFragmentWith(PlaylistContentFragment())
}
fun goToPlaylistMenu() {
pagerAdapter.replaceLastFragmentWith(PlaylistMenuFragment())
}
The adapter method that is being called:
fun replaceLastFragmentWith(newFragment: Fragment) {
fragmentList[LAST_FRAGMENT_INDEX] = newFragment
notifyItemChanged(LAST_FRAGMENT_INDEX)
}
If I click a button inside PlaylistMenuFragment that goes to PlaylistContentFragment, executing the methods above, everything works fine. But if then I click a button inside PlaylistContentFragment that goes back to PlaylistMenuFragment, the app crashes, throwing the following exception:
java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling androidx.viewpager2.widget.ViewPager2$RecyclerViewImpl{8ebfd7a VFED..... ......ID 0,0-720,1040 #1}, adapter:com.pegoraro.musicast.main.PagerAdapter#a12492b, layout:androidx.viewpager2.widget.ViewPager2$LinearLayoutManagerImpl#e567188, context:com.pegoraro.musicast.main.MainActivity#e8e6424
The crash traces back to the methods above. If I wrap the notifyItemChanged inside the adapter method with a try-catch block, the app works as intended, with no sign of problem, unless I look in the logcat, which still shows the exception being thrown. It is a dirty fix, which I don't like:
fun replaceLastFragmentWith(newFragment: Fragment) {
fragmentList[LAST_FRAGMENT_INDEX] = newFragment
try {
notifyItemChanged(LAST_FRAGMENT_INDEX)
} catch (e: Exception) {
Log.d("TEST", e.toString())
}
}
I'm trying to implement a sort of navigation between these two Fragments inside a TabLayout, and this is the way I managed to do. So if the error comes from a fundamental problem of how I am approaching this issue, what is an alternative? And if not, what could be the cause of the problem? Thank you.

Calling any of notifyXX() methods on the ViewPager2 adapter is the same as that of the RecyclerView, because ViewPager2 internally functions based on RecyclerView.
And since notifyXX() methods work in background thread, and in your case this directly affects one of the ViewPager current fragments; so you need to explicitly do this in UI thread
viewpager.post {
notifyItemChanged(LAST_FRAGMENT_INDEX)
}

Related

PagingDataAdapter.refresh() not working after fragment navigation

I am using PagingDataAdapter in one fragment to show user activity.
in fragment class level,
private var activityAdapter: ActivityFeedAdapter? = null
in onCreate() I am initializing before use as,
activityAdapter = initAdapter()
also in onCreate(),
this.lifecycleScope.launchWhenResumed {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.getActivityFeed().observe(viewLifecycleOwner) {
it?.let {
activityAdapter?.submitData(lifecycle, it)
}
}
}
}
and after onStart(), I am setting a click Listener on a view to refresh pagingdata from the UI as,
binding?.refresh?.setOnClickListener {
activityAdapter?.refresh()
}
Everything works fine when I use it for the first load. But after I navigate to some fragment and get back to the same screen, clicking on refresh only handles click event but does not refresh the adapter.
BTW, I have initialized the adapter in onCreate() because I need the adapter to maintain loaded data across screen transitions. Anyone help me...
I got the bug... :))
In onCreate() I was setting observer with lifecycleOwner as viewLifecycleOwner.
But viewLifecycleOwner is only active from onCreateView() till onDestroyView(). So after navigation to a different fragment and getting back from there, the new observer was not set. The old observer is canceled due to lifecycleOwner is destroyed. So I could refresh more data in PagingDataAdapter.
When setting the observer please rethink which lifecycleOwner is to be used. Hope this might help someone. :)

Android repeatOnLifecycle in fragment inside ViewPager2

If I add the following code snippet to a "normal" fragment it gets started and cancelled as expected when navigating to and from the fragment, but if I add this to fragment inside a view pager 2 it is not cancelled even though the fragmens onPause method is invoked. Is this by design or am I missing something?
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
try {
while (isActive) {
println("Fragment alive....")
delay(1000)
}
} catch (ex: CancellationException) {
println("Cancelled fragment...")
throw ex
}
}
}
I am no sure if I got your question right, but I can already tell that you're relaying on the wrong lifecycle event if you wish that your code gets executed when the Fragment is visible, for that you need to use repeatOnLifecycle(Lifecycle.State.RESUMED). Using this your code will start executing as soon as the Fragment is visible and gets cancelled when it gets paused.
Using repeatOnLifecycle(Lifecycle.State.STARTED) your code will start executing when the Fragment is started (ready to get displayed) and gets cancelled when it gets stopped.

Error Fragment not attached to an activity when I leave a fragment and enter it again

When I change the fragment and return to the one that was at the beginning, running a line I get the error Fragment not attached to an activity. It is strange because before I get to this line, I use requireAcitivity() and it works fine. The error appears after calling an ArrayAdapter custom
When I call the arrayadapter, the requiredActivity method still works:
val adapter = ListUserGameInfoAdapter(requireContext(), gameViewModel!!)
gameInfoListUsers.adapter = adapter
Each row of the list created by the arrayAdapter has a button.
icon.setOnClickListener {
gameViewModel.userToDeleteFromGamePositionLiveData.value = position
}
The button changes the value of a LiveData of the viewModel. By using an observer I pick up this new value of the LiveData. Inside the code of the observer, if I call requiredActivity(), the following error appears
val userToDeleteObserver = Observer<Int> {
if (it != null) {
//If I call requiredActivity here, I get the error Fragment not attached to an activity
showDialog(it)
}
}
gameViewModel!!.userToDeleteFromGamePositionLiveData.observe(
requireActivity(),
userToDeleteObserver
)
The strange thing is that the first time I enter this fragment the error does not appear, only when I go to another one and come back to this one.
You're adding an observer in your fragment, but tying it to the lifecycle of your activity. Therefore it will keep observing for changes even when the fragment has been destroyed, which is why you're getting a crash when calling requireActivity() in your observer.
If you do some debugging, you'll probably notice that the observer is actually triggering twice, once for the old fragment (no longer attached to an activity) and once for the new fragment.
You should be using Fragment.getViewLifecycleOwner() instead.

Expected the adapter to be 'fresh' while restoring state

I have a viewpager2 with multiple fragments in FragmentStateAdapter. Whenever I try to open a new fragment and then go back to my current one with viewpager2, I get an exception:
Expected the adapter to be 'fresh' while restoring state.
It seems FragmentStateAdapter is unable to properly restore its state as it is expecting it to be empty.
What could I do to fix this ?
it can be fixed by viewPager2.isSaveEnabled = false
So my problem was that I was creating my FragmentStateAdapter inside my Fragment class field where it was only created once. So when my onCreateView got called a second time I got this issue. If I recreate adapter on every onCreateView call, it seems to work.
I encountered the same problem with ViewPager2. After a lot of efforts on testing different methods this worked for me:
public void onExitOfYourFragment() {
viewPager2.setAdapter(null);
}
When you come back to the fragment again:
public void onResumeOfYourFragment() {
viewPager2.setAdapter(yourAdapter);
}
This adapter/view is useful as a replacement for FragmentStatePagerAdapter.
If what you seek is to preserve the Fragments on re-entrance from the Backstack that would be extremely difficult to achieve with this adapter.
The team placed to many breaks in place to prevent this, only god knows why...
They could have used a self detaching lifeCycle observer, which ability was already foresaw in its code, but nowhere in the android architecture makes use of that ability....
They should have used this unfinished component to listen to the global Fragments lifecycle instead of its viewLifeCycle, from here on, one can scale the listening from the Fragment to the viewLifeCycle. (attach/detach viewLifeCycle observer ON_START/ON_STOP)
Second... even if this is done, the fact that the viewPager itself is built on top of a recyclerView makes it extremely difficult to handle what you would expect from a Fragment's behavior, which is an state of preservation, a one time instantiation, and a well defined lifecycle (controllable/expected destruction).
This adapter is contradictory in its functionality, it checks if the viewPager has already been fed with Fragments, while still requiring a "fresh" adapter on reentrance.
It preserves Fragments on exit to the backStack, while expecting to recreate all of them on reentrance.
The breaks on place to prevent a field instantiated adapter, assuming all other variables are already accounted for a proper viewLifeCycle handling (registering/unregistering & setting and resetting of parameters) are:
#Override
public final void restoreState(#NonNull Parcelable savedState) {
if (!mSavedStates.isEmpty() || !mFragments.isEmpty()) {
throw new IllegalStateException(
"Expected the adapter to be 'fresh' while restoring state.");
}
.....
}
Second break:
#CallSuper
#Override
public void onAttachedToRecyclerView(#NonNull RecyclerView recyclerView) {
checkArgument(mFragmentMaxLifecycleEnforcer == null);
mFragmentMaxLifecycleEnforcer = new FragmentMaxLifecycleEnforcer();
mFragmentMaxLifecycleEnforcer.register(recyclerView);
}
where mFragmentMaxLifecycleEnforcer must be == null on reentrance or it throws an exception in the checkArgument().
Third:
A Fragment garbage collector put in place upon reentrance (to the view, from the backstack) that is postDelayed at 10 seconds that attempts to Destroy off screen Fragments, causing memory leaks on all offscreen pages because it kills their respective FragmentManagers that controls their LifeCycle.
private void scheduleGracePeriodEnd() {
final Handler handler = new Handler(Looper.getMainLooper());
final Runnable runnable = new Runnable() {
#Override
public void run() {
mIsInGracePeriod = false;
gcFragments(); // good opportunity to GC
}
};
mLifecycle.addObserver(new LifecycleEventObserver() {
#Override
public void onStateChanged(#NonNull LifecycleOwner source,
#NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
handler.removeCallbacks(runnable);
source.getLifecycle().removeObserver(this);
}
}
});
handler.postDelayed(runnable, GRACE_WINDOW_TIME_MS);
}
And all of them because of its main culprit: the constructor:
public FragmentStateAdapter(#NonNull FragmentManager fragmentManager,
#NonNull Lifecycle lifecycle) {
mFragmentManager = fragmentManager;
mLifecycle = lifecycle;
super.setHasStableIds(true);
}
I've faced the same issue.
After some researching I've came to that it was related to instance of Adapter. When it is created as a lazy property of Fragment it crashes with that error.
So creating Adapter in Fragment::onViewCreated resolves it.
I was also getting this java.lang.IllegalStateException: Expected the adapter to be 'fresh' while restoring state. when using ViewPager2 within a Fragment.
It seems the problem was because I was executing mViewPager2.setAdapter(mFragmentStateAdapter); in my onCreateView() method.
I fixed it by moving mViewPager2.setAdapter(mMyFragmentStateAdapter); to my onResume() method.
I solved this problem by testing if it is equal null
if(recyclerView.adapter == null) {recyclerView.adapter = myAdapter}
I've been struggling with this and none of the previous answers helped.
This may not work for every possible situation, but in my case fragments containing ViewPager2 were fixed and few, and I solved this by doing fragment switch with FragmentTransaction's show() and hide() methods, instead of replace() commonly recommended for this. Apply show() to the active fragment, and hide() to all others. This avoids operations like re-creating views, and restoring state that trigger the problem.
I got this problem after moving new SDK version. (Expected the adapter to be 'fresh' while restoring state)
android:saveEnabled="false" at ViewPager2 can be a quick fix but it may not be what you want.
<androidx.viewpager2.widget.ViewPager2
android:saveEnabled="false"
Because this simply means your viewPager2 will always come on the first tab when your activity is recreated due to the same reason you are getting this error ( config change and activity recreate).
I wanted users to stay wherever they were So I did not choose this solution.
So I looked in my code a bit. In my case, I found a residual code from early days when I was just learning to create android app.
There was a useless call to onRestoreInstanceState() in MainActivity.onCreate, I just removed that call and it fixed my problem.
In most cases, you should not need to override these methods.
If you want to override these , do not forget to call super.onSaveInstanceState / super.onRestoreInstanceState
Important Note from documentation
The default implementation takes care of most of the UI per-instance
state for you by calling View.onSaveInstanceState() on each view in
the hierarchy that has an id, and by saving the id of the currently
focused view (all of which is restored by the default implementation
of onRestoreInstanceState(Bundle)). If you override this method to
save additional information not captured by each individual view, you
will likely want to call through to the default implementation,
otherwise be prepared to save all of the state of each view yourself.
Check if the information you want to preview is part of a view that may have an ID. Only those with an ID will be preserved automatically.
If you want to Save the attribute of the state which is not being saved already. Then you override these methods and add your bit.
protected void onSaveInstanceState (Bundle outState)
protected void onRestoreInstanceState (Bundle savedInstanceState)
In latest SDK versions Bundle parameter is not null, so onRestoreInstanceState is called only when a savedStateIsAvailable.
However, OnCreate as well gets savedState Parameter. But it can be null first time, so you need to differentiate between first call and calls later on.
Change your fragmentStateAdapter code from
MyPagerAdapter(childFragmentManager: FragmentManager,
var fragments: MutableList<Fragment>,
lifecycle: Lifecycle
) : FragmentStateAdapter(childFragmentManager,lifecycle)
to
MyPagerAdapter(fragment: Fragment,
var fragments: MutableList<Fragment>
) : FragmentStateAdapter(fragment)
Note: Here we are removing lifecycle and fragmentManager dependency and fragment state gets restored on back press.

Androidx Navigation IllegalStateException after onSaveInstanceState

I have an app using AndroidX's Navigation library, but I'm getting odd behavior. Particularly around my app going in/out of the background. Here are two examples:
In a simple on click listener in a Fragment I have:
(Kotlin)
button.setOnClickListener {
findNavController().popBackStack()
}
From this, I see crashes saying it threw an IllegalStateException since it ran after onSaveInstanceState.
I have a ViewModel associated with my Fragment and I register my observers to the fragment view's lifecycle. This means that I get notified during onStart. Some key events, such as login state determine the app's navigation. In my case I have a splash screen that could go to either a login screen or the main screen. Once a user completes login, I reset the navigation (taking me back to the splash screen). Now the auth state is ready and I want to navigate to the main fragment, this throws an error often because onResume must be called before the FragmentManager is considered ready. I get an error saying I'm in the middle of a transaction and I can't add a new one. To mediate this I had to write this strange bit of code:
(Kotlin)
private fun safeNavigateToMain() {
if (fragmentManager == null) {
return
}
if (!isResumed) {
view?.post { safeNavigateToMain() }
return
}
try {
findNavController().navigate(R.id.main)
} catch (tr: Throwable) {
view?.post { safeNavigateToMain() }
}
}
Does anyone know how I can get the navigation controller to play nice with the fragment lifecycles without having to add these workarounds?
As per the Navigation 1.0.0-alpha03 release notes:
FragmentNavigator now ignores navigation operations after FragmentManager has saved state, avoiding “Can not perform this action after onSaveInstanceState” exceptions b/110987825
So upgrading to alpha03 should remove this error.

Categories

Resources