3

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()
        }
    }
Cipri
  • 57
  • 1
  • 9
  • 1
    Assuming `doLogout()` has been implemented correctly, so it doesn't launch background tasks, you do this correctly already. – broot Mar 18 '23 at 23:08
  • it does do a "withContext(Dispatchers.Default)" is this an error? – Cipri Mar 18 '23 at 23:27
  • I want to wait for all the function to finish, including background tasks or coroutines. – Cipri Mar 18 '23 at 23:41
  • Suspending functions are by default executed sequentially. If you don't run anything using external/additional coroutine scopes, then the function returns only after it finished running all its code. Your current approach should be fine. Do you observe a different behavior or you just wanted to confirm if this guaranteed to work as expected? – broot Mar 19 '23 at 00:19
  • Suspend functions are synchronous. Your code *has* to wait for it to return before execution continues, just like any other function. But like any other function, you could define it to internally start some other asynchronous task that it doesn't wait for before returning. Normally, you wouldn't do that when composing your suspend function. – Tenfour04 Mar 19 '23 at 00:52
  • Yes, I am trying to debug and in the middle of the logout function, isLogin will be assigned. After, doLogout continues... – Cipri Mar 19 '23 at 01:13
  • 1
    Sounds like you're doing something wrong inside `doLogout` if I understand what you're saying correctly. – Tenfour04 Mar 19 '23 at 01:28
  • Probably yes, can someone of you help me with this? Could we chat somewhere or have a fast call? Thanks for your time. – Cipri Mar 19 '23 at 10:19
  • 1
    You can start by providing the code of your `doLogout()` if possible. – broot Mar 19 '23 at 10:54
  • I have updated the question with the complete flow. – Cipri Mar 19 '23 at 15:17
  • So as I said earlier, the problem is that you use `viewModelScope.launch()` which invokes the operation in the background. Make it a suspend function and invoke the code directly. Also, why initially did you ask about a suspending function? In year real example you call a non-suspend function. – broot Mar 19 '23 at 15:27
  • So the best way is to make onLogout suspend? And dont use anymore viewModelScope.launch(Dispatchers.IO)? – Cipri Mar 19 '23 at 15:47
  • I am in right path to solve this? – Cipri Mar 20 '23 at 10:10
  • What a should use in user profile Composable to run the suspend function? GlobalScope.lunch{}? – Cipri Mar 21 '23 at 18:51

6 Answers6

1

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.

Tenfour04
  • 83,111
  • 11
  • 94
  • 154
0

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.

Pablo Valdes
  • 734
  • 8
  • 19
0

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")
                    }
                }
            }
        }
    }
Dominic Fischer
  • 1,695
  • 10
  • 14
0

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.

  • This would block the main thread to wait for IO. Don't do this!!!! – Tenfour04 Mar 28 '23 at 13:08
  • I know it does. I suggested it to pause interaction with the app until logout is completed. That would prevent other data being altered after it was sent. – RandomLonelyDev Mar 28 '23 at 15:38
  • The UI thread should never be blocked. Users hate it because it freezes all interaction with the phone and makes it seem like it has crashed, and it can cause the ANR error message to pop up. Google hates it because it makes their devices seem broken. Google Play almost certainly uses ANR stats to diminish apps in search results if they are guilty of doing this. – Tenfour04 Mar 28 '23 at 16:13
  • Logouts shouldn't take that long. – RandomLonelyDev Mar 29 '23 at 12:36
0

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

yardımcı Etis
  • 140
  • 2
  • 12
0

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()
              }
       }
    }
}
Cipri
  • 57
  • 1
  • 9