Search code examples
androidandroid-livedataandroid-architecture-componentsandroid-viewmodel

How to react on changes within a UiModel when it's exposed via LiveData by the ViewModel?


I watched this awesome talk by Florina Muntenescu on KontlinConf 2018 where she talked about how they reshaped their app architecture.

One part of the talk was how they expose a UiModel (not ViewModel) via LiveData from the ViewModel. (watch here)

She made a example similar to this:

class MyViewModel constructor(...) : ViewModel() {

    private val _uiModel = MutableLiveData<UiModel>()
    val uiModel: LiveData<UiModel>
        get() = _uiModel
}

A view declaration for the ViewModel above could be:

<layout>
    <data>
        <variable
            name="viewModel"
            type="com.demo.ui.MyViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout>

        <EditText
            android:id="@+id/text"
            android:text="@={viewModel.uiModel.text}" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

She didn't talked about (or I missed it) how they react to property changes within the UiModel itself. How can I execute a function everytime text changes?

When having the text in separate LiveData property within the ViewModel I could use MediatorLiveData for this like:

myMediatorLiveData.addSource(text){
   // do something when text changed
}

But when using the approach above the UiModel does not change instead the values of it are changed. So this here doesn't work:

myMediatorLiveData.addSource(uiModel){
   // do something when text inside uiModel changed
}

So my question is how can I react on changes inside a UiModel in the ViewModel with this approach?

Thanks for advice, Chris


Solution

  • I want to summarize my research regarding the topic above.

    As @CommonsWars said in the comments above you can implement the field in the UiModel as ObservableFields. But after some hands on I currently prefer the approach described here.

    This led me to the following code:

    ViewModel:

    class MyViewModel constructor(...) : ViewModel() {
       val uiModel = liveData{
           val UiModel uiModel = UiModel() // get the model from where ever you want
           emit(uiModel)
       }
    
       fun doSomething(){
          uiModel.value!!.username = "abc"
       }
    }
    

    UiModel

    class UiModel : BaseObservable() {
    
       @get:Bindable // puts '@Bindable' on the getter
       var text = ""
          set(value) {
             field = value
             notifyPropertyChanged(BR.text) // trigger binding
          }
    
       val isValid: Boolean
       @Bindable("text") get() { // declare 'text' as a dependency
          return !text.isBlank()
       }
    }
    

    Layout

    <layout>
        <data>
            <variable
                name="viewModel"
                type="com.demo.ui.MyViewModel" />
        </data>
    
        <androidx.constraintlayout.widget.ConstraintLayout>
    
            <EditText
                android:id="@+id/text"
                android:text="@={viewModel.uiModel.text}" />
    
            <Button
                android:id="@+id/sent_btn"
                android:enabled="@{viewModel.uiModel.isValid}" />
    
        </androidx.constraintlayout.widget.ConstraintLayout>
    </layout>
    

    I've chosen this approach because of the way we change properties of the UiModel from within a ViewModel.

    We can you can set/get the username in the ViewModel by:

    fun doSomething(){
       uiModel.value!!.username = "abc"
    }
    
    fun doSomething(){
       uiModel.value!!.username
    }
    

    When you implementing it by ObservableFields you have to set/get the username by:

    fun doSomething(){
       uiModel.value!!.username.set("abc")
    }
    
    fun doSomething(){
       uiModel.value!!.username.get()
    }
    

    You can pick the approach which suites best for your needs! Hope this helps someone.

    Chris