把EditText交给ViewModel管理

2,272 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

Android小萌新今天在做项目的时候遇到一个小问题,来记录一下~

在做一个登录界面的时候,想使用DataBinding+ViewModel+LiveData

但是怎样让ViewModel拿到EditText控件的实例呢?一开始想到把DataBinding对象从Activity传入ViewModel,后来发现不可行,因为DataBinding在初始化的时候需要传入owner参数,而这个owner参数传的是Activity本身,也就是说DataBinding持有了Activity的引用,这时候如果把DataBinding传给ViewModel不就成了ViewModel持有Activity的引用了吗?内存泄漏!不行!

image.png

解决办法是通过DataBinding双向绑定(View可以操作数据,数据变化时通知View),让EditText的内容直接对应到ViewModel中的LiveData上,这样的话在输入框输入的同时LiveData也在随时变化。

一些收获的经验:

1. @={}和@{}

我发现EditText的text属性要使用@={...}而不是像TextView直接使用@{...}来和Livedata绑定,多出来的这个"="我个人认为是TextView和LiveData绑定仅仅只是get数据,而EditText和数据绑定需要get和实时set数据,所以"="可以理解为赋值

<EditText
        ...
        android:text="@={viewModel.inputAccount}"
        ... />
        
<EditText
        ...
        android:text="@={viewModel.inputVerify}"
        ... />

<Button
        ...
        android:onClick="@{(v)->viewModel.onLogin()}"
        ... />

2. 为什么在账号EditText输入一个数,getInputAccount()会被调用两次呢?

public class TemporaryLoginViewModel extends ViewModel {

    private static final String TAG = "TemporaryLoginViewModel";
    MutableLiveData<String> mInputAccount;
    MutableLiveData<String> mInputVerify;

    public MutableLiveData<String> getInputAccount() {
        // TODO:为什么EditText输入一个数,getInputAccount()会调用两次?
        Log.d(TAG, "getInputAccount: Entrance");
        //双检锁
        if (mInputAccount == null)
            synchronized (TemporaryLoginViewModel.class) {
                if (mInputAccount == null)
                    mInputAccount = new MutableLiveData<>();
            }
        //只是TextView展示的话可以返回不可变的LiveData,这里因为是EditText所以只能返回可变的MutableLivedata
        return mInputAccount;
    }

    public MutableLiveData<String> getInputVerify() {
        ...
    }

    public void onLogin() {
        Log.d(TAG, "onLogin: 账号:" + mInputAccount.getValue() + "  验证码:" + mInputVerify.getValue());
    }
}

这就要进入源码去看一眼了,在getInputAccount()上选择findUsages 发现有两处地方调用了它

image.png 第一处在一个回调方法的onChange()中,我们打个断点查看虚拟机栈的栈帧,在第一次执行到断点的时候,虚拟机栈是这样的:

image.png

onChange()内部是这样的:

image.png

也就是说你在输入框里打字使得EditText数据改变的时候,首先回调到onChange()中,在这个onChange()中通过getInputAccount()得到LiveData再给它set一个字符串值

第二处是在executeBindings()中,这个方法是什么时候执行呢?我们让程序继续执行,在下一次执行到断点的时候,虚拟机栈是这样的:

image.png 可以看到在第二次执行到断点的时候,程序从executeBindings()方法中企图调用getInputAccount()

继续向下追踪,就可以看到这样的一个描述

image.png

意思是当View所绑定的数据发生变更的时候,执行此方法

总结

走到这里就很清晰了,整个流程是首先在输入框中输入,当监听到输入后先回调onChange(),在onChange()中通过getInputAccount()得到LiveData,然后修改了LiveData的值;LiveData一但修改,就会重新执行executeBindings(),所以又会调用一次getInputAccount()

到现在就明白了为什么ViewModel中的getInputAccount()会被执行两次啦~

3. getInputAccount()只能返回MutableLiveData

第三个问题也很好理解,为了安全嘛,我一开始试图让getInputAccount()返回一个不可修改的LiveData,然后报错了!

image.png 从第二个问题的分析不难看出,人家内部还要给get到的LiveData执行setValue()呢,所以返回的LiveData一定是可变的MutableLiveData啦~

4. 程序启动时会额外执行一次getInputAccount()

当我查看Activity中的setLifecycleOwner(this)方法时发现它设置了一个LifecycleObserver

image.png 进入这个Observer image.png 它观察到Activity处于onStart状态的时候会调用executePendingBindings()

进入executePendingBindings()瞅瞅

image.png 又要去调用executeBindingsInternal(),这不就是我们上面在虚拟机栈中看到的调用步骤吗?也就是说在Activity在onStart状态时会执行一次getInputAccount()

image.png