I'm using navigation graph to navigate between fragments and I noticed that my viewModel is cleared (onCleared) only when I press the back button, but not when I navigating to another fragment using this code:
val action = MyFragmentDirections.actionMyFragmentToParentFragment()
val navController = findNavController()
navController.navigate(action)
In the logs I see that the fragment onDestroyView() is called but the viewModel's onCleared() is not called.
What am I'm missing?
The framework keeps the ViewModel alive as long as the scope of the activity or fragment is alive.
A ViewModel is not destroyed if its owner destroyed for configuration change,such as screen orientation (in this case also the onDestroy() method called.)
The new instance of owner reconnects to the existing ViewModel instance. But if we intentionally want to finish the activity,then ViewModel will clear.
onClear() method is called before the cleaning occurs.
ViewModel is also cleared on onBackPress(). Because in this case, we also finish the activity intentionally.
Decision:
ViewModel is cleared when -
onBackPressed called
finish() method called
Activity is shut down by the system
due to memory issues.
ViewModel objects are scoped to a Lifecycle. They remain in memory until that Lifecycle object goes away permanently. In the case of an Activity, that's when it is finished, and for a Fragment, when it's detached. See ViewModel Overview for more information.
Without seeing more of your code, it's difficult to answer why onCleared is called on back press and not when the Fragment is destroyed. However, if your ViewModel is tied to the Activity then hitting back might finish the activity and therefore trigger the call to onCleared. Share more of your code if you want a better chance at someone helping you dig deeper.
Related
I have a viewpager with 10+ pages in it, each page corresponds to its PageFragment and PageViewModel instances. When I start to swipe fragments one after another the onCleared() method is called for viewmodel that was left behind 2-3 steps ago, for example when I'm on page 7 the 4-th viewmodel is destroyed. The problem is, when I reach position 8-10 the onCleared() method starts to trigger also for this active viewmodel, which results to bad data representation on screen. Official documentation says that onCleared() is called whenever the viewmodel is not used anymore and should be destroyed, but how should it be destroyed if its data is represented on the active fragment
I tried to find any info about onCleared() method, but found almost nothing. Stack trace contains nothing suspicious. I presume that there is a force lifecycle methods call mechanism, but possible causes of its intervention in my case are unknown to me.
Please share your thoughts why is this behavior happening and how could I fix it.
Does that make sense to launch operations, like db writes, that needs to continue after the fragment lifecycle?
Code sample:
requireActivity().lifecycleScope.launch {
// suspend function invocation
}
MainScope().launch {
// suspend function invocation
}
The most important difference between MainScope and lifecycleScope is in the cancellation management of the launched coroutine.
I know your question is about requireActivity().lifecycleScope, but let me do it step by step.
With lifecycleScope, cancellation is done for you automatically - in the onDestroy() event in either Fragment or Activity, depending on whose lifecycle your hooked into.
With the mainScope, you’re on your own, and would have to add the scope.cancel() yourself, like below:
class MyActivity: Activity {
private val scope = MainScope()
// launch a coroutine on this scope in onViewCreated
override fun onDestroy() {
super.onDestroy()
//you have to add this manually
scope.cancel()
}
}
More info at:
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-main-scope.html
In other words, what you do (or supposed to do) manually with main scope, lifecycleScope does it for you automatically.
Now, at this point, you could (maybe) say, aha - it is the main Scope that I want, because my operation is not automatically cancelled, it’s exactly what I want, so i can just skip adding the cancellation call.
In a way, yes, you will get what you wish, as indeed it will keep running for some indeterminate perid of time until the Fragment and its variables are garbage collected, but you will no longer have access to that scope - the fragment is gone, and when you navigate into the fragment again, a new instance is going to be created, along with a new main scope. And of course you have no guarantee on how long it is going to be before the garbage collection kicks in. And what about exceptions?
Now on to requireActivity().lifecycleScope.
If you use requireActivity().lifeCycleScope, you obviously jump on the Activity lifecycle and get two advantages - a longer life cycle (presumably, there are other fragments in the activity, and say navigation back from the fragment in question just navigates to another fragment within the same activity, and not exits the app), and automatic cancellation in OnDestroy().
That may be enough. But a) your job handle, if you need one, will still stay in the fragment, and yet again, once you lose the fragment, you no longer have access to the job (assuming you need it), and b) your activity will not survive a configuration change (unless you expressly prohibit them). If you want to allow and handle configuration changes, go with the viewModelScope on the activity (and use a shared view model between activity and fragment).
The ViewModel class allows data to survive configuration changes such as screen rotations.
And, finally, if none of that is enough (your “Db save” operation is taking a really long time), use a Service (or WorkManager, etc). But at that point, the proper question to ask will be “why is it taking so long?” and focus on that.
What i want to do is very simple, i just want to test my IllegalState (during fragment commit) logic behind my activities.
I want to pause the activity, try to commit a fragment and then asserts that i'm handling this right.
But it seems to be not possible to actually pause and then resume activity during Espresso tests. Is there a way to do this without launching another activity?
Quintin is correct in his answer to point you to the ActivityScenario.moveToState(newState:) method but he is missing some details which I hope to fill here.
First of all, note that the ActivityScenario.launch(activityClass:) method not only launches the activity but waits for its lifecycle state transitions to be complete. So, unless you're calling Activity.finish() in your activity's lifecycle event methods, you can assume that it is in a RESUMED state by the time the ActivityScenario.launch(activityClass:) method returns.
Secondly, once your activity is launched and in a RESUMED state, then moving it back to the STARTED state will actually cause your activity's onPause() method to be called. Likewise, moving the activity back to the CREATED state, will cause both its onPause() and onStop() methods to be called.
Thirdly, once you've moved the activity back to the CREATED or STARTED state, you have to move it forward to the RESUMED state before you can perform view assertions and view actions on it, or otherwise your test method will throw a NoActivityResumedException.
All of the above is summarised in the following test method:
#Test
fun moving_activity_back_to_started_state_and_then_forward_to_resumed_state() {
val activityScenario = ActivityScenario.launch(MyActivity::class.java)
// the activity's onCreate, onStart and onResume methods have been called at this point
activityScenario.moveToState(Lifecycle.State.STARTED)
// the activity's onPause method has been called at this point
activityScenario.moveToState(Lifecycle.State.RESUMED)
// the activity's onResume method has been called at this point
}
To see this in action, refer to this sample application and this test class in particular.
You could try the ActivityScenario, but they do not include Pause as a State. You might get away with the recreate() method, but that is Pausing and Resuming.
//possibly:
// #get:Rule var activityScenarioRule = activityScenarioRule<MyActivity>()
val scenario = ActivityScenario.launch(MyActivity::class.java)
scenario.moveToState(Lifecycle.State.RESUMED)
//...
scenario.recreate()
//...
I would suggest having some interface / abstract handler that you can unit test outside of Android context.
I have done ample research on this, and there is not one clear solution on the problem.
In the life-cycle, particularly in the Fragment life-cycle, following Exception comes any moment after onPause().
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
My logic says, that to continue with the current fragment, after it reaches this state, I have to restart the activity and again point back to the intended fragment using Intent.
I want to be clear on what is happening and what should be real solution to deal with it.
I need to know the pros and cons of this mechanism; its importance in Fragment or Activity life-cycle.
Also, if I am changing the Windows Feature in onCreate to not to go to sleep, unless if the user has manually pressed the home button, will still the activity will go to this state?
This exception happens when you're trying to add/remove/replace/interact in any other way with a Fragment inside the Activity when it's paused.
Which means Activity will not be able to restore it's state (restore the state of a Fragment which has been changed) if it will be destroyed right away.
Best solution here, is to check that Activity is NOT paused during the interaction with a Fragment.
Another option is to use commitAllowingStateLoss() to interact with Fragment transaction, with a risk of losing it's state.
See:
https://developer.android.com/reference/android/app/FragmentTransaction.html#commitAllowingStateLoss()
In a perfect world you should analyze each crash carefully and add checks to verify that you interact with fragments only when Activity is up and running.
A better explanation is presented in a new Android developer reference and guide documents for using JetPack Life Cycle Listener.
https://developer.android.com/topic/libraries/architecture/lifecycle#kotlin
The library makes the components Activity Life Cycle aware. That means you do not require an abstract baseActivity class which overrides every life cycle callback, and record that state in a boolean variable. LifeCycle listener will do it for you.
All you have to do is stop introducing a new fragment or stop any Loader that updates the UI when its response returns. The right time to do this is before onStop or onSavedInstance state is called, and your components will be made aware of it.
It clearly states that after the onSavedInstancState or onStop is called the UI becomes immutable till the onStart of the Activity is called again. Sometimes you have to call restart the same activity using NEW TASK and CLEAR TASK flags using intent, when this state occurs and there is no chance that otherwise onStart is going to be called.
Happy Coding :-)
i have a fragment that is hosted inside of an activity. when user prsses the back button i need to save the model data and have it available the next time user opens the fragment/activity. But just while in the app, it does not need to be persisted to disk. So for example if user destroyed the process, then there is no need to keep the model data, it can be fetched from network again.
what i have tried:
icePick and onSavedInstance calls but these dont seem to kick in when user presses the back button on the fragment. tell me if im wrong.
here is what i have implemented in my fragment:
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable("myModel", Parcels.wrap(myModel));
}
i am using the parceler library if that makes any difference. I can also convert the code to kotlin if required. when i hit the back button the fragment gets popped off the stack and the activity contain it calls onDestroy but im not getting any call back in onSaveInstanceState. Also when i check in onCreate() savedInstanceState is null. I have not overrided onSavedInstance in the activity, just in the fragment. What am i doing wrong ?
i had a though to use a database to do this, but i just need it while in memory and there should be a way to do this without a DB.
from what i learned if user hits the back button onSaveInstance is not called by the system:
If an activity is in the foreground, and the user taps the Back button, the activity transitions through the onPause(), onStop(), and onDestroy() callbacks. In addition to being destroyed, the activity is also removed from the back stack.
It is important to note that, by default, the onSaveInstanceState()
callback does not fire in this case.
source: here
#onSaveInstanceState of fragment is strictly coupled to activity lifecycle
According to doc
Called to retrieve per-instance state from an activity before being
killed
You operates only with fragments and activity is left untouched,
so this method is definitely can't be used in your case and shouldn't.
My suggestion is to use some kind of persistent storage though interface. It could be in memory storage (any type of singleton, like suggested in comments. It may be scoped to app or activity or to custom case (you have to control manually cache lifecycle) and injected with dagger, for example), shared-preferences based storage, database storage. It is easy to test if you follow dependency injection patterns & use structural pattern like MVP (but it is not a point of this question)
So store the data in the repository on change or in the onPause method (because it is the last guaranteed to call when screen is being gone). And restore it in onCreate