1

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[0])
    }

    suspend fun dataLoader(){
            dataSource.getData().collect {
                loadedData = it
            }
}


    
    fun doSomething(loadedData: List<DataType>){
        _state.value = _state.value.copy(thing = newValueFromLoadedData[0])
    }

}

and obviously loadedData is not initialized.

Also, please let me know if this not the right way of doing what I want to do...

nayriz
  • 353
  • 1
  • 17
  • How are you actually making this data available to your UI after it is processed? Is it also delivered as a flow that changes whenever the `qaDataSource` changes? – ianhanniballake Sep 09 '22 at 04:24
  • @ianhanniballake I'm using MutableStateFlow (please see edits). I've also rename qaDataSource to MyDataSource for consistency. – nayriz Sep 09 '22 at 04:28
  • So `doSomething` creates a new `MyState` object that you put in your `_state`? – ianhanniballake Sep 09 '22 at 04:30
  • It doesn't create a new object, it updates the existing one ( _state.value = _state.value.copy(thing = newValue) – nayriz Sep 09 '22 at 04:47
  • May I suggest you change your way of thinking? Flows are intended for reactive style programming. Challenge whether you need a separate, redundant property that you have to keep in sync with the source of truth in the Flow, instead of just doing your work in the flow collector when it arrives. – Tenfour04 Sep 09 '22 at 04:48
  • @Tenfour04 I very much welcome your suggestion, but you'd need to show me a concrete example as I'm nowhere near your level. – nayriz Sep 09 '22 at 04:52

1 Answers1

4

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),
      MyState() // the default value
    )
}

If you have multiple sources of data (i.e., multiple Flows) 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),
      MyState()
    )
}

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.

ianhanniballake
  • 191,609
  • 30
  • 470
  • 443
  • I don't understand what you are doing: 1) doSomething does not return a variable, it only updates the state 2) when you say "you shouln't be collecting in viewModel", do you mean never as a principle, or in this particular case? The database is not going to emit new values when the UI is in the background 3) When viewModel starts, it needs to initialize the state with a specific value of the loadedData, so am I not supposed to do what you suggested in init{} It's well possible there are huge gaps in my understanding, sorry about that... – nayriz Sep 09 '22 at 05:31
  • 1) Yes, you'd change that method to return your full state object. That's why I talked about having all sources of data go into one combined flow. 2) Never as a principle, yes. It doesn't load now, but if you start to sync with a server and push updates to your device. It is still a non-zero amount of work to keep that machinery spinning forever. 3) No, you don't. Once the UI starts collecting on the `state`, that will happen automatically. – ianhanniballake Sep 09 '22 at 05:37
  • Thanks for 1) and 2) | re 3) Do you mean I should specify the initial value in the UI? I don't see how I can do that in the viewModel without using init{} – nayriz Sep 09 '22 at 05:49
  • The code already sets a default value - it is the last parameter to `stateIn` – ianhanniballake Sep 09 '22 at 13:39