https://kotlinlang.org logo
Title
m

Marc Knaup

08/12/2019, 9:15 PM
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

Dominaezzz

08/12/2019, 9:18 PM
What's wrong with
MutableLiveData<...>()
? Or just
liveData { ... }
?
l

louiscad

08/12/2019, 9:28 PM
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

Marc Knaup

08/12/2019, 9:52 PM
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

Mark Murphy

08/12/2019, 10:20 PM
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

Adam Powell

08/13/2019, 1:36 AM
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

Marc Knaup

08/13/2019, 3:42 AM
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

Melih Aksoy

08/13/2019, 9:13 AM
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

Marc Knaup

08/13/2019, 11:49 AM
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

Melih Aksoy

08/13/2019, 12:55 PM
It’s more using DB data as source to others that decorate UI but not vice-versa. A rough eg
//Assume repo.getData() returns liveData { ... }
class MyViewModel(private val repo: Repository) : ViewModel() {

    val textData: LiveData<Data>
        get() = repo.getData().map {
            it.textToDisplay
        }
}
m

Marc Knaup

08/13/2019, 2:19 PM
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

Adam Powell

08/13/2019, 2:22 PM
what's your source of truth? does the DB get updated by changes you're making via the ViewModel?
m

Marc Knaup

08/13/2019, 2:22 PM
No, the DB won’t get updated until the Activity decides to persist the data.
a

Adam Powell

08/13/2019, 2:23 PM
so in this case the DB is initial state, but it's still observable?
m

Marc Knaup

08/13/2019, 2:26 PM
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

Adam Powell

08/13/2019, 2:27 PM
hence your question right after this in the channel 🙂
m

Marc Knaup

08/13/2019, 2:28 PM
exactly 😄
a

Adam Powell

08/13/2019, 2:31 PM
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

Marc Knaup

08/13/2019, 2:34 PM
I don’t yet. I thought about blocking the UI. But the editing controls won’t be visible without the initial data anyway.
a

Adam Powell

08/13/2019, 2:34 PM
alright, let's assume you're unlocking the editing controls after this LD emits for the first time
alright, maybe something like:
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

Marc Knaup

08/13/2019, 2:40 PM
That can probably do it 🤔 Would I have to close the channel at the end once done?
l

louiscad

08/13/2019, 2:40 PM
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

Adam Powell

08/13/2019, 2:40 PM
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

louiscad

08/13/2019, 2:41 PM
Not needed since the ViewModel lives longer than the UI (the hot source), and eventually, they're all garbage collected.
a

Adam Powell

08/13/2019, 2:41 PM
(oops, one more edit to the snippet since I messed up `==
/
!=` 😅 for the is-initialized check)
m

Marc Knaup

08/13/2019, 2:44 PM
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