'Android, MVI pattern + Kotlin Flow + Room + Retrofit, how fit them together?

I'm pretty new in the world of MVI pattern. So I'm trying to understand how fit together all the pieces. I have an app that I structured using MVI pattern (or at least it was what I was meant to do). I have my fragment (I used navigation component but at the moment focus just on one fragment), which is supported by its own ViewModel. Then I have a repository class where all viewmodels retrieve data. Repository has 2 source of data, a web API and a local DB used as cache of data, I used Room for DB management. I tried different approaches to the problem. At the moment I have done in this way: In the DAO I used this instruction to retrieve data from the DB:

@Query("SELECT * FROM Users WHERE idTool=:idTool AND nickname LIKE '%' || :query || '%'")
fun users(idTool: Int, query: String) : Flow<List<User>>

Then in my repository I simple get this query to forward to ViewModels:

fun usersFlow(idTool: Int, query: String) = userDao.users(idTool, query)

In the ViewModel I created two MutableLiveData, coordinated by a MediatorLiveData:

val nicknameQuery = MutableStateFlow("")
private val nicknameQueryFlow = nicknameQuery.flatMapLatest {
    repository.usersFlow(idToolQuery.value, it)
}
val idToolQuery = MutableStateFlow(DEFAULT_TOOL_ID)
private val idToolQueryFlow = idToolQuery.flatMapLatest {
    repository.usersFlow(it, nicknameQuery.value)
}

val users = MediatorLiveData<List<User>>()

init {
    users.addSource(nicknameQueryFlow.asLiveData()) {
        users.value = it
    }
    users.addSource(idToolQueryFlow.asLiveData()) {
        users.value = it
    }

    fetchUsers()
}

In this way, from my fragment, I can simply update nicknameQuery or idToolQuery to have an updated list in my RecyclerView. My first doubt is that in this way the fetch of data from my DB is done 2 times, one time for each mutable, but I'd like to retrieve data just one on the app opening (maybe the solution fro this is just check in the nicknameQuery that current query is different from the passed one, in this way since at the beginning current query is empty and it pass an empty query, it is bypassed).

In the Init method of ViewModel, I also call fetchUsers():

private fun fetchUsers() {
    viewModelScope.launch {
        repository.fetchUsers(DEFAULT_TOOL_ID).collect {
            _dataState.value = it
        }
    }
}

This method checks into the database if there are already cached users with this specific idTool, if not it fetches them from the web and it stores retrieved data into the DB. This is the method inside my repository class:

suspend fun fetchUsers(
    idTool: Int,
    forceRefetch: Boolean = false
): Flow<DataState<List<User>>> = flow {

    try {
        var cachedUser = userDao.users(idTool, "").first()
        val users: List<User>

        if(cachedUser.isEmpty() || forceRefetch) {
            Log.d(TAG, "Retrieve users: from web")
            emit(DataState.Loading)

            withContext(Dispatchers.IO) {
                appJustOpen = false
                val networkUsers =
                    api.getUsers(
                        idTool,
                        "Bearer ${sessionClient.tokens.accessToken.toString()}"
                    )
                users = entityMapper.mapFromEntitiesList(networkUsers)

                userDao.insertList(users)
            }
        } else {
            users = cachedUser
        }

        emit(DataState.Success(users))
    } catch (ex: Exception) {
        emit(DataState.Error(ex))
    }
}

This method checks if I have already users inside the DB with this specific idTool, if not it fetches them from API. It uses a DataState to update the UI, based on the result of the call. During the fetch of data, it emits a Loading state, this shows a progress bar in my fragment. If data is correctly fetched it emits a Success, and the fragment hides the progress bar to shows the recycler view. This is done in the following way. In my ViewModel I have this mutable state

private val _dataState = MutableLiveData<DataState<List<User>>>()
val dataState: LiveData<DataState<List<User>>> get() = _dataState

As you saw above, my fetch method is

private fun fetchUsers() {
    viewModelScope.launch {
        repository.fetchUsers(DEFAULT_TOOL_ID).collect {
            _dataState.value = it
        }
    }
}

And finally in my fragment I have:

userListViewModel.dataState.observe(viewLifecycleOwner, { dataState ->
            when (dataState) {
                is DataState.Success -> {
                    showUserList()
                }
                is DataState.Error -> {
                    Log.e("TEST", dataState.exception.toString())
                    hideLoader()
                    Toast.makeText(activity, "Error retrieving data: ${dataState.exception}", Toast.LENGTH_LONG).show()
                }
                is DataState.Loading -> {
                    showLoader()
                }
                else -> {
                    // Do Nothing in any other case
                }
            }
        })

At this moment Success state takes a list of users, but this list is there from a previous approach, at the moment it is useless since after data is fetched list is inserted into the DB, and I have a Flow to the DB which takes care to update the UI. In this way when I change idTool, when I change query, when I remove a user, the view is always notified Is this approach correct?

Before this, I used another approach. I returned not a flow from my DB but just a List. Then my fetchUsers always returned a DataState<List>, it checked in the DB and if didn't found anything it fetched data from the web and returned that list. This approach caused me some problems, since every time I changed idTool or query, I always had to call fetchUsers method. Even if a user was removed from database, views didn't get notified since I didn't have a direct flow with the DB.



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source