0

In my ViewModel I have a lateinit var to hold some LiveData. The way this variable is initialized depends on the data and the current date. Can't do it in SQL. This is the ViewModel:

class MainViewModel {
    lateinit var timeStamps: LiveData<List<TimeStamp>>

    init {
        viewModelScope.launch {
            val db = RoomDB.getInstance(application).timeStampDao()
            val lastTimeStamp = db.getLast()
            if (lastTimeStamp == null
                || (lastTimeStamp.instant < setToStartOfDay(Calendar.getInstance()).timeInMillis)
                && lastTimeStamp.action == ACTION.END_WORK) {
                timeStamps = db.getAllAfterLive(Calendar.getInstance().timeInMillis)
            } else {
                db.getLastAction(ACTION.START_WORK)?.let { lastStartWork ->
                    val startOfDay = setToStartOfDay(initCalendar(lastStartWork.instant)).timeInMillis
                    db.getFirstActionAfter(ACTION.START_WORK, startOfDay)?.let {
                        timeStamps = db.getAllAfterLive(it.instant)
                    }
                }
            }

Here I access timeStamps in my Activity:

override fun onCreate(savedInstanceState: Bundle?) {

    viewModel.timeStamps.observe(this) { list -> recordsAdapter.submitList(list) }

This leads to a UninitializedPropertyAccessException: onCreate runs faster than the timeStamps initialization launched in parallel.

I fixed this by introducing another lateinit var for a callback:

class MainViewModel {
    lateinit var timeStamps: LiveData<List<TimeStamp>>
    lateinit var timeStampsInitializedCallback: () -> Unit

    init {
        viewModelScope.launch {
            // inspect the data and initialize timeStamps
            timeStampsInitializedCallback()
        }

which I initialize in onCreate:

override fun onCreate(savedInstanceState: Bundle?) {

    viewModel.timeStampsInitializedCallback = {
        viewModel.timeStamps.observe(this) { list -> recordsAdapter.submitList(list) }
    }

This works, but it introduces a race condition. Should the initialization for timeStamps unexpectedly finish before the callback is initialized, I'd get another UninitializedPropertyAccessException and be back where I started.

How can I improve this code?

user1785730
  • 3,150
  • 4
  • 27
  • 50
  • You shouldn't need to do this anyway - a `LiveData` with no initial value in the constructor is "empty", and anything that `observe`s it won't get a callback until a value is pushed to the `LiveData`. You can just wire it up, and things will react to values and events when they happen. CommonsWare's answer is the typical way to set them up, and it should work for what you're doing? – cactustictacs Jun 10 '22 at 16:26

2 Answers2

2

You can also use liveData builder function:

class MainViewModel {
    val timeStamps: LiveData<List<TimeStamp>> = liveData {
        // inspect the data and initialize timeStamps
        emit(timeStamps) // emit list of TimeStamps
        emitSource(liveData) // emit another LiveData
    }

}

// in Activity

override fun onCreate(savedInstanceState: Bundle?) {
    viewModel.timeStamps.observe(this) { list -> recordsAdapter.submitList(list) }
}

The liveData code block starts executing when LiveData becomes active and is automatically canceled after a configurable timeout when the LiveData becomes inactive.

Sergio
  • 27,326
  • 8
  • 128
  • 149
  • `emit()` expects `List`, not `LiveData>`. I think this is not what I want. I've updated my question to show what I'm actually doing. – user1785730 Jun 10 '22 at 17:11
  • @user1785730 please check my updated answer. You can call `emitSource` function to emit another `LiveData` object. – Sergio Jun 10 '22 at 19:09
0

The simplest option seems like MutableLiveData:

class MainViewModel {
    private val _timeStamps = MutableLiveData<List<TimeStamp>>()
    val timeStamps: LiveData<List<TimeStamp>> = _timeStamps

    init {
        viewModelScope.launch {
            // inspect the data and set a value on _timeStamps
        }

Depending on what the coroutine is doing, there may be other options (e.g., asLiveData() on a Flow, MediatorLiveData).

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • When I set up `timeStamps` this way, will `timeStamps` update observers due to updates in the database? – user1785730 Jun 10 '22 at 16:54
  • @user1785730: I do not know what database you are referring to. But, if you call `_timeStamps.postValue()`, that value will be delivered to active observers. – CommonsWare Jun 10 '22 at 16:58
  • That is not what I want to do. After the initial logic to figure out the data I want `timeStamps` to be wired to the database. I've updated my question with the code of what I'm actually doing. – user1785730 Jun 10 '22 at 17:07
  • There are two cases: in both cases I assign to `timeStamps` LiveData from the database. – user1785730 Jun 10 '22 at 17:08
  • 1
    @user1785730: You could use `MediatorLiveData`. Frankly, having a Room DAO return `LiveData`, in an app where you are already using coroutines, does not seem like a great solution. If you have your Room DAO return `Flow`, you would have more options. – CommonsWare Jun 10 '22 at 17:22