How to wait for multiple jobs in Android ViewModel? - android

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.

In order to await for all of your parallel refresh() operations, simply use awaitAll():
coroutineScope.launch {
_refreshing.value = true
repository.accounts.value?.map { account ->
async {
repository.refresh(account.name)
}
}.awaitAll()
_refreshing.value = false
}
Furthermore, It's not advised to wrap coroutines with try/catch.
You can read more on this here.

Related

Emitting ui state while collecting does not update ui

This init block is in my ViewModel:
init {
viewModelScope.launch {
userRepository.login()
userRepository.user.collect {
_uiState.value = UiState.Success(it)
}
}
}
This is very similar to what's actually written on the app, but even this simple example doesn't work. After userRepository.login(), user which is a SharedFlow emits a new user state. This latest value DOES get collected within this collect function shown above, but when emitting a new uiState containing the result, the view does not get such update.
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Doing this for some reason, does not work. I suspect the issue is related to the lifecycle of the viewmodel, because when I treat the viewmodel as a singleton, this doesn't happen. It happens only when the viewmodel gets destroyed and then created a 2nd (or more) time(s).
What I'm trying to achieve is that the screen containing the view model is aware of the user state. Meaning that when I navigate to the screen, I want it to collect the latest user state, and then decide which content to show.
I also realize this is not the best pattern, most likely. I'm currently looking into a solution that holds the User as part of the app state and collecting per screen (given that it basically changes all or many screens and functionalities) so if you have any resources on an example on such implementation I'd be thankful. But I can't get my head around why this current implementation doesn't work so any light shed on the situation is much appreciated.
EDIT
This is what I have in mind for the repository
private val _user = MutableSharedFlow<User>()
override val user: Flow<User> = _user
override suspend fun login() {
delay(2000)
_user.emit(LoggedUser.aLoggedUser())
}
override suspend fun logout() {
delay(2000)
_user.emit(GuestUser)
}
For your case better to use this pattern:
ViewModel class:
sealed interface UserUiState {
object NotLoggedIn : UserUiState
object Error : UserUiState
data class LoggedIn(val user: User) : UserUiState
}
class MyViewModel #Inject constructor(
userRepository: UserRepository
) : ViewModel() {
val userUiState = userRepository.login()
.map { user ->
if (user != null)
UserUiState.LoggedIn(user)
else
UserUiState.Error
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = UserUiState.NotLoggedIn
)
}
Repository class:
class UserRepository {
fun login(): Flow<User?> = flow {
val user = TODO("Your code to get user")
if (isSuccess) {
emit(user)
} else {
emit(null)
}
}
}
Your screen Composable:
#Composable
fun Screen() {
val userUiState by viewModel.userUiState.collectAsStateWithLifecycle()
when (userUiState) {
is UserUiState.LoggedIn -> { TODO("Success code") }
UserUiState.NotLoggedIn -> { TODO("Waiting for login code") }
UserUiState.Error -> { TODO("Error display code") }
}
}
How it works: login() in repository returns autorized user flow which can be used in ViewModel. I use UserUiState sealed class to handle possible user states. And then I convert User value in map {} to UserUiState to display it in the UI Layer. Then Flow of UserUiState needs to be converted to StateFlow to obtain it from the Composable function, so I made stateIn.
And of course, this will solve your problem
Tell me in the comments if I got something wrong or if the code does not meet your expectations
Note: SharedFlow and StateFlow are not used in the Data Layer like you do.
EDIT:
You can emiting flow like this if you are working with network:
val user = flow of {
while (true) {
// network call to get user
delay(2000)
}
}
If you use Room you can do this in your dao.
#Query(TODO("get actual user query"))
fun getUser(): Flow<User>
It is a better way and it recommended by android developers YouTube channel

Kotlin's StateFlow.asLiveData() isn't updating observers when it changes quickly

I have a ViewModel that uses StateFlow.asLiveData() to expose a Repository class's StateFlow items and I'm trying to write a test for the ViewModel. My tests are configured with a Mock of an Observer<LoadingStatus> on the ViewModel's exposed LiveData.
The code I'm testing calls this method to update its loading status:
suspend fun MutableStateFlow<LoadingStatus>.performWithStatusUpdates(operation: suspend () -> Unit) {
this.value = LoadingStatus.Loading()
try {
operation.invoke()
this.value = LoadingStatus.Success()
} catch (e: Throwable) {
this.value = LoadingStatus.Error(e)
}
}
My tests look something like this:
fun testSomething() = runTest {
viewModel.doSomething()
advanceUntilIdle()
argumentCaptor<LoadingStatus>().apply {
verify(loadingStatusObserver, atLeast(2)).onChanged(capture())
assertTrue(allValues.any { it is LoadingStatus.Loading })
assertTrue(allValues.any { it is LoadingStatus.Success })
}
}
The ViewModel contains code like this:
val loadingStatus = repository.loadingStatusObservable.asLiveData(
viewModelScope.coroutineContext + ioDispatcher
) // when running tests, ioDispatcher is a StandardTestDispatcher passed into the viewModel
fun doSomething() {
viewModelScope.launch(ioDispatcher) {
repository.doSomething()
}
}
And the repository does something like this:
val loadingStatusObservable = MutableStateFlow<LoadingStatus>(LoadingStatus.Idle())
suspend fun doSomething() {
loadingStatusObservable.performWithStatusUpdates {
apiService.doSomethingElse()
}
}
The repository has similar tests that call doSomething() and verify that the status goes to loading and then success, and they pass, but the view model ones fail to pick up the Loading status. If I comment out the line in performWithStatusUpdates that sets the status to success after the operation, the tests do pick up the Loading status, so I'm convinced it's something to do with timing. I have run this code with print statements and debuggers and verified that the status is updating correctly, but the change isn't getting picked up by the observers.
How can I make an observer on a StateFlow.asLiveData() detect all changes, even when they're quickly followed by another, different change?

How to clear repository cache when the user logs out?

I have an repository that contains an in-memory cache list inside a StateFlow. The problem is that whenever the user logs out and logs into another account, the old data from the previous user is still there.
object Repository {
private lateinit var remoteDataSource: RemoteDataSource
operator fun invoke(remoteDataSource: remoteDataSource) {
this.remoteDataSource = remoteDataSource
return this
}
private val myList = MutableStateFlow(listOf<myData>())
suspend fun getData(): Flow<List<myData>> =
withContext(Dispatchers.IO) {
if (myList.value.isEmpty()) {
val response = remoteDataSource.getData()
if (response != null) {
myList.value = response.map { it.toMyData() }
}
}
myList
}
suspend fun addData(newData: MyData) =
withContext(Dispatchers.IO) {
myList.value = myList.value.plus(newData)
remoteDataSource.addData(myData.toMyDataRequest())
}
}
This repository is used by multiple ViewModels. The list itself is only observed by one screen (let's call it myFragment), but other screens can add new elements to it. I've tried to clear the repository on myFragment's onDestroyView, but it clears the list whenever the user navigates away from myFragment (even when it's not a logout).
We could observe whenever the user logs out in an userRepository, but i don't know how to observe data in one repository from another repository (there's nothing like viewModelScope.launch to collect flows or something like that).
What approach can be used to solve this? And how would it clear the list?
i don't know how to observe data in one repository from another repository
I'd argue you shouldn't in this case.
You have a use-case: Logout.
When you invoke this use-case, you should perform al the necessary operations that your app requires. In this case, you should call your repository to let it know.
suspend fun clearData() =
withContext(Dispatchers.IO) {
// clear your data
}
I'd argue that you shouldn't hardcode the Dispatcher, since you'll likely test this at some point; in your tests you're going to use TestDispatcher or similar, and if you hardcode it, it will be harder to test. You write tests, right?
So now your use case..
class LogoutUseCase(repo: YourRepo) {
operator fun invoke() {
repo.clearData()
//do the logout
}
}
That's how I would think about this.
Your scope for all this is the UI that initiated the logout...

Kotlin Coroutine Flow: When does wasting resource happen when using Flow

I am reading this article to fully understand the dos and donts of using Flow while comparing it to my implementation, but I can't grasp clearly how to tell if you are wasting resource when using Flow or flow builder. When is the time a flow is being release/freed in memory and when is the time that you are wasting resource like accidentally creating multiple instances of flow and not releasing them?
I have a UseCase class that invokes a repository function that returns Flow. In my ViewModel this is how it looks like.
class AssetViewModel constructor(private val getAssetsUseCase: GetAssetsUseCase) : BaseViewModel() {
private var job: Job? = null
private val _assetState = defaultMutableSharedFlow<AssetState>()
fun getAssetState() = _assetState.asSharedFlow()
init {
job = viewModelScope.launch {
while(true) {
if (lifecycleState == LifeCycleState.ON_START || lifecycleState == LifeCycleState.ON_RESUME)
fetchAssets()
delay(10_000)
}
}
}
fun fetchAssets() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
getAssetsUseCase(
AppConfigs.ASSET_BASE_URL,
AppConfigs.ASSET_PARAMS,
AppConfigs.ASSET_SIZES[AppConfigs.ASSET_LIMIT_INDEX]
).onEach {
when(it){
is RequestStatus.Loading -> {
_assetState.tryEmit(AssetState.FetchLoading)
}
is RequestStatus.Success -> {
_assetState.tryEmit(AssetState.FetchSuccess(it.data.assetDataDomain))
}
is RequestStatus.Failed -> {
_assetState.tryEmit(AssetState.FetchFailed(it.message))
}
}
}.collect()
}
}
}
override fun onCleared() {
job?.cancel()
super.onCleared()
}
}
The idea here is we are fetching data from remote every 10 seconds while also allowing on demand fetch of data via UI.
Just a typical useless UseCase class
class GetAssetsUseCase #Inject constructor(
private val repository: AssetsRepository // Passing interface not implementation for fake test
) {
operator fun invoke(baseUrl: String, query: String, limit: String): Flow<RequestStatus<AssetDomain>> {
return repository.fetchAssets(baseUrl, query, limit)
}
}
The concrete implementation of repository
class AssetsRepositoryImpl constructor(
private val service: CryptoService,
private val mapper: AssetDtoMapper
) : AssetsRepository {
override fun fetchAssets(
baseUrl: String,
query: String,
limit: String
) = flow {
try {
emit(RequestStatus.Loading())
val domainModel = mapper.mapToDomainModel(
service.getAssetItems(
baseUrl,
query,
limit
)
)
emit(RequestStatus.Success(domainModel))
} catch (e: HttpException) {
emit(RequestStatus.Failed(e))
} catch (e: IOException) {
emit(RequestStatus.Failed(e))
}
}
}
After reading this article which says that using stateIn or sharedIn will improve the performance when using a flow, it seems that I am creating new instances of the same flow on-demand. But there is a limitation as the stated approach only works for variable and not function that returns Flow.
stateIn and shareIn can save resources if there are multiple observers, by avoiding redundant fetching. And in your case, you could set it up to automatically pause the automatic re-fetching when there are no observers. If, on the UI side you use repeatOnLifecycle, then it will automatically drop your observers when the view is off screen and then you will avoid wasted fetches the user will never see.
I think it’s not often described this way, but often the multiple observers are just observers coming from the same Activity or Fragment class after screen rotations or rapidly switching between fragments. If you use WhileSubscribed with a timeout to account for this, you can avoid having to restart your flow if it’s needed again quickly.
Currently you emit to from an external coroutine instead of using shareIn, so there’s no opportunity to pause execution.
I haven't tried to create something that supports both automatic and manual refetching. Here's a possible strategy, but I haven't tested it.
private val refreshRequest = Channel<Unit>(Channel.CONFLATED)
fun fetchAssets() {
refreshRequest.trySend(Unit)
}
val assetState = flow {
while(true) {
getAssetsUseCase(
AppConfigs.ASSET_BASE_URL,
AppConfigs.ASSET_PARAMS,
AppConfigs.ASSET_SIZES[AppConfigs.ASSET_LIMIT_INDEX]
).map {
when(it){
is RequestStatus.Loading -> AssetState.FetchLoading
is RequestStatus.Success -> AssetState.FetchSuccess(it.data.assetDataDomain)
is RequestStatus.Failed -> AssetState.FetchFailed(it.message)
}
}.emitAll()
withTimeoutOrNull(100L) {
// drop any immediate or pending manual request
refreshRequest.receive()
}
// Wait until a fetch is manually requested or ten seconds pass:
withTimeoutOrNull(10000L - 100L) {
refreshRequest.receive()
}
}
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(4000L), replay = 1)
To this I would recommend not using flow as the return type of the usecase function and the api call must not be wrapped inside a flow builder.
Why:
The api call actually is happening once and then again after an interval it is triggered by the view model itself, returning flow from the api caller function will be a bad usage of powerful tool that is actually meant to be called once and then it must be self-reliant, it should emit or pump in the data till the moment it has a subscriber/collector.
One usecase you can consider when using flow as return type from the room db query call, it is called only once and then the room emits data into it till the time it has subscriber.
.....
fun fetchAssets() {
viewModelScope.launch {
// loading true
val result=getusecase(.....)
when(result){..process result and emit on state..}
// loading false
}
}
.....
suspend operator fun invoke(....):RequestStatus<AssetDomain>{
repository.fetchAssets(baseUrl, query, limit)
}
.....
override fun fetchAssets(
baseUrl: String,
query: String,
limit: String
):RequestStatus {
try {
//RequestStatus.Loading()//this can be managed in viewmodel itself
val domainModel = mapper.mapToDomainModel(
service.getAssetItems(
baseUrl,
query,
limit
)
)
RequestStatus.Success(domainModel)
} catch (e: HttpException) {
RequestStatus.Failed(e)
} catch (e: IOException) {
RequestStatus.Failed(e)
}
}

Emit coroutine Flow from Room while backfilling via network request

I have my architecture like so:
Dao methods returning Flow<T>:
#Query("SELECT * FROM table WHERE id = :id")
fun itemById(id: Int): Flow<Item>
Repository layer returning items from DB but also backfilling from network:
(* Need help here -- this is not working as intended **)
fun items(): Flow<Item> = flow {
// Immediately emit values from DB
emitAll(itemDao.itemById(1))
// Backfill DB via network request without blocking coroutine
itemApi.makeRequest()
.also { insert(it) }
}
ViewModel layer taking the flow, applying any transformations, and converting it into a LiveData using .asLiveData():
fun observeItem(): LiveData<Item> = itemRepository.getItemFlow()
.map { // apply transformation to view model }
.asLiveData()
Fragment observing LiveData emissions and updating UI:
viewModel.item().observeNotNull(viewLifecycleOwner) {
renderUI(it)
}
The issue I'm having is at step 2. I can't seem to figure out a way to structure the logic so that I can emit the items from Flow immediately, but also perform the network fetch without waiting.
Since the fetch from network logic is in the same suspend function it'll wait for the network request to finish before emitting the results downstream. But I just want to fire that request independently since I'm not interested in waiting for a result (when it comes back, it'll update Room and I'll get the results naturally).
Any thoughts?
EDIT
Marko's solution works well for me, but I did attempt a similar approach like so:
suspend fun items(): Flow<List<Cryptocurrency>> = coroutineScope {
launch {
itemApi.makeRequest().also { insert(it) }
}
itemDao.itemById(1)
}
It sounds like you're describing a background task that you want to launch. For that you need access to your coroutine scope, so items() should be an extension function on CoroutineScope:
fun CoroutineScope.items(): Flow<Item> {
launch {
itemApi.makeRequest().also { insert(it) }
}
return flow {
emitAll(itemDao.itemById(1))
}
}
On the other hand, if you'd like to start a remote fetch whose result will also become a part of the response, you can do it as follows:
fun items(): Flow<Item> = flow {
coroutineScope {
val lateItem = async { itemApi.makeRequest().also { insert(it) } }
emitAll(itemDao.itemById(1))
emit(lateItem.await())
}
}

Categories

Resources