Kotlin compose how to know when suspend function is finished?

1.2k Views Asked by At

This is the complete flow:

this is in the user profile:

.clickable {
         loginViewModel.onLogout()
         isLogin= false
   }

isLogin should be assign to false only when doLogout is finished How can I run another function or code only when this function is done? Can I know when the function is finished?

this is from ViewModel():

fun onLogout() {
        viewModelScope.launch(Dispatchers.IO) {
            withContext(Dispatchers.Main){
                loginStatus = "out"
                getUserType = null
            }
            FirebaseMessaging.getInstance().token.addOnCompleteListener {
                val token = it.result
                viewModelScope.launch(Dispatchers.IO) {
                    try {
                        repo.doLogout(token)
                    }catch (exp: Exception){
                        println("FCM token error")
                    }
                }
            }
        }
    }

This is from the repository:

suspend fun doLogout(token: String) {
        val userId = appService.currentUser!!.id
            realm.write {
                var user = query<UserInfo>("_id = $0", userId).first().find()
                if (user != null) {
                    user = findLatest(user)!!.also {
                        it.FCMToken.remove(token)
                    }
                    copyToRealm(user)
                }
        }
        withContext(Dispatchers.Default) {
            realm.syncSession.uploadAllLocalChanges()
            appService.currentUser?.logOut()
        }
    }
6

There are 6 best solutions below

0
Cipri On BEST ANSWER

I found the solution that works without errors.

This is my onLogout in LoginlView:

suspend fun onLogout(token: String, onCompletion: (Boolean) -> Unit) {
    viewModelScope.launch {
        loadingLogout = true
        withContext(Dispatchers.IO) {
            try {
                repo.doLogout(token)
                onCompletion(true)
            } catch (exp: Exception) {
                println("FCM token error")
                onCompletion(false)
            }
            withContext(Dispatchers.Main) {
                loginStatus = "out"
                getUserType = null
            }
        }
        repo = RealmRepo()
        loadingLogout = false
    }
}

I use onCompletion to check when the logout is done

Since this function is suspended, I call (from clickable()) a coroutine like this:

coroutineScope.launch {
   loginViewModel.onLogout(token!!) { logoutSuccessful ->
       if (logoutSuccessful) {
         context.getSharedPreferences("sharedPreferences", Context.MODE_PRIVATE)
    .edit().apply {
     putBoolean("isLoggedIn", false)
     apply()
              }
       }
    }
}
2
Pablo Valdes On

You can receive a callback lambda in your logout () function and invoke it when your async work is done. Although I kinda dislike that solution. The best solution is to expose a logoutEffect SharedFlow in your ViewModel, which your Composable collects to receive logout events.

0
Dominic Fischer On

Move the isLogin variable into your view model.

.clickable {
         loginViewModel.onLogout()
   }
val isLogin = mutableStateOf(true)

fun onLogout() {
        viewModelScope.launch(Dispatchers.IO) {
            withContext(Dispatchers.Main){
                loginStatus = "out"
                getUserType = null
            }
            FirebaseMessaging.getInstance().token.addOnCompleteListener {
                val token = it.result
                viewModelScope.launch(Dispatchers.IO) {
                    try {
                        repo.doLogout(token)
                        isLogin = false
                    }catch (exp: Exception){
                        println("FCM token error")
                    }
                }
            }
        }
    }
4
RandomLonelyDev On

put all your background tasks in your coroutine into an aysnc block assigned to a variable. at the end of doing your tasks, return any value. you can await this value in a runBlocking {} block to pause execution until logout is completed.

0
yardımcı Etis On

I don't suggest this solution for your case but for your question of "Can I know when the function is finished?", you may use invokeOnCompletion

    private var job: Job? = null
 
    job=viewModelScope.launch(Dispatchers.IO) {
                withContext(Dispatchers.Main){
                    loginStatus = "out"
                    getUserType = null
                }
                FirebaseMessaging.getInstance().token.addOnCompleteListener {
                    val token = it.result
                    viewModelScope.launch(Dispatchers.IO) {
                        try {
                            repo.doLogout(token)
                        }catch (exp: Exception){
                            println("FCM token error")
                        }
                    }
                }
            }

    job!!.invokeOnCompletion {
    
    if (it is Exception) { //CancellationException
            Log.d("Job","Here handle exception")
        }else{
            Log.d("Job","Done Succesfully")
        }
     }

However ,instead, use function as paramater or val isLogin = mutableStateOf(true) as @DominicFischer replied

1
Tenfour04 On

Your problem is that in viewModel.onLogout(), you're launching a new coroutine asynchronously instead of just making it a suspend function. A suspend function is synchronous. Launching a coroutine is asynchronous to the caller. You want this function to be synchronous, because you want to wait for it to finish.

You also need to get rid of addOnCompleteListener since that is also an asynchronous call. (You have an asynchronous call in another asynchronous call, inside a third nested asynchronous call since you launch another coroutine inside the listener!) This can be replaced with the await() suspend function call (since suspend functions are synchronous). If you get an unresolved reference and can't import the await() extension function, make sure you are using the -ktx version of the Firebase library in your dependencies.

You don't need withContext when you are calling a suspend function, you can remove that from your repo.doLogout call.

So change your ViewModel function to:

suspend fun onLogout() = withContext(Dispatchers.Main) {
    loginStatus = "out"
    getUserType = null
    try {
        val token = FirebaseMessaging.getInstance().token.await()
        repo.doLogout(token)
    } catch (exp: Exception){
        println("FCM token error")
    }
}

I'm not positive if = withContext(Dispatchers.Main) is necessary above. Not enough familiar with Compose yet myself to know if it's safe to modify MutableState from any thread, so I just put that there defensively.

I'm not familiar with Realm, so I can't really comment on whether your repo function is done correctly.