Upon calling my ViewModel's saveUser(), the Firebase Firestore document is updated successfully, but the coroutine Job gets cancelled, catching a JobCancellationException, and the log "User #${user.id} saved !" is never printed. Where does this cancellation comes from and how can it complete instead ?
// ViewModel.kt
fun saveUser(user: User) {
viewModelScope.launch(Dispatchers.IO) {
Repository.saveUser(user)
Log.d("test", "User #${user.id} saved !")
}
}
// Repository.kt
suspend fun saveUser(user: User) {
val documentReference = db
.collection(USERS_COLLECTION).document(user.id)
try {
documentReference.set(user).await()
Log.d("test", "Good")
} catch (e: Exception) {
Log.e("test", "Not good") // catches a JobCancellationException
}
}
Something is cancelling the Job, and your coroutine is appropriately cooperating.
It could be that your ViewModel is being cleared, since you are launching the coroutine using viewModelScope. The ViewModel should not get destroyed unless its owner (Fragment or Activity) is being destroyed. Are you doing something like trying to call finish() on your Activity while this is happening, or performing a Fragment transaction?
Or, it could be that something else in the coroutine context is cancelling the Job due to an error. My guess would be that the call to documentReference.set(user) is causing some error, and await() is cancelling the job, maybe.
Also make sure your dependencies for Firestore, Jetpack, and the KTX extensions are up to date. This may be a bug that has been fixed.
Related
I have a lifecycle aware fragment scope coroutine function that checks whether a value received from the fragments' parent activity has a certain value. If the value is null a function is called that has viewModelScope.launch coroutine scope to start a count down before showing a dialog to inform the user that the value disables certain app functionalities.
The problem is that the viewModelScope.launch coroutine function is called all the time even though the conditional if statement is not true.My question is why would a viewModelScope coroutine function be called if it is inside a conditional that is clearly false? I did notice that if I Log an output inside the if conditional it is not logged and if I Log output outside the viewModelScope.launch coroutine it is also not called. So the scoped code runs notwithstanding the value of the conditional.
The workaround for this was to make the viewmodel function a suspend function and remove the viewModelScope.launch coroutine. But why would a function be called that does not meet a conditional. Does coroutines transcend the boundaries of logic?
The lifecycleScope function has the following make up:
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.status.collectLatest {
binding.contentScanner.tvScannerStatus.text = it
if (statusCheck(it) == null) {
viewModel.reactToInactiveScanner(it) // This function is called even though the condition is false
}
}
}
}
The viewModelScope coroutine:
fun reactToInactiveScanner(s: String) {
viewModelScope.launch {
for(i in 1..5) {
if(isScannerUnavailable(s)) break
delay(1000L)
}
_scannerActive.value = isScannerUnavailable(s)
}
}
Probably statusCheck(it) is null when you think it is not ooor, since reactToInactiveScanner(s: String) is launching a coroutine in the view model scope and suspending for at least 5 seconds, and given that the viewmodel survives configuration change, doesnt matter what the lifecycle is doing, the coroutine in the viewmodel scope will keep running for 5 seconds.
try to make the function suspending :
suspend fun reactToInactiveScanner(s: String) {
for(i in 1..5) {
if(isScannerUnavailable(s)) break
delay(1000L)
}
_scannerActive.value = isScannerUnavailable(s)
}
and launch it in the lifecycle scope, so when the lifecycle stop the coroutine get canncelled and when it starts the coroutine gets launched again
I try to implement deleting user in FirebaseAuth using Kotlin flow (SharedFlow).
In onDeleteAccountClicked() there is delete() method called from FirebaseAuth which may throw AuthReauthenticationRequiredException. When the exception is thrown, app redirects to another fragment to reauthenticate, then call onDeleteAccountClicked() once again, but flow emits nothing.
ViewModel
private val _deleteAccount = MutableSharedFlow<() -> Unit>()
fun onDeleteAccountClicked() {
logd("outside the viewModelScope")
viewModelScope.launch {
logd("inside the viewModelScope")
_deleteAccount.emit {
logd("emitting log")
firebaseAuth.deleteUser()
//throw AuthReauthenticationRequiredException()
}
}
}
init {
viewModelScope.launch {
_deleteAccount
.onEach {
it()
}
.catch {
if (it is AuthReauthenticationRequiredException) {
_redirectToSignInMethodsScreen.emit(Unit)
}
}
.collect()
}
}
Logs "outside the viewModelScope" and "inside the viewModelScope" shows every time when the method is called, but "emitting log" only for the first time.
Am I even trying to do it the right way?
I just tested the code, and it works for me. I called onDeleteAccountClicked() three times with delay between calling, and all three logs "emitting log" inside emit lambda were printed. Try to remove calling firebaseAuth.deleteUser() inside emit lambda and test. Calling FirebaseUser.delete function when user is already deleted throws FirebaseAuthInvalidUserException exception. Maybe that's why you didn't see logs - because FirebaseUser.delete function throws an exception.
I think the structure you use for calling just one function is a bit complicated, I can suggest to get rid of _deleteAccount flow and just wrap firebaseAuth.deleteUser() inside try-catch (you even don't need to launch a coroutine for that):
fun onDeleteAccountClicked() {
try {
firebaseAuth.deleteUser()
} catch(e: AuthReauthenticationRequiredException) {
_redirectToSignInMethodsScreen.emit(Unit)
}
}
I've noticed that some of the users have issues to use flexible in-app update, the JobCancellationException: Job was cancelled is thrown with incomprehensible stack trace:
at dalvik.system.VMStack.getThreadStackTrace(VMStack.java)
at java.lang.Thread.getStackTrace(Thread.java:1538)
at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1068)
at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1063)
at java.lang.Thread.dispatchUncaughtException(Thread.java:1955)
Unfortunately, I don't which part of the code is causing the issue. This is the only coroutine related code, staying in MyViewModel:
init {
viewModelScope.launch {
try {
appUpdateManager.requestUpdateFlow().collect { appUpdateResult ->
// Do something with result.
}
} catch (e: InstallException) {
// Do something with an error.
}
}
}
fun requestUpdate(fragment: Fragment) {
viewModelScope.launch {
try {
val appUpdateInfo = appUpdateManager.requestAppUpdateInfo()
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.FLEXIBLE,
fragment,
REQUEST_CODE
)
} catch (e: IntentSender.SendIntentException) {
}
}
}
I suspect that code inside requestUpdateFlow() is calling offer while the coroutine job is already cancelled and I can't see the exact stacktrace, because Play Core library is obfuscated?
I'm using following versions of the libraries:
"com.google.android.play:core:1.7.2"
"com.google.android.play:core-ktx:1.7.0"
JobCancellationException: Job was cancelled is thrown in almost case is job in coroutine scope is cancelled.
Example: User go to a screen a in which call api to get something. But user press back to close this screen while api not complete. Thus, when receive response, job cancelled before -> exception.
To more handle JobCancellationException you can using suspendCancellableCoroutine.
More detail : https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/suspend-cancellable-coroutine.html
I have a file managing class that can save a big file. The file manager class is an application singleton, so it outlives my UI classes. My Activity/Fragment can call the save suspend function of the file manager from a coroutine and then show success or failure in the UI. For example:
//In MyActivity:
private fun saveTheFile() = lifecycleScope.launch {
try {
myFileManager.saveBigFile()
myTextView.text = "Successfully saved file"
} catch (e: IOException) {
myTextView.text = "Failed to save file"
}
}
//In MyFileManager
suspend fun saveBigFile() {
//Set up the parameters
//...
withContext(Dispatchers.IO) {
//Save the file
//...
}
}
The problem with this approach is that I don't want the save operation to be aborted if the Activity is finished. If the activity is destroyed before the withContext block gets going, or if the withContext block has any suspension points in it, then saving will not be completed because the coroutine will be canceled.
What I want to happen is that the file is always saved. If the Activity is still around, then we can show UI updates on completion.
I thought one way to do it might be to start a new coroutineScope from the suspend function like this, but this scope still seems to get cancelled when its parent job is cancelled.
suspend fun saveBigFile() = coroutineScope {
//...
}
I thought another alternative might be to make this a regular function that updates some LiveData when it's finished. The Activity could observe the live data for the result, and since LiveData automatically removes lifecycle observers when they're destroyed, the Activity is not leaked to the FileManager. I'd like to avoid this pattern if the something less convoluted like the above can be done instead.
//In MyActivity:
private fun saveTheFile() {
val result = myFileManager.saveBigFile()
result.observe(this#MyActivity) {
myTextView.text = when (it) {
true -> "Successfully saved file"
else -> "Failed to save file"
}
}
}
//In MyFileManager
fun saveBigFile(): LiveData<Boolean> {
//Set up the parameters
//...
val liveData = MutableLiveData<Boolean>()
MainScope().launch {
val success = withContext(Dispatchers.IO) {
//Save the file
//...
}
liveData.value = success
}
return liveData
}
You can wrap the bit that you don't want to be cancelled with NonCancellable.
// May cancel here.
withContext(Dispatchers.IO + NonCancellable) {
// Will complete, even if cancelled.
}
// May cancel here.
If you have code whose lifetime is scoped to the lifetime of the whole application, then this is a use case for the GlobalScope. However, just saying GlobalScope.launch is not a good strategy because you could launch several concurrent file operations that may be in conflict (this depends on your app's details). The recommended way is to use a globally-scoped actor, in the role of an executor service.
Basically, you can say
#ObsoleteCoroutinesApi
val executor = GlobalScope.actor<() -> Unit>(Dispatchers.IO) {
for (task in channel) {
task()
}
}
And use it like this:
private fun saveTheFile() = lifecycleScope.launch {
executor.send {
try {
myFileManager.saveBigFile()
withContext(Main) {
myTextView.text = "Successfully saved file"
}
} catch (e: IOException) {
withContext(Main) {
myTextView.text = "Failed to save file"
}
}
}
}
Note that this is still not a great solution, it retains myTextView beyond its lifetime. Decoupling the UI notifications from the view is another topic, though.
actor is labeled as "obsolete coroutines API", but that's just an advance notice that it will be replaced with a more powerful alternative in a future version of Kotlin. It doesn't mean it's broken or unsupported.
I tried this, and it appears to do what I described that I wanted. The FileManager class has its own scope, though I suppose it could also be GlobalScope since it's a singleton class.
We launch a new job in its own scope from the coroutine. This is done from a separate function to remove any ambiguity about the scope of the job. I use async
for this other job so I can bubble up exceptions that the UI should respond to.
Then after launch, we await the async job back in the original scope. await() suspends until the job is completed and passes along any throws (in my case I want IOExceptions to bubble up for the UI to show an error message). So if the original scope is cancelled, its coroutine never waits for the result, but the launched job keeps rolling along until it completes normally. Any exceptions that we want to ensure are always handled should be handled within the async function. Otherwise, they won't bubble up if the original job is cancelled.
//In MyActivity:
private fun saveTheFile() = lifecycleScope.launch {
try {
myFileManager.saveBigFile()
myTextView.text = "Successfully saved file"
} catch (e: IOException) {
myTextView.text = "Failed to save file"
}
}
class MyFileManager private constructor(app: Application):
CoroutineScope by MainScope() {
suspend fun saveBigFile() {
//Set up the parameters
//...
val deferred = saveBigFileAsync()
deferred.await()
}
private fun saveBigFileAsync() = async(Dispatchers.IO) {
//Save the file
//...
}
}
I read a lot of docs about Kotlin coroutines but still having some doubts. I'm using Retrofit with coroutines so I need to do request with Dispatchers.IO context but use result within Dispatchers.Main context to assign it to ViewModel. My code is:
fun doHttpreq() {
viewModelScope.launch(Dispatchers.IO) {
try {
//should I call await() here? (I guess the correct way to keep execution of request outside of Main thread)
val request = RestClient.instance.getItems().await()
withContext(Dispatchers.Main) {
//or should I call await() here? (BUT need request to be executed outside of Main thread!)
if (request.isSuccessful) {
//asign items to ViewModel
} else {
//asign error to ViewModel
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
//asign error to ViewModel
}
}
}
}
You can take your deffered job in variable and then await it on your Main dispatcher like below :
try {
//Rather than await here, you take your Job as Deffered
val request: Deferred? = RestClient.instance.getItems()
withContext(Dispatchers.Main) {
//Yes, you can await here because it's non-blocking call and can be safely obtained from here once completed
val result = request?.await()
if (request.isSuccessful) {
//asign items to ViewModel
} else {
//asign error to ViewModel
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
//asign error to ViewModel
}
}
What official doc states about await() :
Awaits for completion of this value without blocking a thread and resumes when deferred computation is complete, returning the resulting value or throwing the corresponding exception if the deferred was cancelled.
This suspending function is cancellable. If the Job of the current coroutine is cancelled or completed while this suspending function is waiting, this function immediately resumes with CancellationException.
This function can be used in select invocation with onAwait clause. Use isCompleted to check for completion of this deferred value without waiting.
As Coroutines are suspending instead of blocking, there should not be any need to manage the thread they are running on. In your case Retrofit handles this for you. Also the Deferred type is actually a hot data source. This means that the Call is executed before you even call await on it. await just waits for the data to be there.
So instead you can launch on the Main dispatcher directly. Therefore you only have one place to call await() from.
viewModelScope.launch(Dispatchers.Main) {
try {
val request = RestClient.instance.getItems().await()
if (request.isSuccessful) {
//asign items to ViewModel
} else {
//asign error to ViewModel
}
} catch (e: Exception) {
//asign error to ViewModel
}
}