Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 558
Sadeq Shajary Avatar asked Feb 05 '20 18:02

Sadeq Shajary


People also ask

Does coroutine block main thread?

Coroutines is our recommended solution for asynchronous programming on Android. Noteworthy features include the following: Lightweight: You can run many coroutines on a single thread due to support for suspension, which doesn't block the thread where the coroutine is running.

How do I stop main thread on Kotlin?

You can do this by creating a property in your activity ( val scope = MainScope() ) and canceling it in onDestroy() ( scope. cancel() ).

Do Kotlin coroutines use threads?

In Kotlin coroutines, the execution is PINNED to one single CPU thread. Suspend and resume operations will make sure the coroutine will run on that same thread until the end.

Does a coroutine run on a different thread?

Coroutines are a design pattern for writing asynchronous programs for running multiple tasks concurrently. In asynchronous programs, multiple tasks execute in parallel on separate threads without waiting for the other tasks to complete.


1 Answers

You should 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
}
like image 71
Tenfour04 Avatar answered Oct 13 '22 08:10

Tenfour04