Search code examples
androidkotlinandroid-roomkotlin-coroutines

Concurrently store access token in Repository class. Kotlin


I'm building a LoginActivity for using Android Studio and Kotlin. I managed to make the API call I need, but now I want to store the access token from the API response locally in a Room DB. My problem is that I want the response from the call to be sent back to the LoginViewModel as soon as it is received, whilst concurrently, the token is being saved in the DB.

//in LoginModelView
fun onLoginButtonPress(email:String, password:String ) {
        loginState.value= LoginState(loading = true)
        val requestBody: LoginRequest = LoginRequest(email,password)

        viewModelScope.launch{
            val isAuthorized : Boolean = _repository.makeLoginRequest(requestBody)
            if (isAuthorized){
                loginState.value= LoginState(authorized = true)
            }else{
                loginState.value= LoginState(credentialsDeclined = true)
            }
        }

    }
// LoginRepository
class LoginRepository {
    suspend fun makeLoginRequest(requestBody: LoginRequest): Boolean{
        var isAuth: Boolean = false
        try {
            val response = apiService.login(requestBody)
            when (response.code()){
                200 -> isAuth = true
                404 -> isAuth=false
            }

            response.body()?.data?.let { Log.d("token", it.Token) }

        }catch (e:Exception){
            e.message?.let { Log.d("LoginViewModelError", it) }
        }
        if (isAuth){
            viewModelScope
            // launch coroutine to save some data into Room DB


            }
        }
        return isAuth
    }
}

I can figure out the Room DB aspect but I am not sure about the concurrency aspect. What is the recommended way to solve this? I can't use viewModelScope.launch and launching a coroutine inside a couroutine seems like a bad idea since the parent coroutine may be terminated before the child coroutine finishes. I thought that maybe i could a make token a LiveData observed from another repository class but that seems inneficient since the token needs to be written once


Solution

  • You want makeLoginRequest to perform two IO operations that should be moved off the main thread and run asynchronously:

    1. An HTTP request over the internet
    2. Database access on the local file system

    You launch them in a new coroutine in the view model's scope. What is missing is that makeLoginRequest should switch to the IO dispatcher with

    suspend fun makeLoginRequest(requestBody: LoginRequest): Boolean = withContext(Dispatchers.IO) {
        ...
    }
    

    Just replace return isAuth with isAuth and you are good to go. This takes care of operation #1.

    Now to the problem at hand, where operation #2 should take place. Your idea is to launch another coroutine so you can instantly return the result of operation #1. Although that is possible I want to question why you even want to do that. Yes, waiting for operation #2 to finish before letting the view model continue with the result that is already available seems like a waste of time. But then again, the view model had to wait already for a considerable amount of time (for the network access over the internet to complete). Adding to this the delay caused by the database access on the local filesystem, however, won't make any measurable difference because it will be way, way faster than the internet access. My suggestion would be to just synchronously store the token in the database after the HTTP request finished, and then return to the view model. It's the simple solution.

    If, however, you really, really want to return to the view model with the database access running asynchronously in the background you need to launch a new coroutine. That's not a bad thing in itself, it's actually what structured concurrency (the principle behind coroutines) is all about. And you shouldn't be afraid of the coroutine being cancelled: Cancelling a coroutine is cooperative. Your database access won't be interrupted just because the coroutine scope it is running in was cancelled. If you do lengthy operations in your coroutine, though, you should actively check if the coroutine was cancelled to honor that request and terminate your operation. In your case you don't want that, so just don't do it. :)

    The other thing is that the view model's scope isn't that easily cancelled anyways. For example, it will survive configuration changes that will trigger your activity to be recreated - the view model and it's scope on the other hand won't be affected by it. Have a look at the view model's lifecycle for more details.

    Now, to launch a new coroutine you would need a coroutine scope first. The viewModelScope, however, isn't available in the repository. You can always create a new scope in a suspend function like this, though:

    coroutineScope { 
        launch { 
            // database access
        }
    }
    

    But this won't help you. Although launch will instantly return, coroutineScope will block until all launched coroutines are finished. You would have to wait after all for the database access to be completed.

    Instead, you need another scope. That scope should be passed to your function as a parameter:

    suspend fun makeLoginRequest(
        requestBody: LoginRequest,
        scope: CoroutineScope,
    ): Boolean = withContext(Dispatchers.IO) {
        ...
    
        scope.launch {
            // database access
        }
    
        isAuth
    }
    

    The function call in the view model now needs to provide the scope. The viewModelScope is good candidate for that. Since your function call already is located in that scope, you can use this:

    val isAuthorized: Boolean = _repository.makeLoginRequest(requestBody, this)
    

    With all of this said I want to reiterate: Just use the simple solution and skip the coroutine altogether. Remember to switch to the IO dispatcher though. The network access as well as the database access should always be done on that dispatcher.