Fragment lose data from ViewModel after recreating view - android

Imagine that you have 2 fragments connected to one (or more) viewModel(s) and inside of activity you'll switch between them. Once you open fragment, viewModel works as expected, so I start listening for changes from onCreate method, code example:
viewModel = new ViewModelProvider(requireActivity(), new InventoryTasksFactory()).get(InventoryTasksViewModel.class);
viewModel.inventoryTasksResponse().observe(this, new Observer<Response<List<InventoryTask>>>() {
#Override
public void onChanged(Response<List<InventoryTask>> listResponse) {
handleResponse(listResponse);
}
});
But when you switching to another fragment and going back, fragment becomes blank. I understand that fragment listening changes inside of viewModel, and you should manually getting value from viewModel and I get value from viewModel inside of onCreateView method, code example:
Response<List<InventoryTask>> inventory = viewModel.inventoryTasksResponse().getValue();
if (inventory!=null){
handleResponse(inventory);
}
Problem is that Response has 3 states: Running, Success, Error, and depends on those states view is updating. So, in first fragment opening, view updating twice and it leads to skipping frames and display blinking.
I was thinking about keeping data inside of fragment, but I want to avoid data duplicating. Besides of that, in case of sharedViewModel, you'll get issues about updating data inside of fragment!
Please, help me!

Observing your data from onViewCreated(View view, Bundle savedInstanceState) might work out.

Related

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.

EditText doesn't update

There are 2 fragments there. One of them contains an EditText (fragmentA).
If I update the EditText by calling EditText.setText() from another Fragment it doesn't display the newly set text although it has been set as I've set a breakpoint and saw it. It still shows the old text.
The following code is executed in the fragment without EditText if user clicks a button:
fragmentA.getActivity().runOnUiThread(new Runnable() {
#Override
public void run() {
editTextInFragmentA.setText(text, TextView.BufferType.EDITABLE);
}
}
// Close the fragment
getActivity().getSupportFragmentManager().popBackStack();
What's wrong with this approach? May it be caused by the fact that fragmentA is hidden by another fragment when EditText.setText is called?
I can realize from the popBackStack that your probably using replace when placing fragments into their containers. When a replace transaction is made, the old fragment gets it's state saved and all of the UI components (EditText, TextView, Button) get their state saved as well. Once the state is saved, the UI components gets destroyed such that trying to reference them after the old fragment has been removed from view will not work (usually causing NPEs if you try to set or change something in them but I'm not sure what's happening in your case). Upon going back to the old fragment using popBackStack, the UI is re-inflated within the fragment onCreateView, hence you'll have newly created UI component. In case there's an available saved state, it will be used to revert the UI back to when it was before it got replaced with another fragment. Since the text in the EditText got saved with the saved state, it was reverted back when you went back to the old fragment, hence setting it from the new fragment doesn't help. This technique is for efficiently using memory hence preventing the app from causing OutOfMemory exceptions.
To correct this, the old fragment should contain a callback which the new fragment could call to update the EditText value when UI is re-created. The callback would be passed from the old fragment to the new one, and then the new fragment would call this callback and pass it the new string. This new string should be stored first using a global variable inside the old fragment. When the old fragment is re-creating the UI in it's onCreateView, check if the global string variable is not null and not empty and hence update the EditText using setText().

Restoring a Fragment State when returned after clicking Backpressed of another fragment

Here is my problem area:
I have a Fragment A. Once it is attached, in its onCreateView, I load a webservice to fetch the data from the server and after that I set that data on the list view using a Base Adapter. Now on the Item Clicks of the list view I replace the Fragment A with Fragment B using replace Methods of the Fragment Transactions and addtoBackstack("FragmentA").
FragmentManager fm =getActivity().getFragmentManager();
fm.beginTransaction().replace(R.id.content_frame, Fragment B).commit();
Now here when I press back button on Fragment B, it takes me to Fragment A but the webservice again starts loading.
My Problem: I just want that when it returns to Fragment A, it should show its previous state and should not call the webservices again.
Thanks
OnCreateView for a fragment runs on the creation of the view every time it needs to be drawn. By going back you are causing the view to be recreated and hence the webservices are loading again.
I believe that if you only want the web services to load once then you could move the code to the "onCreate" method instead, but its probably a better idea to move this code to "onResume" instead and include some logic that checks whether you need to load your webservices again or not.
This way everytime the fragment is paused and then loaded again you could ensure that the fragment still has everything it needs.
(source: xamarin.com)
EDIT:
So for example you could have
#Override
public void onResume() {
super.onResume(); // Always call the superclass method first
if (data == null) { //Or list is empty?
getWebData()
}
}

How to pre-load a fragment before showing?

In my activity I have several fullscreen fragments, each of them downloads some data from web (using an async task) and shows them to the user. The fragments are showed one at a time.
To be more specific, each of the fragment readings some urls from a sqlite database, and fetch the content before showing them in a list, if that matters. The data loading tasks can be done in the OnCreate() function.
I would like to preload all the fragment (at least starting the downloading), when I show a splash screen. Pretty much like a viewpager preload its fragments.
I am wondering how to achieve this? I tried initialize/create all the fragments in the OnCreate() function of my activity, hoping the OnCreate() of fragments could be called earlier, but the OnCreate() and OnCreateView() function of the fragments are not called until a fragment is about to show to the user.
It sounds like you need to separate your model (the data which is downloaded) from your view (the fragments). One way to do this is to start the downloading AsyncTasks in your activity, rather than starting them in each fragment. Then when the fragments are eventually displayed they can show the data which has been downloaded (or a spinner or some other indication that the download process is still executing).
Fragment's onActivityCreated(Bundle) tells the fragment that its activity has completed its own Activity.onCreate().
So your solution to this problem is initialize or create or do your stuffs which you want to preload before fragments are created, inside your Fragment's onActivityCreated(Bundle)
see documents for fragment's lifecyle
The earliest pace you can start loading is either in a static singleton or in the Application Class
What I end up doing is the following, (1) add all the fragments into the container. So they (and their view) will be created and initialized. (2) hide those not in use and only show the one I would like the user to see. (3) use FragmentTrasaction.show()/FragmentTrasaction.hide() to manipulate the visibility instead of FragmentTrasaction.add() or FragmentTrasaction.replace().
If you following this approach, be warn that all the fragments will be cached in memory. But the benefit is the switch between fragment will be fast and efficient.
I was facing the same problem and then I used this method, suppose we are having an EditText in the fragment, then we can use codes like this
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
//this method allows you to input or instantiate fragments before showing this to an activity conidering id is "editTextEditProfileFirstName"
EditText firstName = (EditText) getActivity().findViewById(R.id.editTextEditProfileFirstName);
firstName.setText("This is my first name", TextView.BufferType.EDITABLE);
super.onViewCreated(view, savedInstanceState);
}

How does `onViewStateRestored` from Fragments work?

I am really confused with the internal state of a Fragment.
I have an Activity holding only one Fragment at once and replaces it, if another Fragment should get shown. From the docs onSaveInstanceState is called ONLY if the Activitys onSaveInstanceState is getting called (which isn't called in my case).
If I stop my Fragment, I'll store its state myself inside a Singleton (yeah, I know I hate Singletons, too, but wasn't my idea to do so).
So I have to recreate the whole ViewHirarchy, create new Views (by using the keyword new), restore its state and return them in onCreateView.
I also have a Checkbox inside this View from which I explicitly do NOT want to store its state.
However the FragmentManager wants to be "intelligent" and calls onViewStateRestored with a Bundle I never created myself, and "restores" the state of the old CheckBox and applies it to my NEW CheckBox. This throws up so many questions:
Can I control the bundle from onViewStateRestored?
How does the FragmentManager take the state of a (probably garbage-collected) CheckBox and applies it to the new one?
Why does it only save the state of the Checkbox (Not of TextViews??)
So to sum it up: How does onViewStateRestored work?
Note I'm using Fragmentv4, so no API > 17 required for onViewStateRestored
Well, sometimes fragments can get a little confusing, but after a while you will get used to them, and learn that they are your friends after all.
If on the onCreate() method of your fragment, you do: setRetainInstance(true); The visible state of your views will be kept, otherwise it won't.
Suppose a fragment called "f" of class F, its lifecycle would go like this:
- When instantiating/attaching/showing it, those are the f's methods that are called, in this order:
F.newInstance();
F();
F.onCreate();
F.onCreateView();
F.onViewStateRestored;
F.onResume();
At this point, your fragment will be visible on the screen.
Assume, that the device is rotated, therefore, the fragment information must be preserved, this is the flow of events triggered by the rotation:
F.onSaveInstanceState(); //save your info, before the fragment is destroyed, HERE YOU CAN CONTROL THE SAVED BUNDLE, CHECK EXAMPLE BELLOW.
F.onDestroyView(); //destroy any extra allocations your have made
//here starts f's restore process
F.onCreateView(); //f's view will be recreated
F.onViewStateRestored(); //load your info and restore the state of f's view
F.onResume(); //this method is called when your fragment is restoring its focus, sometimes you will need to insert some code here.
//store the information using the correct types, according to your variables.
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable("foo", this.foo);
outState.putBoolean("bar", true);
}
#Override
public void onViewStateRestored(Bundle inState) {
super.onViewStateRestored(inState);
if(inState!=null) {
if (inState.getBoolean("bar", false)) {
this.foo = (ArrayList<HashMap<String, Double>>) inState.getSerializable("foo");
}
}
}

Categories

Resources