We are integrating flow in out Kotlin Native proje...
# coroutines
j
We are integrating flow in out Kotlin Native project to implement viewmodels in the common module so those viewmodels can be shared between iOS and Android. At the moment we have started using flow for this which seems to fit nicely in the overall architecture. A viewmodel can expose a flow to which the UI can collect to. The UI can also indicate it wants new data (refresh). I have implemented this in the following way, by using a MutableStateFlow indicating the refresh status. I have a simple example implementation ((with documentation)) below which seems to work quite well. Is this the correct approach to this problem with Kotlin flows?
Copy code
@ExperimentalCoroutinesApi
class PersonsViewModel {
    private val realApi = RealApi()

    // cached list of persons
    private var cachedPersons: List<Person>? = null

    // indicates of the data should be retrieved from network or cache. This is a MutableStateFlow since its value
    // can be updated by the UI (for example uses refreshes the page).
    private val refreshTrigger: MutableStateFlow<RefreshTrigger> = MutableStateFlow(RefreshTrigger.FromCache())

    // flatMapLatest on refreshTrigger because we are interested in the latest value only.
    var persons: Flow<CommonDataContainer<List<Person>>> = refreshTrigger.flatMapLatest { trigger ->
        flow<CommonDataContainer<List<Person>>> {
            // Notify the UI we are loading data
            emit(CommonDataContainer.Loading())
            val refresh = trigger is RefreshTrigger.Refresh
            val cached = cachedPersons
            if (!refresh && cached != null) {
//                Return the cached data if not explicitly refreshed and data is in cache.
                emit(CommonDataContainer.Success(cached))
            } else {
                // Refresh or data is not in cache, retrieve from network.
                retrievePersonsFromNetwork()
            }
        }
    }

    private suspend fun FlowCollector<CommonDataContainer<List<Person>>>.retrievePersonsFromNetwork() {
        val response = realApi.retrievePersons()
        when (response) {
            is Success -> {
                cachedPersons = response.data
                // Emit the data.
                emit(CommonDataContainer.Success(response.data))
            }
            // Something went wrong, emit a failure.
            is Failure -> emit(CommonDataContainer.Failure())
        }
    }

    suspend fun refresh() {
        yield()
        refreshTrigger.value = RefreshTrigger.Refresh()
    }
}

// CommonDataContainer can represent various states which are interesting to collectors.
sealed class CommonDataContainer<out T> {
    // Indicates the data is loading
    class Loading<T> : CommonDataContainer<T>()
    // Indicates an error occurred retrieving the data
    class Failure<T> : CommonDataContainer<T>()
    // Indicates success. Holds the data in the data property.
    class Success<out T>(val data: T) : CommonDataContainer<T>()
}

// Indicates where the data should com from. Both classes  have a unique id to make sure the Refresh trigger is always emitted by MutableStateFlow
// since MutableStateFLow does not emit the value if the new value is the same as the current value.
sealed class RefreshTrigger {
    data class FromCache(private val id: String = Random.nextInt().toString()) : RefreshTrigger()

    /**
     * Every instance has a unique id so the refresh StateFlow sees it as a new value. StateFlow does not emit the same value twice if
     * the current value is the same as the new value.
     */
    data class Refresh(private val id: String = Random.nextInt().toString()) : RefreshTrigger()
}
i
Just a few thoughts: 1. caching logic should be in repository 2. Flow is not lifecycle-aware afaik, so you need to manually unsubscribe in fragments/activities 3. you don't have any error handling - it just returns a generic error. list of possible errors is quite wide, you can refer to sample of our implementation: https://github.com/ildar2/MySandbox/blob/master/app/src/main/java/kz/ildar/sandbox/data/ApiCaller.kt - I'd recommend to add some tests as well
j
The sample served indeed as a simplified example demonstrating the refresh logic (with caching in the viewmodel and error handling as simple as possible). Caching is indeed on the repository level and the viewmodel in Android bridges, with viewModelScope, is responsible for collecting to this flow (so it is cancelled when the onCleared of the viewModel is called). The Android viewmodel acts as a bridge between Android UI and the viewmodel in common. My question was more about the use of the MutableStateFlow and the refresh handling. Thanks for your example!
d
I have a question for my own ignorance ... where is the flowOn or equivalent to migrate the flow to the main UI thread -- or is that outside of this class?