I need to have some data loaded from a database in my View Model when is it created. Since the data comes in the form of a Flow, I'm passing it to a lateinit variable using collect. However, I need to further process the data after it is loaded in it, but I don't know how to wait for the data to actually be loaded.
This what I have so far:
class MyViewModel: ViewModel() {
...
private var _state = MutableStateFlow(MyState())
var state = _state.asStateFlow()
private val myDataSource: MyDataSource
lateinit var loadedData: List<DataType>
init {
viewModelScope.launch {
dataLoader()
}
doSomething(loadedData)
}
suspend fun dataLoader(){
dataSource.getData().collect {
loadedData = it
}
}
fun doSomething(loadedData: List<DataType>){
_state.value = _state.value.copy(thing = newValueFromLoadedData)
}
}
and obviously loadedData is not initialized.
Also, please let me know if this not the right way of doing what I want to do...
CodePudding user response:
You shouldn't be collecting in your ViewModel at all - that causes your ViewModel to continue to process data even when your UI isn't present (i.e., when your screen goes onto the back stack or the user hits the Home button). Instead, you want to use map
to doSomething
on each element in your list, transforming it into the state you want to provide to your UI, and use stateIn
to make the Flow
into a StateFlow
your UI can consume.
class MyViewModel: ViewModel() {
val state = qaDataSource.getData().map { loadedData ->
val newState = doSomething(loadedData)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000))
}
If you have multiple sources of data (i.e., multiple Flow
s) that coordinate to build up your total state, then you'd want to use combine
to tie them all together, thus allowing you to build your state from all of them, re-emitting a new state whenever any of them change:
class MyViewModel: ViewModel() {
private val qaDataFlow = qaDataSource.getData()
private val secondDataFlow = //
combine(qaDataFlow, secondDataFlow).map { qaLoadedData, secondLoadedData ->
val newState = doSomething(qaLoadedData, secondLoadedData)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000))
}
This means your ViewModel isn't doing anything unless your UI is actually present and is transforming each change from your data sources into new data for your UI without collecting in the ViewModel.
CodePudding user response:
You can simply use .onCompletion
event.
According to docs
onCompletion event returns a flow that invokes the given action after the flow is completed or cancelled, passing the cancellation exception or failure as cause parameter of action. Conceptually, onCompletion is similar to wrapping the flow collection into a finallyblock,
for example the following imperative snippet:
try {
myFlow.collect { value ->
println(value)
}
} finally {
println("Done")
}
In your case, you can do the following
class MyViewModel: ViewModel() {
...
private val myDataSource: QADataSource
lateinit var loadedData: List<DataType>
init {
viewModelScope.launch {
qaDataSource.getData()
.onCompletion {
doSomething(loadedData)
}
.collect {
loadedData = it
}
}
}
fun doSomething(loadedData: List<DataType>){
...
}
}
.onCompletion
event will trigger once your flow is completed or cancelled.