Given the following components
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)
}
}
I am working on an Android application that is using these components (powered by Room for the database and Retrofit for API access).
In my ViewModel I maintain a RecyclerView that lists all accounts. I enable users to refresh that list manually. The respective (part of the) ViewModel looks like this:
fun refresh() {
viewModelScope.launch {
repository.accounts.value?.forEach {
launch { repository.refresh(it.name) }
}
}
Timber.i("Done refreshing!")
}
I do want the refresh to update all accounts in parallel, this is why I am using launch. I have also decided to do this in the ViewModel, rather than in the repository, since that would have required to launch a new coroutine in the repository. Which per this post is discouraged since repositories don't have a natural lifecycle.
The above function, refresh, is invoked from the UI and shows a refresh-indicator while the RecyclerView is updated. So I want to stop this indicator once all accounts have been updated.
My code as shown above doesn't do this, since it will launch all the updates and then print the log statement before all updates have been finished. As a result the refresh-indicator disappears although there are still updates.
So my question (finally) is: how can I refactor the code so that it runs all updates in parallel, but makes sure refresh doesn't return before all of them have finished?
EDIT #1
Going back to what I want to achieve: showing the refresh-indicator while the view is updating, I came up with the following (changed the refresh function in the ViewModel):
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
}
}
}
The ViewModel exposes a LiveData for when it is refreshing and the fragment can observe it to show or hide the spinner. This seems to do the trick. However, it still doesn't feel right and I appreciate any improved solutions.