Denis Jung
Denis Jung

Reputation: 175

How to simplify MVVM Repository function using Room and Retrofit

Below is ViewModel class and Repository class of Android MVVM design pattern.
(1) ViewModel calls a function of Repository in Coroutine.
(2) Then Repository checkes data in SQLite.
(3) If there is no data in SQLite, request data using Retrofit.
(4) After retrieve data from Retrofit, the data is saved in SQLite and sent to ViewModel.

It's working correctly, just it looks the hierarchy of coroutine is complicated.(In StateRepository.reqStates())
When I remove coroutine in Repository, SQLite does not work.
If this code can be more simple, please let me know.
Thank you.


class WeatherViewModel @Inject constructor(private val stateRepository: StateRepository) : ViewModel() {
    val ldWeather = MutableLiveData<Weather>()

    // Request Card data list to Repository
    fun reqStates() {
        viewModelScope.launch {
            val res = stateRepository.reqStates()

            withContext(Dispatchers.Main) {
                res?.let { ldStates.postValue(it) }
            }
        }
    }
}


open class StateRepository @Inject constructor(var api: StateApi, private val db: StateDatabase) {

    suspend fun reqStates(): List<State>? {
        return suspendCoroutine { continuation ->
            CoroutineScope(Dispatchers.IO).launch {
                // Search State list in SQLite
                val states = db.stateDao().getAll()
                if(!states.isNullOrEmpty()) {
                    continuation.resume(states)
                } else {
                    resSuspendCoroutine(continuation)
                }
            }
        }
    }

    private fun resSuspendCoroutine(continuation: Continuation<List<State>?>) {
        val call: Call<List<State>> = api.states()
        // Request State list to server when SQLite is empty
        call.enqueue(object : Callback<List<State>> {
            override fun onResponse(call: Call<List<State>>, response: Response<List<State>>) {
                continuation.resume(response.body())
                // Save state list in SQLite
                response.body()?.let { saveStatesInDB(it) }
            }

            override fun onFailure(call: Call<List<State>>, t: Throwable) {
                continuation.resume(null)
            }
        })
    }

    // Save state list in SQLite
    private fun saveStatesInDB(states: List<State>) {
        CoroutineScope(Dispatchers.IO).launch {
            states.forEach { db.stateDao().insert(it) }
        }
    }
}

Upvotes: 0

Views: 53

Answers (1)

ianhanniballake
ianhanniballake

Reputation: 200080

Creating a CoroutineScope is generally considered a bad practice - instead, you want to embrace the idea of structured concurrency - that every coroutine is structured in a way that has a clear parent/child relationship.

Similarly, you want your UI to be reactive to changes - changes in the database should be reflected in the UI directly. That means your Room database should be returning observable queries - a Flow<List<Data>> or LiveData<List<Data>> rather than just a single List<Data> that doesn't change over time as the database changes.

This means there's a few changes you should make on your StateDao and StateApi:

  1. Your StateDao should return a Flow<List<State>> so that it automatically sends updates as your database changes:
@Dao
interface StateDao{
    @Query("SELECT * FROM states")
    fun getStates(): Flow<List<State>>
  1. Your StateApi should be using Retrofit's support for suspend methods instead of using a Call.
@GET("states")
suspend fun states(): List<State>

This allows us to rewrite your code as:

class WeatherViewModel @Inject constructor(
    private val stateRepository: StateRepository
) : ViewModel() {

    // Get your states as a Flow, converting to a LiveData is optional
    val states: Flow<List<State>> = stateRepository.getStates().asLiveData()
}

open class StateRepository @Inject constructor(
    var api: StateApi,
    private val db: StateDatabase
) {

    // Return a Flow that automatically sends new data
    // as the database changes
    fun getStates(): Flow<List<State>> =
        db.stateDao().getStates().onEach { states ->
            // When the database emits an empty set of data, we'll
            // load from the network
            if (states.isEmpty()) {
                val newStates = api.states()
                // By using NonCancellable, we'll ensure the entire set of
                // data is added even if the user leaves the screen the
                // ViewModel is tied to while this is still going
                withContext(NonCancellable) {
                    newStates.forEach { db.stateDao().insert(it) }
                }
            }
        }
}

Generally, you'd want to Schedule network calls with WorkManager so that they are automatically retried even if the network is down, but that would involve just scheduling the work in the isEmpty() block and moving the StateApi call into the Worker and can be done as a separate step.

Upvotes: 1

Related Questions