【问题标题】:How to wait for multiple jobs in Android ViewModel?如何在 Android ViewModel 中等待多个作业?
【发布时间】:2020-02-16 21:19:44
【问题描述】:

给定以下组件

data class Account(val name: String)

data class GetAccountRequest(val name: String)

@Dao
interface AccountDao {
    @Query("SELECT * FROM accounts ORDER BY name ASC")
    fun all(): LiveData<List<Account>>
}

interface AccountOperations {
    @GET("/foo/account")
    suspend fun getAccount(@Body request: GetAccountRequest): Account
}

class AccountRepository(private val dao: AccountDao, private val api: AccountOperations) {
    val accounts: LiveData<List<Account>> = dao.all()

    suspend fun refresh(name: String) {
        val account = api.getAccount(GetAccountRequest(name))
        dao.insert(account)
    }
}

我正在开发一个使用这些组件的 Android 应用程序(由 Room 支持数据库,Retrofit 支持 API 访问)。

在我的 ViewModel 中,我维护了一个列出所有帐户的 RecyclerView。我允许用户手动刷新该列表。相应的(部分)ViewModel 如下所示:

fun refresh() {
    viewModelScope.launch {
        repository.accounts.value?.forEach {
            launch { repository.refresh(it.name) }
        }
    }
    Timber.i("Done refreshing!")
}

我确实希望刷新以并行更新所有帐户,这就是我使用launch 的原因。我还决定在 ViewModel 中而不是在存储库中执行此操作,因为这需要在存储库中启动一个新的协程。不鼓励使用 this post,因为存储库没有自然的生命周期。

上面的函数refresh 是从 UI 调用的,并在更新 RecyclerView 时显示一个刷新指示符。所以我想在所有帐户都更新后停止这个指标。

上面显示的我的代码没有这样做,因为它会启动所有更新,然后在所有更新完成之前打印日志语句。结果,尽管仍有更新,但刷新指示器消失了。

所以我的问题(最后)是:我如何重构代码以使其并行运行所有更新,但确保 refresh 在所有更新完成之前不会返回?

编辑#1

回到我想要实现的目标:在视图更新时显示刷新指示器,我想出了以下内容(更改了 ViewModel 中的 refresh 函数):

fun refresh() {
        viewModelScope.launch {
            try {
                coroutineScope {
                    _refreshing.value = true
                    repository.accounts.value?.map { account ->
                        async {
                            repository.refresh(account.name)
                        }
                    }
                }
            } catch (cause: CancellationException) {
                throw cause
            } catch (cause: Exception) {
                Timber.e(cause)
            } finally {
                _refreshing.value = false
            }
        }
    }

ViewModel 在刷新时公开一个 LiveData,片段可以观察它以显示或隐藏微调器。这似乎可以解决问题。但是,它仍然感觉不对,我感谢任何改进的解决方案。

【问题讨论】:

  • async 用于并行性,而不是结构化并发

标签: android kotlin android-architecture-components coroutine kotlin-coroutines


【解决方案1】:

为了对所有并行的refresh() 操作使用await,只需使用awaitAll()

coroutineScope.launch {
            _refreshing.value = true

            repository.accounts.value?.map { account ->
                async {
                    repository.refresh(account.name)
                }
            }.awaitAll()

            _refreshing.value = false
        } 

此外,不建议使用try/catch 包装协程。 你可以阅读更多关于here的内容。

【讨论】:

  • 谢谢。我已经通读了这篇文章,但找不到其中说不鼓励使用 try/catch 包装协程的部分。你能详细说明一下吗?另外,在这种情况下,您还会如何处理异常?我在示例中使用了coroutineScope,我认为我不需要调用awaitAll(),因为这似乎是自动发生的。
  • 没问题@aix,:) 那篇文章的第一部分,在介绍建议用try/catch 包装协程会带来几个后果之后,在你的情况下,你通过抛出CancellationException 捕捉到它之后(前提是你的范围有SupervisorJob),那么你在这里是安全的。回答您的第二个问题,为了在根级别捕获所有异常,我会将CoroutineExceptionHandler 添加到我的根CoroutineContext
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-04-17
  • 1970-01-01
  • 1970-01-01
  • 2020-10-26
  • 1970-01-01
  • 1970-01-01
  • 2021-12-01
相关资源
最近更新 更多