So, for coroutines there’s that nice `val myData =...
# android
m
So, for coroutines there’s that nice
val myData = liveData { … }
extension. How can I achieve the same with
MutableLiveData
? There is no
mutableLiveData { … }
. 1. Load document from DB using a coroutine upon activation (
onActive
). 2. Update view model when Fragment requests changes & save them to DB (also using a coroutine).
d
What's wrong with
MutableLiveData<...>()
? Or just
liveData { ... }
?
l
onActive
is when you enter the
liveData { }
lambda. You can here perform your DB loading, and then loop over a channel for the change requests coming from the Fragment.
m
MutableLiveData()
cannot be passed a coroutine for initialization.
liveData {}
is not mutable directly. I thought about looping over a channel but thought it to be unnecessary complicated for a rather simple use case. I just hope that there is a better solution 😕
m
Presently, the
LiveData
handed to your
liveData { }
code is a
MutableLiveData
, as
CoroutineLiveData
extends
MediatorLiveData
, which is a
MutableLiveData
. See https://androidx.tech/artifacts/lifecycle/lifecycle-livedata-ktx/2.2.0-alpha03-source/androidx/lifecycle/CoroutineLiveData.kt.html. You might consider filing a feature request for
mutableLiveData { }
. What isn't clear to me from the current code is whether the
CoroutineLiveData
will support other things updating it independent of its coroutine. If it does, then
mutableLiveData { }
should be just a clone of
liveData { }
with the
MutableLiveData
type.
a
I'm confused as to why you would want this 🤔
The point of the liveData builder is to give a well defined scope, lifecycle, and single source of truth. Returning MutableLiveData defeats all three
MutableLiveData is more like an Rx Subject or a Guava SettableFuture - it's good for those cases where you have some existing code to adapt into being observable
It's convenient but you lose a lot of the kinds of guarantees and ease of reasoning about it that you gain from the suspending builder
m
So how would I model loading initial data (e.g. a document) from a database (using the view model) while accepting modifications to the document from a fragment through the view model? In both cases the view model implementation is the single source of truth for - the current model of the view. Maybe I’m missing something here.
m
liveData { }
is a scoped builder you can use if you contracted your return type to
LiveData
. Return type could be very well a
Flow
or
T
generic itself. If you separate
LiveData
that you observe for UI from
LiveData
that feeds your VM with data and relate them with
transformations
-
MediatorLiveData
, you won’t need to make it mutable.
m
Do you mean that the VM live data uses two sources, DB & UI? The latter is then another live data which is mutable and contains the changes from the UI and both will be merged?
m
It’s more using DB data as source to others that decorate UI but not vice-versa. A rough eg
Copy code
//Assume repo.getData() returns liveData { ... }
class MyViewModel(private val repo: Repository) : ViewModel() {

    val textData: LiveData<Data>
        get() = repo.getData().map {
            it.textToDisplay
        }
}
m
That example doesn’t allow the UI to change the data in the VM though. I’d need
textToDisplay
be a
MutableLiveData
and use
switchMap
instead of
map
. Then it would be as I’ve written above:
>>Do you mean that the VM live data uses two sources, DB & UI? The latter is then another live data which is mutable and contains the changes from the UI and both will be merged?
a
what's your source of truth? does the DB get updated by changes you're making via the ViewModel?
m
No, the DB won’t get updated until the Activity decides to persist the data.
a
so in this case the DB is initial state, but it's still observable?
m
The DB is initial state only. Afterward that, DB changes aren’t considered anymore - just the UI makes incremental changes. At some later point the result is persisted to DB again (usually when leaving the activity).
a
hence your question right after this in the channel 🙂
m
exactly 😄
a
hang on, looking something up that can probably help here
how do you handle the case where the db query is slower than the user?
if the initial data isn't loaded by the time the user backs out or similar?
or hits a button to make a change?
m
I don’t yet. I thought about blocking the UI. But the editing controls won’t be visible without the initial data anyway.
a
alright, let's assume you're unlocking the editing controls after this LD emits for the first time
alright, maybe something like:
Copy code
val fromUi = Channel<State>(Channel.CONFLATED)
val ld = liveData {
  if (latestValue == null) {
    emit(suspendingLoadDataFromDb())
  }

  for (state in fromUi) {
    emit(state)
  }
}
☝️ 1
sorry, a few edits in there for formatting and to make sure the channel doesn't get cancelled
and then you can use
fromUi.offer(stateFromUi)
as the implementation of whatever ViewModel method you're using to get data in there
m
That can probably do it 🤔 Would I have to close the channel at the end once done?
l
Adam snippet is what I had in mind when I told to use a channel to go from UI (Fragment in your case) to LiveData.
a
only bother closing the channel if you want to signal that the UI can send no more data
which probably isn't worth doing
1
l
Not needed since the ViewModel lives longer than the UI (the hot source), and eventually, they're all garbage collected.
a
(oops, one more edit to the snippet since I messed up `==
/
!=` 😅 for the is-initialized check)
m
Thank you so far @louiscad and @Adam Powell. I think I’ll try that approach 🙏 My next challenge will then be to merge that approach with
SavedStateHandle
😄 Initial load from DB -> load from saved state if present -> incremental updates on UI
👍 1