79

I'm using Android MVVM architecture with LiveData. I have an object like this

public class User {
    private String firstName;
    private String lastName;

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
}

And my view model looks like this

public class InfoViewModel extends AndroidViewModel {
    MutableLiveData<User> user = new MutableLiveData<>();

    public InfoViewModel(@NonNull Application application) {
        super(application);
        User user = new User();
        user.setFirstName("Alireza");
        user.setLastName("Ahmadi");

        this.user.setValue(user);
    }

    public LiveData<User> getUser(){
        return user;
    }

    public void change(){
        user.getValue().setFirstName(user.getValue().getFirstName() + " A ");
    }
}

How can I make sure when some field in user object changes observers get notified? BTW it is important to me to keep this data in the separate object and not use primary values like Strings in my ViewModel.

RonTLV
  • 2,376
  • 2
  • 24
  • 38
Alireza Ahmadi
  • 5,122
  • 7
  • 40
  • 67
  • I wander whta's the difference of the first answer with the second one to this post, the second one looks more easy. – David May 24 '18 at 21:29

7 Answers7

65

I don't think there is any best practice as such recommended by android for this. I would suggest you to use the approach which uses cleaner & less boilerplate code.

If you are using android data binding along with LiveData you can go with the following approach:

Your POJO object would look something like this

public class User extends BaseObservable {
    private String firstName;
    private String lastName;

    @Bindable
    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
        notifyPropertyChanged(BR.firstName);
    }

    @Bindable
    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
        notifyPropertyChanged(BR.lastName);
    }
}

So you would be already having a class which notifies whenever its property changes. So you can just make use of this property change callback in your MutableLiveData to notify its observer. You can create a custom MutableLiveData for this

public class CustomMutableLiveData<T extends BaseObservable>
        extends MutableLiveData<T> {


    @Override
    public void setValue(T value) {
        super.setValue(value);

        //listen to property changes
        value.addOnPropertyChangedCallback(callback);
    }

    Observable.OnPropertyChangedCallback callback = new Observable.OnPropertyChangedCallback() {
        @Override
        public void onPropertyChanged(Observable sender, int propertyId) {

            //Trigger LiveData observer on change of any property in object
            setValue(getValue());

        }
    };


}

Then all you need to do is use this CustomMutableLiveData instead of MutableLiveData in your View Model

public class InfoViewModel extends AndroidViewModel {

    CustomMutableLiveData<User> user = new CustomMutableLiveData<>();
-----
-----

So by doing this you can notify both view & LiveData observer with little change to existing code. Hope it helps

Abhishek V
  • 12,488
  • 6
  • 51
  • 63
  • I do not know that why . in my class could not find BR. – Sup.Ia Oct 11 '18 at 16:25
  • @Sup.Ia Re-build the project to generate BR file – Abhishek V Oct 11 '18 at 16:27
  • 2
    is there a way to do this without data binding? I mean, an approach like this one in where you can choose when the liveData's value has changed. – MiguelHincapieC Nov 20 '18 at 23:05
  • I was reading this article https://medium.com/@tylerwalker/property-aware-mutablelivedata-with-kotlin-4fb5830ae730 that explains same solution but in Kotlin. However I couldn't get it to work. I think it is outdated because it is using lifecycle version = 1.1.1. How to set the value inside of `onPropertyChanged()` in Kotlin? `value = value` does not compile. – Marat Mar 24 '19 at 05:41
  • BR stands for? Anyone knows that? – Kwnstantinos Nikoloutsos Apr 27 '19 at 22:40
  • The BR is also giving me issues – PhilBlais May 23 '19 at 01:42
  • BR stands for BindingResources. It is created when compiling your project. For every ``-tag in your data-bound layout xml there will be a reference in BR. Similar to normal resources that are also defined in xml (e.g. R.string.something) – muetzenflo Jun 22 '19 at 11:50
  • Hi, it works without the CustomMutableLiveData class. Why do i need the class? – A.K. Aug 13 '19 at 20:07
  • 1
    Who unregisters the listener if we change to a completely different value object? And don't we register a new listener object every time we change one of the User object fields? – morpheus05 Jan 14 '20 at 14:16
  • i think @morpheus05 is right. – Bilawal muzaffar Nov 03 '21 at 18:00
28

When using MVVM and LiveData, you can re-bind the object to the layout so it will trigger all changes on the UI.

Given "user" is a MutableLiveData<User> in the ViewModel

ViewModel

class SampleViewModel : ViewModel() {
    val user = MutableLiveData<User>()

    fun onChange() {
        user.value.firstname = "New name"
        user.value = user.value // force postValue to notify Observers
        // can also use user.postValue()
    }
}

Activity/Fragment file:

viewModel = ViewModelProviders
            .of(this)
            .get(SampleViewModel::class.java)

// when viewModel.user changes, this observer get notified and re-bind
// the user model with the layout.
viewModel.user.observe(this, Observer {
    binding.user = it //<- re-binding user
})

Your layout file shouldn't change:

<data>
    <variable
        name="user"
        type="com.project.model.User" />
</data>

...

<TextView
        android:id="@+id/firstname"
        android:text="@{user.firstname}"
        />
sonique
  • 4,539
  • 2
  • 30
  • 39
28

If you are using Kotlin and LiveData, I can offer you 2 ways - with and without extension fucntion:

Without extension function

liveData.value = liveData.value?.also { it ->
    // Modify your object here. Data will be auto-updated
    it.name = "Ed Khalturin"
    it.happyNumber = 42
}

Same, but with extension

// Extension. CopyPaste it anywhere in your project
fun <T> MutableLiveData<T>.mutation(actions: (MutableLiveData<T>) -> Unit) {
    actions(this)
    this.value = this.value
}

// Usage
liveData.mutation {
    it.value?.name = "Ed Khalturin"
    it.value?.innerClass?.city= "Moscow" // it works with inner class too
}
DVegasa
  • 380
  • 3
  • 7
5

How can I make sure when some filed in user object changes observers get notified? BTW it is important to me to keep this data in the separate object and not use primary values like Strings in my ViewModel.

You can use androidx.lifecyle.Transformation class to monitor for individual fields.

val user = MutableLiveData<User>();
//to monitor for User.Name
val firstName: LiveData<String>  = Transformations.map(user) {it.firstName}
val lastName: LiveData<String>  = Transformations.map(user) {it.lastName}

you update user as per normal, and listen for firstname/lastname to monitor for changes in those fields.

John Pang
  • 2,403
  • 25
  • 25
Angel Koh
  • 12,479
  • 7
  • 64
  • 91
  • Are you sure this works? in theory Transformations are only updated when the observer for root object changes, so it will still need to change liveData value and not field – htafoya Oct 09 '22 at 01:08
5

From reddit - @cedrickc's answer :

add an extension function to MutableLiveData:

fun <T> MutableLiveData<T>.modifyValue(transform: T.() -> T) {
   this.value = this.value?.run(transform)
}
avisper
  • 878
  • 12
  • 11
  • 5
    fun MutableLiveData.setField(transform: T.() -> Unit) { this.value = this.value?.apply(transform) } – act262 Jun 11 '21 at 03:33
0

For your observer get notified you should use setValue if you do this user.getValue().setFirstName(user.getValue().getFirstName() + " A "); your observer will not be notified !

View Model

public MutableLiveData<User> getUser() {
    return user;
}

Activity / Fragment

mModel = ViewModelProviders.of(this).get(InfoViewModel.class);
mModel.getUser().observe(this, s -> {
    // User has been modified
});

Somewhere in your activity / fragment

This will trigger the observer :

mModel.getUser().setValue(user);

If you want to update only one field from an object instead of update the whole object you should have multiples MutableLiveData<String>

// View Model
private MutableLiveData<String>         firstName;
private MutableLiveData<String>         lastName;

//Somewhere in your code
mModel.getFirstName().setValue(user.getValue().getFirstName() + " A ");
mModel.getFirstName().observe(this, s -> {
    // Firstname has been modified
});
florian-do
  • 865
  • 1
  • 13
  • 27
-2

Simple Easy Solution in 2021 (also works for StateFlow):

The key point here is, .value must be changed to a Different Object (reference), so that the binding ui will get updated.

//Your model
data class Student(val name: String)

//Your ViewModel class
fun onStudentClick() {
    val newName = "Kate"    //change field/property

    _student.value = _student.value.copy(name = newName)    //must be different object!!
}
Sam Chen
  • 7,597
  • 2
  • 40
  • 73