对象字段变化时的LiveData更新

80

我正在使用Android MVVM架构和LiveData。我有一个像这样的对象

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;
    }
}

我的视图模型如下所示

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 ");
    }
}

如何确保当用户对象中的某个字段更改时,观察者会收到通知?顺便说一下,对我来说将此数据保存在单独的对象中并不使用ViewModel中的主要值(例如字符串)很重要。


我想知道这篇帖子的第一个答案和第二个答案有什么区别,第二个答案看起来更简单。 - David
7个回答

65
我认为安卓并没有针对这个问题推荐任何最佳实践。我建议您使用更简洁和少量样板代码的方法。
如果您正在使用安卓数据绑定以及 LiveData,您可以选择以下方法:
您的 POJO 对象应该类似于这样:
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);
    }
}

您可能已经有一个类,当其属性发生更改时会通知。因此,您可以在MutableLiveData中利用此属性更改回调通知其观察者。您可以为此创建自定义MutableLiveData。

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());

        }
    };


}

那么你需要在View Model中使用CustomMutableLiveData,而非MutableLiveData,这样就可以了。
public class InfoViewModel extends AndroidViewModel {

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

这样做可以在很少改变现有代码的情况下通知视图和LiveData观察者。希望能有所帮助。


我不知道为什么在我的类中找不到BR。 - Sup.Ia
2
有没有一种不使用数据绑定的方法来做到这一点?我的意思是,像这样的一种方法,在这种方法中,您可以选择何时更改liveData的值。 - MiguelHincapieC
我正在阅读这篇文章 https://medium.com/@tylerwalker/property-aware-mutablelivedata-with-kotlin-4fb5830ae730,它讲解了同样的解决方案,但是用 Kotlin 实现。然而我无法使其正常工作。我认为它已经过时,因为它使用的生命周期版本为1.1.1。如何在 Kotlin 中在 onPropertyChanged() 内设置值?value = value 不能编译。 - Marat
1
如果我们更改为完全不同的值对象,谁会注销侦听器?每次更改User对象字段时,我们不是都会注册一个新的侦听器对象吗? - morpheus05
我认为@morpheus05是正确的。 - Bilawal muzaffar
显示剩余5条评论

28

在使用MVVM和LiveData时,您可以将对象重新绑定到布局,以便在UI上触发所有更改。

假设" user "是ViewModel中的MutableLiveData<User>

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()
    }
}

活动/碎片文件:

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
})

您的布局文件不应更改:

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

...

<TextView
        android:id="@+id/firstname"
        android:text="@{user.firstname}"
        />

2
这个可以工作!但是重新绑定会有任何性能方面的缺点吗? - AouledIssa
这是比被标记为最佳更简单和更清晰的方法。 - ejdrian313
22
这对我来说似乎不太对。使用LiveData的整个意义不就是为了避免重新绑定吗? - Crearo Rotar
绑定viewModel不起作用。对我来说,重新绑定对象非常有效。 - Alireza
2
这不是重新绑定,它只是刷新变量,这将反映在用户界面上的更改。我看不出有什么问题。 - fullmoon
显示剩余2条评论

28

如果你正在使用Kotlin和LiveData,我可以提供两种方式 - 带有和不带有扩展函数:

不带扩展函数

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

同样的内容,但带有扩展

// 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
}

3
好主意。我可以通过使用 actions: MutableLivedata<T>.() -> Unit 来进一步增强它,这样 lambda 表达式就可以在不使用 it 的情况下编写了。 - Aleksandar Stefanović
优雅。我错过的一点是,设置完全相同的值对象会触发通知给观察者。希望在未来的版本中不会被“优化”。 - Afilu
2
它应该如何运作?这只会在初始化时触发一次,而不是每次值改变时。 - Carteră Veaceslav
它在对 .innerArray 进行添加操作时会如何表现? - Nick M
这个不再起作用了。 - Lifes

5

来自@cedrickc的回答

为MutableLiveData添加一个扩展函数:

fun <T> MutableLiveData<T>.modifyValue(transform: T.() -> T) {
   this.value = this.value?.run(transform)
}

5
fun <T> MutableLiveData<T>.setField(transform: T.() -> Unit) { this.value = this.value?.apply(transform) }这段代码定义了一个扩展函数 setField,它接受一个 lambda 表达式 transform,该 lambda 表达式的类型是 (T) -> Unit。当调用 setField 函数时,它将使用传递给它的 transform lambda 表达式修改 MutableLiveData 对象中的值并更新 UI。 - act262

5

如何确保用户对象中的某个字段更改时通知观察者?顺便说一下,对我来说将这些数据保存在单独的对象中并不使用ViewModel中的主值(例如字符串)很重要。

您可以使用androidx.lifecycle.Transformation类来监视每个字段。

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}

您按照正常流程更新用户信息,并监听名字(firstname)和姓氏(lastname)字段以监测它们的变化。


你确定这个能行吗?理论上,只有当根对象的观察者发生变化时,转换才会更新,因此仍然需要更改liveData值而不是字段。 - htafoya

0

为了让您的观察者得到通知,您应该使用setValue。如果您这样做:user.getValue().setFirstName(user.getValue().getFirstName() + " A "); 您的观察者将不会收到通知!

视图模型

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

活动/片段

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

在您的Activity / Fragment中的某个位置

这将触发观察者:

mModel.getUser().setValue(user);

如果您想仅更新对象中的一个字段而不是整个对象,您应该使用多个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
});

-2

2021年的简单易行解决方案(也适用于StateFlow):

关键在于,必须将.value更改为不同的对象(引用),以便绑定的UI得到更新。

//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!!
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接