Search code examples
androidkotlinretrofit

Kotlin coroutines block main thread in Android


I am new in Kotlin and coroutines. I have a fun in my activity and inside it, check User username and password and if its true, return Users object.
every thing is Ok. but when I press button, my activity blocked and wait for response of Users login.
I use this fun:

private fun checkLogin() : Boolean {           
        runBlocking {
            coroutineScope {
                launch {
                    user = viewModel.getUserAsync(login_username.text.toString(), login_password.text.toString()).await()
                }
            }
            if(user == null){
                return@runBlocking false
            }
            return@runBlocking true
        }
        return false
    }  

It's my ViewModel :

class LoginViewModel(app: Application) : AndroidViewModel(app) {
    val context: Context = app.applicationContext
    private val userService = UsersService(context)

    fun getUserAsync(username: String, password: String) = GlobalScope.async {
        userService.checkLogin(username, password)
    }
}

UsersService:

class UsersService(ctx: Context) : IUsersService {
        private val db: Database = getDatabase(ctx)
        private val api = WebApiService.create()
        override fun insertUser(user: Users): Long {
            return db.usersDao().insertUser(user)
        }

        override suspend fun checkLogin(username: String, pass: String): Users? {
            return api.checkLogin(username, pass)
        }
    }

    interface IUsersService {
        fun insertUser(user: Users) : Long
        suspend fun checkLogin(username: String, pass: String): Users?
    }

And it is my apiInterface:

interface WebApiService {

    @GET("users/login")
    suspend fun checkLogin(@Query("username") username: String,
                   @Query("password")password: String) : Users

How can I resolve issue of blocking my activity when waiting for retrieve data from server?


Solution

  • You should almost* never use runBlocking in an Android app. It is only meant to be used in the main function of a JVM app or in a test to allow the use of coroutines that complete before the app exits. It otherwise defeats the purpose of coroutines, because it blocks until all of its lambda returns.

    You also shouldn't use GlobalScope, because it makes it tricky to cancel your jobs when the Activity closes, and it starts the coroutine in a background thread instead of the main thread. You should use a local scope for the Activity. You can do this by creating a property in your activity (val scope = MainScope()) and canceling it in onDestroy() (scope.cancel()). Or if you use the androidx.lifecycle:lifecycle-runtime-ktx library you can just use the existing lifecycleScope property.

    And if you always await your async job before returning, then your whole function will block until you get the result, so you have taken a background task and made it block the main thread.

    There are a couple ways you can go about fixing this.

    1. Make the ViewModel expose a suspend function, and the activity calls it from a coroutine.
    class LoginViewModel(app: Application) : AndroidViewModel(app) {
        //...
    
        // withContext(Dispatchers.Default) makes the suspend function do something
        // on a background thread and resumes the calling thread (usually the main 
        // thread) when the result is ready. This is the usual way to create a simple
        // suspend function. If you don't delegate to a different Dispatcher like this,
        // your suspend function runs its code in the same thread that called the function
        // which is not what you want for a background task.
        suspend fun getUser(username: String, password: String) = withContext(Dispatchers.Default) {
            userService.checkLogin(username, password)
        }
    }
    
    //In your activity somewhere:
    lifecycleScope.launch {
        user = viewModel.getUser(login_username.text.toString(), login_password.text.toString())
        // do something with user
    }
    
    1. With proper viewmodel encapsulation, the Activity really shouldn't have to launch coroutines like this. The user property should be a LiveData in the ViewModel that the activity can observe. So then the coroutines only need to be launched from within the ViewModel:
    class LoginViewModel(app: Application) : AndroidViewModel(app) {
        //...
        private val _user = MutableLiveData<User>()
        val user: LiveData<User> = _user
    
        init {
            fetchUser()
        }
    
        private fun fetchUser(username: String, password: String) = viewModelScope.launch {
            val result = withContext(Dispatchers.Default) {
                userService.checkLogin(username, password)
            }
            _user.value = result
        }
    }
    
    //In your activity somewhere:
    viewModel.user.observe(this) { user ->
        // do something with user
    }
    

    *If you are working with a library of code that doesn’t support coroutines and it requires you to override or implement a non-suspend function that will be called off the main thread and can handle blocking, long-running code, then it would be acceptable to use runBlocking as a bridge between these two worlds to be able to use you coroutine-based code when interacting with that library.