Android kotlin coroutines, viewModelScope behavior - android

I am having this interesting problem. I need to do some work immediately after insertion, but viewModelScope randomly, or at least it looks like randomly, skips functions except for the first one.
Example:
fun insertItem(item: SingleItem) = viewModelScope.launch {
itemsRepository.insertItem(item)
increaseAmount(item.catId)
}
So in this example everything runs ok only after fresh app install, but then on the next app launches second function "increaseAmount" will be randomly skipped and i don`t know why.
And it doesn't matter what goes after first function. I tried simple "Log" and it gets skipped as well. Is it normal for viewModelScope?
EDIT
Checked for exceptions. Second function throws an exception that the job was cancelled:
kotlinx.coroutines.JobCancellationException: Job was cancelled; job=SupervisorJobImpl{Cancelling}#2d87ff
Also, in my Fragment it is called like this:
viewModel.insertItem(newItem)
root.findNavController().popBackStack()
So after calling this function i go back to previous Fragment. Is it possible that viewModel gets destroyed before it finishes executing all work?

Is it normal for viewModelScope?
No, it is not. In coroutines functions call must be sequential. Functions itemsRepository.insertItem(item) and increaseAmount(item.catId) must be called one after another. I see a couple of reasons why second function is not called:
Function itemsRepository.insertItem(item) throws some exception.
Current coroutine scope is cancelled before second function call.
Edit:
ViewModel objects are scoped to the Lifecycle passed to the ViewModelProvider when getting the ViewModel. The ViewModel remains in memory until the Lifecycle it's scoped to goes away permanently: in the case of an activity, when it finishes, while in the case of a fragment, when it's detached.
After you call root.findNavController().popBackStack() your fragment will be detached, the ViewModel cleared and coroutine job cancelled.
You can initialize the ViewModel in the fragment in the following way:
private val viewModel: YourViewModel by activityViewModels()
Initializing viewModel in this way it will be scoped to the Lifecycle of an Activity.
To use activityViewModels() add next line to the dependencies of app's build.gradle file:
implementation "androidx.fragment:fragment-ktx:1.2.5"

Related

What's the difference between launching a coroutine inside a fragment using `MainScope()` and `requireActivity().lifecycleScope`?

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.

ViewModel is cleared on on back press

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.

Android : Does viewmodelscope cancel all jobs on detach from window of fragment

I want to know if oncleared of viewmodel is called when onDetach of a fragment is called. This is to make sure that all coroutines will be cancelled. I was getting a IllegalStateException: Fragment not attached to a context before refactoring to kotlin and coroutines. Now I am using viemodelscope to do these tasks.
If any Context or UI related logic has to be executed on the result of any asynchronous or API calls, can result in this issue even if onDetach is called. You should make safe calls like null checking to get rid of that exception.
As Google notes on the official documentation:
Figure 1 illustrates the various lifecycle states of an activity as it
undergoes a rotation and then is finished. The illustration also shows
the lifetime of the ViewModel next to the associated activity
lifecycle. This particular diagram illustrates the states of an
activity. The same basic states apply to the lifecycle of a fragment.
And this is the image:
So the answer is: ViewModel's onCleared is called when onDestroy is called from Activity/Fragment .
As for your coroutines, you should cancel() the job on the onCleared()

Android: How to unit test "IllegalStateException: Can not perform this action after onSaveInstanceState"

I'm having a crash in my app where sometimes a dialog.show is called after the activity's lifetime. I know where this happens and would like to unit test every bug that occurred in the app to avoid it to appear again. But how can something like this be (unit?) tested?
It's difficult to unit test the exception because the occurrence is tightly bound to the Activity lifecycle as the exception message suggests - the isolation of the occurrence is practically impossible .
You could employ Robolectric and try to verify whether the dialog.show() method is invoked before onSaveInstanceState call but I would not approach the problem this way. And the tests using Robolectric are no longer unit tests.
I met with a few solutions that eliminated the exception occurrence:
You could instantiated an internal queue storing functions deferring the FragmentTransaction-related methods execution and recognize whether the activity has called onSaveInstanceState at the time the show() method is attempted to be executed.
If the activity is in the created/started/resumed state you could execute show() immediately. If not, store the function deferring the show() execution and execute them
A few lines of pseudocode below:
if (isCreatedOrStartedOrResumed) {
dialog.show()
} else {
internalQueue.add {
dialog.show()
}
}
Has the activity returned to the resumed state, execute all pending functions
fun onResume() {
super.onResume()
while(internalQueue.isNotEmpty()) {
internalQueue.poll().invoke()
}
}
This approach is not immune to configuration change though, we lose the deferred invocations once the activity gets rotated.
Alternatively, you could use ViewModel which is designed to retain the activity state across configuration change such as the rotation and store the deferred executions queue from the 1st approach inside the view model. Make sure the functions storing deferred dialog.show() executions are not anonymous classes - you may end up with memory leak introduced.
Testing:
The way I would test the dialog displaying gracefully would be the Espresso instrumentation tests.
I would also unit test view model storing/executing deferred executions.
If we consider structuring code using MVP or MVVM architectural pattern we could store the deferred executions queue within one of the architecture class members and unit test them too.
I would also include LeakCanary as a safety net against memory leaks.
Optionally, we could still use robolectric and develop integration test verifying:
the internal queue deferring dialog.show() executions after onSaveInstanceState gets called
the internal queue executing pending dialog.show() executions stored after activity has called onSaveInstanceState and returned to the resumed state again.
the dialog.show() executed immediately in case the activity is in created/started/resumed state.
That's all I have at the moment, hope you will figure out the satisfactory tests suite verifying correct dialog displaying based on the approaches suggested.

commit() fragment transaction after activity.onSaveInstanceState() has been called

In my application I have an Activity that holds 3 Fragments. The very first time the Activity is created, Fragment 1 is displayed. Next, all fragment transactions will be executed after a network operation. For example: Fragment 1 has a button to make a request to the server and when the result is ready, Fragment 1 uses a listener to call a method defined inside the parent activity, to replace fragment 1 with fragment 2.
This works fine, except when the parent activity receives the callback after its state has been saved by onSaveInstanceState(). An IllegalStateException is thrown.
I've read some answers about this problem, for example this post and I understood why this exception happens thanks to this blog.
I also take an example that I found here to try to solve the problem. This post suggests to always check if the activity is running before call commit(). So I declared a Boolean variable in the parent activity and I put its value to false in onPause() and to true in onResume().
The parent activity callback called after network operations has been completed is something like this piece of Kotlin code, where next is the number of the replacing fragment:
private fun changeFragment(next:Int){
// get the instance of the next fragment
val currentFragment = createFragment(next)
// do other stuff here
if(isRunning){
// prepare a replace fragment transaction and then commit
ft.commit()
}else{
// store this transaction to be executed when the activity state become running
}
}
This code is working fine and now I'm not getting the Exception anymore, but my question is: it's possible that onSaveInstanceState() is called after I check if(isRunning) and before I call ft.commit(), so that the commit() happens after the activity state has been saved causing IllegalStateException again?
I'm not sure if onSaveInstanceState() could interrupt my changeFragment() method at any point in time. Is it possible?
If the possibility exists and my code may be interrupted between if(isRunning) and ft.commit(), what I can do?
It could be solved adding a try{}catch(){} block like this?:
if(isRunning){
try{
ft.commit()
}catch(ie:IllegalStateException){
// store the transaction and execute it when the activity become running
}
}else{
// store the transaction and execute it when the activity become running
}
Its a bit late but as of API 26+ we can use following to check if we need to do a normal commit or commitAllowingStateLoss().
getSupportFragmentManager().isStateSaved();
Are you storing anything when you're changing states?
If not, then you can try commitAllowingStateLoss().
onSaveInstanceState() would not be able to interrupt your method if your method is being called on the main (UI) thread.
Another approach that tends to make your life easier is to not use callbacks, but rather adopt a reactive pattern like MVVM. In that pattern, your Activity or Fragment subscribe to an observable when they are interested in e.g. network responses and unsubscribe typically in the onStop or onPause lifecycle callbacks so that your methods never get called after onSaveInstanceState. For a good starting place, check the official LiveData overview.

Categories

Resources