I am using activity. Finish() to close an application on logout. However, when the application is opened back up again, my coroutines do not launch, more specifically, a viewModelScope.launch does not fire up. Could this because I am closing the application wrong? I would like to close the application and clear all viewModels and data from the application on logout
Here is how I am closing the application
val activity = (LocalContext.current as? Activity)
activity?. Finish()
Here is the coroutine that fails to launch on restart (user login)
fun login() {
Log.d("Login test", "Attempt to login stage 1")
viewModelScope.launch {
Log.d("Collection test", "Attempt to login stage 2")
userState = AuthResource.Loading
try {
} catch (e: IOException) {
} catch (e: HttpException) {
}
}
}
The first log message is received, but never the second one and the loading state is never activated. I am using kotlin with jetpack compose
When an Activity finishes, the associated view model is terminated. Coroutines that haven't run yet won't be run. That's part of the point of a viewModel- it's bound to a lifecycle, and that lifecycle ends with the Activity. You need to run it on another scope if you want to ensure it happens.
Related
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
Using lifecycleScope.launchWhenStarted I can start a coroutine that will pause if my activity is not in atleast started state, and will resume if when the activity goes back in started state.
I want to replicate this behavior on a coroutine that is running on IO. I tried this, but it doesn't work:
lifecycleScope.launchWhenStarted {
withContext(IO)
{
var i = 0
while (true)
{
Log.i("launchWhenStarted", "STATE: ${lifecycle.currentState.name} ${i++}")
delay(500)
}
}
}
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.
I'm trying out coroutines instead of RxJava on basic network calls for the fist time to see what it's like and running into some issues with lag/threading
In the below code, I'm doing a network call userRepo.Login() and if an exception happens I show an error message and stop the progress animation that I started at the start of the function.
If I leave everything on the CommonPool (or don't add any pool) it crashes saying the animation must be done on a looper thread if an exception happens. In other circumstances I've received errors saying this must be done on the UI thread as well, same problem, different thread requirements.
I can't launch the whole coroutine on the UI thread though, because the login call will block since it's on the UI thread and messes up my animation (which makes sense).
The only way I can see to resolve this, is the launch a new coroutine on the UI thread from within the existing coroutine, which works, but seems weird.
Is this the proper way to do things, or am I missing something?
override fun loginButtonPressed(email: String, password: String) {
view.showSignInProgressAnimation()
launch(CommonPool) {
try {
val user = userRepo.login(email, password)
if (user != null) {
view.launchMainActivity()
}
} catch (exception: AuthException) {
launch(UI) {
view.showErrorMessage(exception.message, exception.code)
view.stopSignInProgressAnimation()
}
}
}
}
You should start from the opposite end: launch a UI-based coroutine, from which you hand off heavy operations to an external pool. The tool of choice is withContext():
override fun loginButtonPressed(email: String, password: String) {
view.showSignInProgressAnimation()
// assuming `this` is a CoroutineScope with dispatcher = Main...
this.launch {
try {
val user = withContext(IO) {
userRepo.login(email, password)
}
if (user != null) {
view.launchMainActivity()
}
} catch (exception: AuthException) {
view.showErrorMessage(exception.message, exception.code)
view.stopSignInProgressAnimation()
}
}
}
This way you keep your natural Android programming model, which assumes the GUI thread.