Coroutines On Android (part III): real work
这是协程系列的第三篇文章
1. 使用协程解决实际问题
协程擅长完成两类任务:
- 请求一次就完成的(比如,打开这个页面,发起一次请求,服务端返回数据,浏览器渲染展示,这次请求就完成了)
- 流请求(这种请求类似于长连接)
以下用例子来说明请求一次就完成的这种任务
- viewModel 中开启协程,因为它和生命周期绑定,在用户离开屏幕的时候,会自动取消协程里面的任务。
class ProductsViewModel(val productsRepository: ProductsRepository): ViewModel() {
private val _sortedProducts = MutableLiveData<List<ProductListing>>()
val sortedProducts: LiveData<List<ProductListing>> = _sortedProducts
/**
* Called by the UI when the user clicks the appropriate sort button
*/
fun onSortAscending() = sortPricesBy(ascending = true)
fun onSortDescending() = sortPricesBy(ascending = false)
private fun sortPricesBy(ascending: Boolean) {
viewModelScope.launch {
// suspend and resume make this database request main-safe
// so our ViewModel doesn't need to worry about threading
_sortedProducts.value =
productsRepository.loadSortedProducts(ascending)
}
}
}
再来看Repository,因为这个仓库是一个object对象,不能感知activity/fragment的生命周期,所以,这里最好只是暴露一些suspend function ,这也是为了复用
class ProductsRepository(val productsDao: ProductsDao) {
/**
* This is a "regular" suspending function, which means the caller must
* be in a coroutine. The repository is not responsible for starting or
* stopping coroutines since it doesn't have a natural lifecycle to cancel
* unnecessary work.
*
* This *may* be called from Dispatchers.Main and is main-safe because
* Room will take care of main-safety for us.
*/
suspend fun loadSortedProducts(ascending: Boolean): List<ProductListing> {
return if (ascending) {
productsDao.loadProductsByDateStockedAscending()
} else {
productsDao.loadProductsByDateStockedDescending()
}
}
}
最后看看Dao, 这是属于Room里的,Room使用了susppend , 它会保证这些调用会是主线程安全的。这也意味着,你可以在Dispatchers.Main中调用。Room使用了自己的dispatcher,所以,你在调用的时候,就不要再指定withContext(Dispatchers.IO),这会使代码变复杂而且查询会变慢。
@Dao
interface ProductsDao {
// Because this is marked suspend, Room will use it's own dispatcher
// to run this query in a main-safe way.
@Query("select * from ProductListing ORDER BY dateStocked ASC")
suspend fun loadProductsByDateStockedAscending(): List<ProductListing>
// Because this is marked suspend, Room will use it's own dispatcher
// to run this query in a main-safe way.
@Query("select * from ProductListing ORDER BY dateStocked DESC")
suspend fun loadProductsByDateStockedDescending(): List<ProductListing>
}
2. 一次请求可参照的模式
简而言之,一次请求就完成的这种,按照以下模式做:
- viewmodel 中启动协程
- Repository暴露suspend functions 并保证是主线程安全的
- database 和 network 暴露并真正实现这些方法并保证主线程安全
3. 遗留的一个bug
以上例子是对商品排序,当用户点击得非常非常快的时候,排序有时候就错乱了。
解决的方案:
- 让用户不要点击那么快
- 发起一次请求排序的时候,立即将点击按钮置灰,在finally中将按钮重置可用
private fun sortPricesBy(ascending: Boolean) {
viewModelScope.launch {
// disable the sort buttons whenever a sort is running
_sortButtonsEnabled.value = false
try {
_sortedProducts.value =
productsRepository.loadSortedProducts(ascending)
} finally {
// re-enable the sort buttons after the sort is complete
_sortButtonsEnabled.value = true
}
}
}
- 并发模式解决:
为了保证多次快速点击的时候, 只能有一次请求进行,可以采用如下三种思想:
(1) 开始一次请求之前,取消上一次的请求
作者封装了一个ControlledRunner,可以cancelPreviousThenRun
class ProductsRepository(val productsDao: ProductsDao, val productsApi: ProductsService) {
var controlledRunner = ControlledRunner<List<ProductListing>>()
suspend fun loadSortedProducts(ascending: Boolean): List<ProductListing> {
// cancel the previous sorts before starting a new one
return controlledRunner.cancelPreviousThenRun {
if (ascending) {
productsDao.loadProductsByDateStockedAscending()
} else {
productsDao.loadProductsByDateStockedDescending()
}
}
}
}
它底层实现就是:
activeTask?.cancelAndJoin()
(2)排队思想
作者封装了一个SingleRunner,使用锁机制,在同一时刻,拿到锁的才可以执行,等执行完毕,释放锁,下一个请求才可以进行
class ProductsRepository(val productsDao: ProductsDao, val productsApi: ProductsService) {
val singleRunner = SingleRunner()
suspend fun loadSortedProducts(ascending: Boolean): List<ProductListing> {
// wait for the previous sort to complete before starting a new one
return singleRunner.afterPrevious {
if (ascending) {
productsDao.loadProductsByDateStockedAscending()
} else {
productsDao.loadProductsByDateStockedDescending()
}
}
}
}
(3)让之前的请求完毕,而不是执行新的请求
也还是在ControlledRunner 中调用joinPreviousOrRun即可。
这和第一种不同之处在于不会丢弃旧的请求,只会丢弃新的请求
class ProductsRepository(val productsDao: ProductsDao, val productsApi: ProductsService) {
var controlledRunner = ControlledRunner<List<ProductListing>>()
suspend fun fetchProductsFromBackend(): List<ProductListing> {
// if there's already a request running, return the result from the
// existing request. If not, start a new request by running the block.
return controlledRunner.joinPreviousOrRun {
val result = productsApi.getProducts()
productsDao.insertAll(result)
result
}
}
}
joinPreviousOrRun底层实现就是调用await
activeTask?.let {
return it.await()
}