Kotlin协程在Android中阻塞主线程

9

我是Kotlin和协程的新手。在我的活动中有一个fun,里面检查User的用户名和密码,如果正确,则返回Users对象。
一切都没问题。但是当我按下按钮时,我的活动会被阻塞并等待Users登录的响应。
我使用了这个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
    }  

这是我的 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)
    }
}

用户服务:

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?
    }

这是我的 API 接口:

interface WebApiService {

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

当等待从服务器检索数据时,我该如何解决阻止我的活动的问题?

2个回答

12
你几乎永远不应该在Android应用中使用runBlocking。它只应该在JVM应用的main函数或测试中使用,以允许在应用退出之前完成的协程的使用。否则,它会破坏协程的目的,因为它会阻塞直到所有lambda返回。
你也不应该使用GlobalScope,因为这会使得在Activity关闭时取消任务变得棘手,并且它会在后台线程而不是主线程中启动协程。你应该为Activity使用一个局部作用域。你可以通过在Activity中创建一个属性(val scope = MainScope())并在onDestroy()中取消它(scope.cancel())来实现。或者,如果你使用androidx.lifecycle:lifecycle-runtime-ktx库,你可以直接使用现有的lifecycleScope属性。
如果你总是在返回之前等待异步任务完成,那么整个函数将会阻塞,使得后台任务阻塞主线程。
你可以采取几种方法来解决这个问题。
让ViewModel暴露一个挂起函数,然后Activity从协程中调用它。
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
}

使用适当的视图模型封装,Activity 实际上不应该像这样启动协程。在 ViewModel 中,`user` 属性应该是一个 LiveData,Activity 可以观察它。因此,协程只需要在 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
}

*如果你正在使用一个不支持协程的代码库,并且它要求你重写或实现一个非挂起函数,该函数将在主线程之外被调用,并且能够处理阻塞、长时间运行的代码,那么使用runBlocking作为这两个世界之间的桥梁是可以接受的,以便在与该代码库交互时能够使用基于协程的代码。

1
非常感谢您详细的回答。我使用了第二种方法,但两种方法都是正确的。我对作用域和生命周期一无所知。这个问题向我展示了我需要更多地尝试它们。谢谢。 - Sadeq Shajary

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接