Reputation: 175
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
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
:
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>>
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