Kotlin flow network bound resource usage with force refresh and repeating errors

Viewed 561

I am new to Android/Kotlin, and I am making an app, which loads different data from network with Room cache, and decided to use "network bound resource" algorithm (NBR in short), which can be simplified to this:

inline fun <ResultType, RequestType> networkBoundResource(
    crossinline query: () -> Flow<ResultType>,
    crossinline fetch: suspend () -> RequestType,
    crossinline saveFetchResult: suspend (RequestType) -> Unit,
    crossinline shouldFetch: (ResultType) -> Boolean = { true }
) = flow {
    val data = query().first()

    val flow = if (shouldFetch(data)) {
        emit(Resource.Loading(data))

        try {
            saveFetchResult(fetch())
            query().map { Resource.Success(it) }
        } catch (throwable: Throwable) {
            query().map { Resource.Error(throwable, it) }
        }
    } else {
        query().map { Resource.Success(it) }
    }

    emitAll(flow)
}

This is the exact sample from Coding in Flow, very simular to Google's Github Browser implementation with LiveData and many more samples I've seen. Resource class is a simple state+data holder like this:

sealed class Resource<out R> {
    class Success<out T>(val data: T) : Resource<T>()
    class Error<out T>(val exception: Throwable, val data: T? = null) : Resource<T>()
    class Loading<out T>(val data: T? = null) : Resource<T>()
}

But after implementing this in my repository class I've found some problems I could not find solution anywhere:

  1. How do I force refresh data? I've made a separate function which reuses fetch and saveFetchResult implementations to put data from network into DB, but I need to refresh UI loading indicators from both NBR flow collector and from that suspend refresh function, because NBR will never re-emit loading state for me. It works, but feels bad.

  2. On error in NBR it is emitting flow which converts any of the DB updated values to Resource.Error, even those coming after successful refresh, which leads to repeating the same error message in UI even everything was fine on refresh.

  3. When this NBR is reused by multiple use cases how do I protect from being fetched multiple times? Do I need a mutex lock for the whole function?

1 Answers

I think it isn't a good idea to separate your refresh scenario, I think it is better to use the below pattern in your ViewModel that can help you to solve your problem.

@HiltViewModel
class YourViewModel @Inject constructor(
    private val repository: YourRepository
) : ViewModel() {

private val _result = MutableStateFlow<Resource<String>?>(null)
val result = _result.asStateFlow()

private var fetchJob: Job? = null
fun getData() {
    fetchJob?.cancel()
    fetchJob = viewModelScope.launch {
        repository.getData().cancellable().collect{
            _result.emit(it)
        }
    }
  }
}
Related