安卓上, 协程可以帮忙解决两大问题:
- 管理长时间运行的任务, 这些任务可能阻塞主线程, 导致 UI 卡顿.
- 在主线程上安全地调用网络或磁盘操作.
安卓上使用协程的最好方式是使用官方的架构组件, 它们提供了对协程的支持. 目前 ViewModel
, Lifecycle
, LiveData
, Room
组件提供了对协程一等的支持.
对 ViewModel
的支持主要是在 ViewModel
上提供了一个称为 ViewModelScope
的 CoroutineScope
, 所有在 ViewModelScope
上启动的协程, 当 ViewModelScope
销毁时自动取消. 这样可以有效防止忘记取消任务时导致的资源泄漏.
class MyViewModel: ViewModel() {
init {
viewModelScope.launch {
// 调用加载数据的挂起函数, 当 ViewModel clear() 时自动取消任务.
}
}
}
其实 viewModelScope
的实现非常简单, 就是一个带有 Dispatchers.Main
的 SupervisorJob
, 当 ViewModel.clear()
时, 在里面调用 Job.cancel()
, 因为结构化并发的原因, 所有在 viewModelScope
范围内启动的协程, 都会级联取消.
每个具有生命周期的对象(Lifecycle)都有一个 LifecycleScope
, 所有在它的范围内启动的协程, 当生命周期对象销毁时, 都会取消. 生命周期对象的 CoroutineScope
可以通过 lifecycle.coroutineScope
或者 lifecycleOwner.lifecycleScope
属性获取.
class MyFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
val params = TextViewCompat.getTextMetricsParams(textView)
val precomputedText = withContext(Dispatchers.Default) {
PrecomputedTextCompat.create(longTextContent, params) //挂起
}
TextViewCompat.setPrecomputedText(textView, precomputedText)
}
}
}
在 Activity
或者 Fragment
中, 我们有时需要等到某个生命周期方法时, 或者至少在某个生命周期方法之后才执行某一任务, 如页面状态至少要 STARTED
才可以执行 FragmentTransaction
, 对这种需求, 生命周期组件也提供了支持. Lifecycle
提供了 lifecycle.whenCreated
, lifecycle.whenStarted
, lifecycle.whenResumed
三个方法, 运行在这些方法内的协程, 如果页面的状态不是至少处于要求的最小状态, 协程将会挂起运行.
class MyFragment: Fragment {
init { // 在构建方法这么早的阶段就启动了协程.
lifecycleScope.launch {
whenStarted {
// 页面 onStart 之后运行, 可调用其他挂起函数
loadingView.visibility = View.VISIBLE
val canAccess = withContext(Dispatchers.IO) {
checkUserAccess()
}
// 当 checkUserAccess 方法返回, 如果页面不是*至少*处于 STARTED 状态.
// 下面的代码会挂起. 否则我们至少处于 STARTED 状态, 可以安全地调用
// fragment transactions 方法
loadingView.visibility = View.GONE
if (canAccess == false) {
findNavController().popBackStack()
} else {
showContent()
}
}
// 这行代码仅在上面的 whenStarted 代码块运行完成后执行, 因为 whenStarted 是挂起函数
}
}
}
如果协程通过上面的 whenXXX
方法启动后, 处于活动状态, 还没有结束, 这时页面销毁了, 则协程会自动取消, 并且会走到下面的 finally
块中, 所在 finally
中, 需要检查页面所处的状态, 再决定做什么动作.
class MyFragment: Fragment {
init {
lifecycleScope.launchWhenStarted {
try {
// 这里调用挂起函数
} finally {
// 页面 DESTROYED 时可能会走到这里
if (lifecycle.state >= STARTED) {
// 检查到页面并非处于 DESTROYED 状态
// Fragment transactions.
}
}
}
}
}
注意: 如果页面 restart
重启了, 但协程并不会重启, 总之要确保信息是正确的.
一个常见的操作是, 异步加载数据, 然后使用 LiveData
提供出去, 如下:
val user: LiveData<User> = liveData (timeoutInMs = 5000) {
val data = database.loadUser() // loadUser 是一个挂起函数.
emit(data)
}
这里 liveData
是一个 builder 函数, 在 builder 代码块中, 它调用挂起函数 loadUser()
然后通过 emit
把加载的数据发射出去.
需要注意的是, 当 LiveData<User>
的状态变为活动时(即有人订阅观察它), 加载动作才会真正执行, 而当它变为不活动时, 并且空闲了 timeoutInMs
毫秒, 它将会自动取消. 如果在完成之前将其取消,则如果LiveData再次变为活动状态,它将重新启动。如果它在先前的运行中成功完成,则不会重新启动。请注意,只有自动取消后,它才会重新启动。如果由于任何其他原因取消了该块(例如,引发CancelationException),则它不会重新启动。
您也可以从块中 emit
多个值。每个 emit
调用都会暂停该块的执行,直到在主线程上设置了LiveData
为止。
val user: LiveData<Result> = liveData {
emit(Result.loading())
try {
emit(Result.success(fetchUser()))
} catch(ioException: Exception) {
emit(Result.error(ioException))
}
}
LiveData 也可以与 Transformations
一起使用, 如下:
class MyViewModel: ViewModel() {
private val userId: LiveData<String> = MutableLiveData()
val user = userId.switchMap { id ->
// 变换操作指定上下文, IO 线程执行
liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
emit(database.loadUserById(id))
}
}
}
任何时候如果你想发射新值, 你也可以使用 emitSource(source: LiveData<T>)
来发射多个值, 这里它产生值的源改变了. 调用 emit()
或者 emitSource()
会移除之前添加的源, 例如:
class UserDao: Dao {
@Query("SELECT * FROM User WHERE id = :id")
fun getUser(id: String): LiveData<User>
}
class MyRepository {
fun getUser(id: String) = liveData<User> {
val disposable = emitSource( // 这里使用数据库作为源
userDao.getUser(id).map {
Result.loading(it)
}
)
try {
val user = webservice.fetchUser(id)
// Stop the previous emission to avoid dispatching the updated user
// as `loading`.
disposable.dispose()
// Update the database.
userDao.insert(user)
// Re-establish the emission with success type.
emitSource( // 重新用数据库作为源, 因为之前 dispose 了.
userDao.getUser(id).map {
Result.success(it)
}
)
} catch(exception: IOException) {
// Any call to `emit` disposes the previous one automatically so we don't
// need to dispose it here as we didn't get an updated value.
emitSource(
userDao.getUser(id).map {
Result.error(exception, it)
}
)
}
}
}
Room 从 v2.1 开始支持协程, 在 DAO
方法中可以定义挂起方法.
@Dao
interface UsersDao {
@Query("SELECT * FROM users")
suspend fun getUsers(): List<User>
@Query("UPDATE users SET age = age + 1 WHERE userId = :userId")
suspend fun incrementUserAge(userId: String)
@Insert
suspend fun insertUser(user: User)
@Update
suspend fun updateUser(user: User)
@Delete
suspend fun deleteUser(user: User)
}
https://developer.android.com/topic/libraries/architecture/coroutines
https://developer.android.com/kotlin/coroutines
https://medium.com/androiddevelopers/coroutines-on-android-part-i-getting-the-background-3e0e54d20bb
https://medium.com/androiddevelopers/easy-coroutines-in-android-viewmodelscope-25bffb605471
https://medium.com/androiddevelopers/room-coroutines-422b786dc4c5