无标题笔记

DBinding权威使用指南

标签(空格分隔): dbing


使用方式

layout:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <!-- 定义变量: private org.kale.viewModel vm -->
        <variable
            name="user"
            type="org.kale.vm.UserviewModel"
            />
    </data>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="@{user.name}"/>
</layout>  

Activity:

    private UserviewModel mUserVm = new UserviewModel();
    
     @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding b = DBinding.bind(this, R.layout.activity_main); // 设置布局
        DBinding.setVariables(b, mUserVm); //设置vm
        
        mUserVm.setName("漩涡鸣人");  // textview中就会自动渲染出文字了
    }

一、设计思路

1.1 三层结构

我们的项目结构里经常会出现这三种东西————M/V/C,这三个东西一定要广义理解为层,他们绝对不是狭义的类对象(因为有些语言中会有view、controller、model这样的类,请不要混淆)。所谓各种模式其实就是这三者的不同组合和通信方式。

无标题绘图.png-9.2kB
无标题绘图.png-9.2kB

要说明白这个问题,就要知道哪些是v,哪些是m,哪些是c。

V:视图层
v层是可以独立数据而显示的,它里面没有什么业务逻辑,仅仅是做展现。简单比喻来看就是一个提线木偶,它本身并不会有生命。Android中的view,比如textview,button,自定义的view当然也属于此类。

除了上述这些类对象外,activity、fragment算不算v层的东西呢?
如果看前面的定义,他们貌似都处于灰色地带,很难得到明确的定义。不过我们可以进行思维方式的转换,人为定义它们的意义。相比起传统的思路,我反而认为activity和fragment是属于v层的,activity可以做视图的绑定操作,并且可以在activity中方便的写视图的动画和布局切换效果。如果把activity变成别的层,那么你就很难找到一个适合的类去做这些事情了。

M:逻辑层
用于封装业务逻辑相关的数据以及对数据的处理方法。M本身是完全独立的个体,并且应该能被监听到执行的结果。M不应该知道view的存在。m层最直观的例子就是网络请求,网络请求是完全独立于视图层的,而且做的事情也是很单一,可以很好的被复用。

C:控制层
用来控制数据、处理view和数据的交互,它主要接收来自view的交互信号和数据层的改变结果,然后做相应的操作。早期的c层是键盘和鼠标,所以是可以直接面向用户进行操作的,但是在移动时代它慢慢变成了一个纯的控制对象。

如果说c层是用来做控制的,adapter算不算c呢?
adapter的意义大家都心知肚明,是用来做数据和view的绑定工作的,顺便更新下ui。如果说数据是血液,view是皮囊,那么adapter就是一个赋予view生命的输血机器。它本该属于的层最初我是很难把控的,我尝试过把它放入c层,也尝试过把它放入activity这样的view层,但都没能得到很好的答案。
最终,在commonAdapter这个项目的启发下,明白了adapter最合适的位置应该是c层。因为adapter中会做很多数据的处理,比如根据数据类型选择item这样的操作。而这样的操作如果放入view层,就会让view层一下子失去复用的能力,因此adapter放入c层是最合适的选择。
顺便说一句,c层是最不容易被复用的。因此如果一个东西是和当前页面独有的,不可能被复用的。那么它就应该死在c里,不要把它放出来。

1.2 MVC

无标题绘图 (2).png-11.3kB
无标题绘图 (2).png-11.3kB

我刚接触android的时候就听过android是MVC模式的,以为android的view层可以理解为layout(xml)层。但之后发现很多项目中竟会在自定义view中处理了很多业务逻辑,而且activity经常被用来做了view和controller的事情,慢慢的android项目就成了下面这样:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        setContentView(R.layout.activity_main); // 设置布局(v层的事情)
        
        final Button button = (Button) findViewById(R.id.button); // 绑定view(v层的事情)
        
        button.setOnClickListener(new View.OnClickListener() { // 设置监听器(v层的事情)
            @Override
            public void onClick(View v) {
                // 发请求,做数据的处理(c层的事情)
                HttpUtil.doPostAsync("http://www.baidu.com", "kale", new CallBack() {
                    @Override
                    public void onRequestComplete(String result) {
                        button.setText(result); // 更新视图(v层的事情)
                    }
                }); 
            }
        });
    }
}

分析
在这段代码中我们认为xml文件就是view层,activity做view和model的绑定操作,看起来activity又像c又像v。而且,在这种情况下activity会越来越臃肿,即便有fragment的加入,也无济于事。这种糟糕的情况就是android设计之初对activity定义不明的一个恶果。

我先抛几个问题:

  1. activity该做view的绑定工作么?
  2. activity要做view的动画操作么?
  3. activity应处理从网络返回的结果么?
  4. activity需要做不同状态下view的状态的控制么?

我相信大家都有了答案吧。为了解决activity臃肿和含义不清的矛盾,慢慢出现了mvp规范。

1.3 MVP

无标题绘图 (3).png-9.4kB
无标题绘图 (3).png-9.4kB

mvp做的事情也相当简单,仅仅是把mvc做了一个小小的改造,产生了清晰的封层。
【流程】view操作p,p会去调用model执行操作,p中接收到结果后去调用v来更新界面。

下面是某个使用mvp的项目中activity的代码:

presenter = new AppInfoPresenter(); // p层

mShowPackageNameBtn.setOnClickListener(v -> {
    v.setEnabled(false); // activity变成v层,这里控制view的相关状态
    
    // 点击后的事情交给p做,p做完后应该给v一个回调。为了说明简单,这里是同步回调。
    string name = presenter.getPackageInfo(getApplication()); // p层将最终的结果交给v层
    
    mShowPackageNameBtn.setText(name); // 得到回调后更新视图
});

这种方式将activity和xml文件变成了一个v,那么所有逻辑都交由p处理。这样的好处就是model对外层不知情,p对view不知情。成为了这样的一个蛋形结构:

无标题绘图 (4).png-19.3kB
无标题绘图 (4).png-19.3kB

内层对外层不知情的好处就是内层可以随意地做复用,坏处就是需要建立相互通信的机制,会带来各种回调。当然,你可以用Rx的方式很简单地做回调,但是我们是否真的应该采用这种到处抛出回调的方式呢?

先别急,我们先看看如果上面的例子变复杂的情况:
我希望点击按钮后,出现一个loading界面,p去做网络请求。无论成功与否,请求回来后都停止loading。如果成功得到了网络数据,才更新页面。

presenter.getPackageInfo(new completeCallback(){
    onAny(){
        // 停止loading
    }
},new successCallback(){
    onSuccess(){
        // 更新界面
    }
});

可以看出mvp中一个很蛋疼的后遗症————各种回调。而且这些回调都是要自己根据不同页面写的,而且每写一个回调就要写一个接口,接口的参数也要根据需求变。我敢保证绝对没办法一次性知道这个回调方法需要几个参数,而参数的类型也不能一次性就确定。

我觉得太麻烦了,我要简单一些。比如让P对v知晓,v也知道p的存在,会不会简单?赶快来看看这种方案是什么样的。

首先让activity实现某个接口比如IAppInfoUi,然后让P调用这个接口对象进行交互。
Activity:

presenter = new AppInfoPresenter(); // p层

protected void onCreate(){}
mShowPackageNameBtn.setOnClickListener(v -> {
    v.setEnabled(false);
    // 点击后的事情交给p做,不会给view回调
    presenter.getPackageInfo(getApplication());
});
}

/**
 * public方法,让p去调用
 */
public void onGotPkgInfo(String name){
    // 得到结果
}

Presenter:

public class AppInfoPresenter extends BasePresenter<IAppInfoUi> implements IAppInfoP {

    @Override
    public void getPackageInfo(Context context) {
        // p对v知情,直接调用v中的public方法(onGotPkgInfo())。
        
        // getView其实得到的就是activity的接口对象,即IAppInfoUi
        getView().onGotPkgInfo(context.getPackageName()); 
    }
}

现在,p和v的交互也不用各种回调了(将activity整体变成了一个回调接口)。那,这种方式有不会有什么问题?
p知道了v,那么p的复用性就丧失了。正如你所见,我利用接口来降低了二者互相知情造成的影响。但这样,你就必须在写view的时候定义接口,但如果你这个页面根本没复用价值,你还要做接口么。如果不做接口,你怎么知道这个页面真的没复用价值呢?而且接口的名字和参数你真的可以一次确定么?

好,我们偷懒一下,先看看能否不定义接口。
Presenter:

public class Presenter {
    // 注意这里的activity的类型是具体的而不是接口
    public MainActivity mActivity;

    public void loadData() {
        // request network 做数据处理
        HttpUtil.doPostAsync("http://www.baidu.com", "kale", (result)-> {
                mActivity.onGotData(result); // 抛数据给v层
            }
        });
    }
}

Activity:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        final Presenter presenter = new Presenter(this); // presenter
        button.setOnClickListener(v-> 
            presenter.loadData(); // p完全不知道这是因为点击而触发的动作,只知道要加载数据了
        );
    }

    /**
     * 被presenter调用的方法
     */
    public void onGotData(String name) {
        button.setText(name); // 更新视图
    }
}

用这种方式p中包含了v对象,可以不写任何回调就能直接触发v的动作,而且不用写接口和回调,甚至还可以支持一个v对应多个p的需求。但是如果你这个view被复用了,就得改一改。这种方案的一大坏处就是灵活性会比较低,但实际运用下来还不错,算是一种偷懒的做法。

顺便提及一下mvp的重要优点:

  1. 当activity意外重启时presenter不会被重启。
  2. activity重启时,presenter与activity会重新绑定,根据数据恢复activity状态。
  3. 而当activity真正销毁时,对应地presenter应该随之销毁

这样的好处可以解决以下2个很实际的问题:
不会每次翻转屏幕都去显示进度条,重新加载数据。
某些低端机,内存不足时activity被销毁,但p会持有数据,避免数据丢失。

1.4 谷歌的MVVM

我们看到了上述mvp的两种实现方案:
第一种灵活但是写起来复杂;第二种简单,但是灵活性不足。
mvvm模式利用数据绑定的特性,自动化实现了第一种的回调模式,很棒对不?但也仅此而已。

先来看看谷歌的dataBinding是怎么做分层处理的吧:
1.先定义一个User类

public class User { 

    private final String firstName;
    private final String lastName;
 
    public String getFirstName() {
        return firstName;
    } 
 
    public String getLastName() {
        return lastName;
    }
    
    void onSomething(){
        // 这里去加载网络
    }
} 

2.在layout文件中绑定user

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{"from:" + user.lastName}" />

3.在java代码中设置实现和数据

@Override 
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ActivityBasicBinding binding = DataBindingUtil.setContentView(
            this, R.layout.activity_basic);

    binding.setUser(new User());
} 

看完之后,是不是感觉很精妙。对,就是这么“精妙”,精妙到view都没办法被复用了。来看看这行代码:

android:text="@{"from:" + user.lastName}"

天哪!你怎么知道这个布局文件不会被复用,如果复用了,这里如果展示的是一个ad.info你该怎么处理。这个先不说,databinding还支持xml中写java代码,比如引入静态方法和简易判断什么的。

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{MyStringUtils.capitalize(user.firstName)}"   // 静态方法
    // or
    android:text="@{user.displayName != null ? user.displayName : user.lastName}" />

干的漂亮,直接让layout文件和java融为一体。现在请你告诉我我该怎么复用这个layout文件。如果我在这里也写了java的简单判断,java代码中也写了一些逻辑,我怎么快速定位问题。
这么写带来的严重后果就是,layout文件和java类强耦合,而且很难定位问题。你就永远不知道这个逻辑是在java代码中写的还是xml中写的了。

注意:我强烈不建议在xml中写java代码,它会增加定位问题的难度。

1.5 理想化的MVVM

理想化的mvvm最好是只用写少量代码就完成了具体需求的东西,超级酷!我希望我加载网络成功后自动会更新到视图上。
之前的做法:
建立xml,在activity中找到view,建立数据模型,写好网络回调,回调成功后依次设置view的状态。
理想化的做法:
写好数据模型,建立xml时直接绑定数据模型,在activity中写好回调就行。
代码如下:

数据模型:

public class UserInfo extends BaseObservable {
    private String name;
    
    @Bindable
    public String getName() { return name; }

    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(com.kale.dream.BR.name);
    }
}

布局文件:

<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable name="user" type="com.kale.dream.UserInfo"/>
    </data>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{user.name}"
        />
</layout>

Activity代码:

public class Dream01 extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final Dream01Binding binding = DataBindingUtil.setContentView(this, R.layout.dream_01);
        HttpUtil.doGet("http://www.kalexxxxxx", new HttpUtil.HttpCallback() {
            @Override
            public void onSuccess(UserInfo info) {
                binding.setUser(info);
            }
        });
    }
}

如果真的可以这么写,那么一切都会简单很多。三个文件搞定了model,vm,v层。只是如果真的这么写就会出现很多问题。比如你直接对json的数据模型做了很多的处理,让model的set和get方法不在纯粹。如果你今天不用dataBinding了,以后要改就会相当困难。现在再来复杂一点,我希望对name进行判断,根据name的数据不同让view呈现View.VISIBLE, View.INVISIBLE, View.GONE三种状态。

数据模型:

public class UserInfo extends BaseObservable {

    private String name;

    @Bindable
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(com.kale.dream.BR.name);
    }

    @Bindable
    public int getViz() {
        switch (name) {
            case "jack":
                return View.VISIBLE;
            case "tony":
                return View.INVISIBLE;
            default:
                return View.GONE;
        } 
    }
}

布局文件:

//……
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{user.name}"
    android:visibility="@{user.viz}" // 仅仅添加了这一行
    />

其余的文件不变。

我们增加了一个需求,只需要动两个文件,是不是很赞。那么坏处就是对model的操作太多了,它已经不再纯粹,可能未来还会做更多的事情。为了解决这个问题,谷歌说不要再动model了,我允许你在xml中写java代码:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{user.name}"
    android:visibility="@{user.name.equals(jack)?View.gone:View.visible}"
    />

这样的做法就可以让我们不动model类,只需要在xml中做逻辑操作。只可惜xml中肯定不能完成所有的视图操作(比如动画),而且xml是有复用价值的。所以我给出的结论就是,理想很丰满,现实很骨感。

1.6 理想妥协于现实后的MVVM

我们不希望一个框架对现有的项目结构做太多的影响,框架影响到用于json解析的model是不能容忍的!这样的话,我们就要建立一个给框架用的数据对象,所以vm现在就变成了一个和前面的json的model无关的独立类。
需要注意的是:
vm仅仅处理和视图展示内容有关的逻辑,比如对显示的内容做格式化这样的事情。除此之外不应处理其他的视图逻辑。

数据模型:

public class UserInfo {

    public String name;

}

viewModel:

public class DreamVm extends BaseObservable{

    private String name;

    @Bindable
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(com.kale.dream.BR.name);
    }
    
    @Bindable
    public int getViz() {
        switch (name) {
            case "jack":
                return View.VISIBLE;
            case "tony":
                return View.INVISIBLE;
            default:
                return View.GONE;
        }
    }

    ///////////////////////////////////////////////////////////////////////////
    // 事务操作
    ///////////////////////////////////////////////////////////////////////////

    public void load() {
        HttpUtil.doGet("http://www.kalexxxxxx", new HttpUtil.HttpCallback<UserInfo02>() {
            @Override
            public void onSuccess(UserInfo02 info) {
                setName(info.name);
            }
        });
    }
}

布局文件:

// 将之前的userinfo换为vm来绑定
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{vm.name}"
    android:visibility="@{vm.viz}"
    />

Activity代码:

public class DreamAct extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        final Dream02Binding binding = DataBindingUtil.setContentView(this, R.layout.dream_02);
        DreamVmp02 viewModel = new DreamVmp02();

        binding.setVm(viewModel);
        
        viewModel.load(); // 加载网络请求
    }
}

这样的做法就没啥问题了,vm处理了逻辑,model变成纯粹的类。而且也不用在xml中写什么java代码了,只要绑定vm的字段即可。view相关的java代码都在activity中完成,如果view的逻辑出错了直接进入activity中定位即可。

我们注意到了vm中做了加载数据的操作,应不应该这么写呢?
不应该!viewModel就是view的数据模型,所以不应该图省事,在vm中写和view无关的业务逻辑。所以下面的这块代码不应该出现在vm中,而是应该放在别的地方。至于放在哪里合适,这就是后话了。

///////////////////////////////////////////////////////////////////////////
    // 事务操作
    ///////////////////////////////////////////////////////////////////////////

    public void load() {
        HttpUtil.doGet("http://www.kalexxxxxx", new HttpUtil.HttpCallback<UserInfo02>() {
            @Override
            public void onSuccess(UserInfo02 info) {
                setName(info.name);
            }
        });
    }

1.5 MVVM模式

看到了databinding的不足和问题,但我们也不能否认它的好处。它优雅的帮我们搞定了回调,而且支持了数据的自动绑定。基于以上的分析,我做了一个小小的改造,让layout和viewModel紧密联系,如图所示:


无标题绘图 (8).png-11.9kB
无标题绘图 (8).png-11.9kB

view层:具体的view,activity,fragment等,做ui展示、ui逻辑、ui动画。
viewModel层:由插件自动生成的具体类,是view展示的数据的java抽象,仅能被model层直接操作。
model层:非ui层面的业务逻辑的实现。包含网络请求,数据遍历等操作,是很多具体类的抽象载体。

这里除了model层的定义很抽象外,其余的v、vm层我都给出了具体类做载体,下面详细说下model层包含些什么。

model层:
因为model层是很多具体类的聚合,主要是做和展示无关的业务逻辑,在其中也会包含很多具体类。

无标题绘图 (9).png-20.2kB
无标题绘图 (9).png-20.2kB

要实现一个页面的需求,我们可能会用到很多类,包括工具类和各种库。这些类都提供了一个或多个功能,我们利用这些功能便可最终实现需求。而所有的调用应该是由一个类来进行的,这个类你可以叫做presenter,也可以叫做别的名字。总之,它就是一个执行非ui层面逻辑的个体。
需要注意的是,这个p和mvp中的p是无关的,我仅仅是没有找到很好的名字,才称之为presenter。

二、如何使用Dbinding

2.1 编写layout文件

首先我们先建立一个xml文件,这里推荐通过改模板的方式快速建立一个dbinding风格的layout文件。

<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <!-- 定义变量: private org.kale.viewModel vm -->
        <variable
            name="user"
            type="org.kale.vm.UserviewModel"
            />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </LinearLayout>
</layout>  

在建立好的layout文件中的<data>标签里可以由<variable/>标签来定义viewModel,这个对象之后会和view的属性进行绑定。目前,我们不用管这个类是否存在,我们只需要定义你想要的类和其对象的名字即可,比如:

<data>
    <!-- 定义变量: private org.kale.UserviewModel user -->
    <variable type="org.kale.UserviewModel"  name="user"/>
</data>

定义好了变量名,我们就可以将其内部的字段与view的attribute进行绑定了:

<TextView
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:text="@{user.name}"
    />

这里我将user中的name字段与text这个属性进行了绑定。(需要注意,这个text字段现在我们还没创建,仅仅是写了出来)。
现在这个textview展示的文字就是我们定义好的UserviewModel中的name的值了。通过对textview的了解,我们可以推测出name这个字段肯定是CharSequence类型的。

2.2 编写layout文件时的问题

1.如何建立layout模板

idea中有个功能就是可以编辑文件模板,下面演示下如何定义这个模板。

1229.gif-1158.5kB
1229.gif-1158.5kB

2.为什么数据块是用<data>包裹,为什么叫这个名字呢?

因为这里的xml变成了两部分,一个就是传统的viewgroup包裹的布局文件,其余就是需要和布局文件绑定的vm。而vm的区块用data做命名是比较合适的,因为vm本身就是数据。
这里需要格外注意的是,现在的layout文件已经不仅仅是一个单纯的布局文件了,更合理的叫法是视图+数据文件。

3.为什么这里定义变量是用<variable/>标签,而不是别的名字呢?

variable本身就是变量的意思,这个名称是很合适的。当然如果写过js的同学,会更加熟悉它的缩写var。但这里的起名上,大家应该不会有什么争议和容易理解错误的地方。

4.为什么是通过type和name这两个属性来定义一个vm?

在java世界中我们是通过:

private org.kale.viewModel vm

来做变量的定义的。在xml文件中不存在什么相互调用的情况,定义的变量都是private的,所以省略了private这个关键字。至于,为什么变量的对象名用type,名字用name,这也是有原因的。

为了说清楚这个问题,我们先来看看jdk中Field这个类的部分代码。

package java.lang.reflect;

public final class Field extends AccessibleObject implements Member {
    private Class<?> clazz;
    private int slot;
    private String name; // 参数的名字【name】
    private Class<?> type;  // 参数的类型【type】
    private int modifiers;
    private transient String signature;
    private transient FieldRepository genericInfo;
    private byte[] annotations;
    private FieldAccessor fieldAccessor;
    private FieldAccessor overrideFieldAccessor;
    private Field root;
    private transient Map<Class<? extends Annotation>, Annotation> declaredAnnotations;  
    // 省略...  
}

可见,这里的命名是参照了java中field的命名来的。这种命名方式会比较符合大众直觉,明白了这个原因,相信以后定义的时候就不会有什么疑惑了。

5.viewModel的name字段该怎么取名?

既然我们的viewModel会和view进行绑定,而且view是有可能被复用的。所以这里的取名我强烈建议和view的意义有关。千万不可脱离view的意义随便取名字,这样以后你用的时候就会很麻烦。简单来说,你完全可以参考之前给view取id的做法来给viewModel取名字。

6.viewModel的type应该怎么写?

既然我们名字搞定了,那么类名基本就出来了。这里需要注意的是,这里的类名是包含完整包名的。我强烈建议所有的viewModel都在一个包中,不要随便放。因为viewModel以后可能会出现重命名和被删除的情况,放在一个包下面方便管理。这里我是在vm这个包下面放所有的viewModel,所以就取了org.kale.vm.UserviewModel这样一个名字。至于其他的viewModel的包名前缀我们也可以规范为org.kale.vm

7.可以在同一个xml中写两个相同类型的viewModel么?

可以,但没必要!
在java中会出现这样的情况:

String firstName;
String lastName;

在一个类文件中定义了两个相同类型(String)的field。但是在xml中这种情况是严格禁止的,也是完全没必要的。因为viewModel是一个有明确含义的对象,并不是基本类型。而且其绑定地view也是特定的,完全没必要定义两个相同类型的viewModel。顺便提一下,两个不同类型的viewModel的name必须是不同的,这个和java的规则完全一致的。

8.真的不用写具体的viewModel类么?

是的,不用。你在xml中定义好了

<variable
    name="user"
    type="org.kale.vm.UserviewModel"
/>

后,这个UserviewModel会通过插件(或通过快捷键)出现在相应的包下,你定义好就可以直接用了,没必要关心具体的实现。这也是dbinding插件的一个强大之处!

9.如何在别的xml中复用已经定义好的类?

viewModel的一大特性就是可复用,viewModel和view都可以是多对多的关系。比如你这个UserviewModel中包含了username这个字段,而别的xml文件中正好需要这个UserviewModel,你完全可以把username定义到那个xml文件中。这样两个xml文件就会共用一个共同类型的数据。复用的方式很简单,就是在别的xml文件中写上

<variable
    name="user"
    type="org.kale.vm.UserviewModel"
/>

就行。

10.如何知道一个vm是否已经存在?

目前如果已经存在的vm会有代码提示的,如果没有代码提示,并且报红的就是之前没有的vm。

11.可以在一个xml文件中定义多个viewModel么?

当然可以,正如我所说的:viewModel是和view绑定的。一个界面中有多个不同模块的view是很常见的,遇到这样的情况你完全可以在xml中定义多个viewModel。要知道viewModel和view都是多对多的关系。

12.viewModel如何做全局改名、删除、移动这样的重构操作?

1.改名和改包名

这个就是和重构相关的问题了。
我们在xml中通过插件产生了viewModel,但如果要改包名和改变viewModel的类名的时候,最简单快捷的方式是,进入到这个类的实体中,通过idea的重构工具进行修改。这样所有的改动会自动同步到使用这个类的xml文件中了。当然,你也可以在这个类被调用的地方通过重构工具进行改名。

1225.gif-403.7kB
1225.gif-403.7kB

2.删除

至于删除某个viewModel也是一样的,仍旧是对java类进行操作。删除的时候注意排查下用到的地方,以免出错。


1227.gif-267.4kB
1227.gif-267.4kB

13.viewModel中的字段如何做改名、删除这样的操作?

1.改名

我们先来看下插件会给我们通过我们的xml生成什么东西:

package org.kale.vm;

public class UserviewModel extends BaseviewModel {

    private java.lang.CharSequence name;

    public final void setName(java.lang.CharSequence name) {
        this.name = name;
        notifyPropertyChanged(BR.name);
    }

    @Bindable
    public final java.lang.CharSequence getName() {
        return this.name;
    }
}

我们看到了我们定义的name字段和其get和set方法。如果我们突然想把这个“name”改名为“nickname”,或者是删除这个name字段呢?做法就是直接重构name这个字段。
下面为了演示方便,减少干扰选项。我把name这个过于通用的字母先改为了nickname。我将演示如何将nickname通过idea的重构工具改为name。

1226.gif-1147kB
1226.gif-1147kB

2.删除

因为idea对于databinding的支持力度很低(未来或许就可以通过重构工具做了),所以在重构字段的时候只能我们自己去排查了。我的排查方案是通过检索当前类使用过的地方,来看下使用当前类的xml中有没有使用过我要删除的字段,如果有就进行处理,如果没有就直接删除。以此来避免删除后出现程序出错的问题。


1228.gif-279.9kB
1228.gif-279.9kB

14.如果我不想通过插件生成vm,可以手动写么?

当然可以的,只需要在xml中加入ignore="true"就好。这样插件就会忽略这个vm,交给开发者自己去建立。

<variable
    name="vm"
    ignore="true"  // 加上这个属性就会被插件无视掉
    type="kale.db.ignore.IgnoredviewModel"
    />

15.如果插件生成的viewModel的属性不能满足我的要求,我可以自己配置生成规则么?

可以的,只需要做下面两步:
1.在项目中的values下的dbinding_config.xml文件中,增加插件生成的规则:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- 
        For original view.
        Example: android:text="name"
    -->

    <string name="text">java.lang.CharSequence</string>

    <!-- 
        For Custom view 
        Example: bind:customAttr="name"
        
        下面这个就是我自定义的规则,如果属性名是customAttr,那么vm中的字段类型是CharSequence
    -->

    <string name="customAttr">java.lang.CharSequence</string>

</resources>

2.在随便一个类中写入如下的java代码:

@BindingAdapter("app:customAttr")
public static void setSrc(CustomView view, CharSequence c) {
    view.setSpecialText(c);
}

这样插件在自动生成代码的时候就会读取你新加入的规则,生成你想要的字段类型了。

2.3 编写java代码

上面说了那么多的layout文件的编写策略,现在该说下如何写java代码了。java代码主要做的工作就是绑定vm和layout文件,其余的操作就直接对vm进行就行了。对于复杂的界面,可以把ui无关的逻辑放入presenter中。

Activity:

public class MainActivity extends AppCompatActivity {

    // 管理界面事件的vm
    private EventViewModel mEvent = new EventViewModel();
    
    private final UserViewModel mUserVm;
    
    private ActivityMainBinding b;

    public MainActivity() {
        mUserVm = new UserViewModel();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        bindViews();
        setViews();
        doTransaction();
    }

    private void bindViews() {
        b = DBinding.bindViewModel(this, R.layout.activity_main, mEvent, mUserVm);
    }

    private void setViews() {
        mEvent.setOnClick(v -> {
            if (v == b.userInfoInclude.headPicIv) {
                startActivity(UserDetailActivity.withIntent(MainActivity.this, mUserVm));
            }
        });
    }

    private void doTransaction() {
        MainPresenter presenter = new MainPresenter(mUserVm);
        if (presenter.init(this)) {
            Toast.makeText(MainActivity.this, "Init Complete", Toast.LENGTH_SHORT).show();
        }
        presenter.loadData();
    }
}

MainPresenter:

public class MainPresenter {
    private UserviewModel mUserviewModel;

    public MainViewModel(UserviewModel userviewModel) {
        mUserviewModel = userviewModel;
    }

    /**
     * 这个当然可以放在构造方法中进行,我这里为了说明view层调用vm的方法,强制加入了一个回调。
     */
    public boolean init(Context context) {
        mUserviewModel.setPic(BitmapFactory.decodeResource(context.getResources(),
            R.drawable.mingren));
        mUserviewModel.setName("漩涡鸣人");  // textview中就会自动渲染出文字了
        return true;
    }

    public void loadData(){
        // 模拟加载网络成功,将名字更新的操作
        mUserviewModel.setName("kale");
    }

    // java层面的测试,不用安装apk
    public static void main(String[] args) {
        UserviewModel userviewModel = new UserviewModel();
        MainPresenter model = new MainPresenter(userviewModel);
    }
}

2.4 编写java代码时的问题

1.presenter会被复用么?

会,但是极少。因为p是和某个特定的逻辑极其相关的,因此复用p的情况十分少见。

2.v和p的关系是什么样的?

p可以做很多和业务逻辑相关的操作,但是vm必须是纯粹的,vm对p完全不知情。
一个p会操作多个vm,一个vm也会被多个p使用。
注意:在本框架中vm是由框架自动生成的,强烈不建议对vm直接做增添与业务逻辑相关的事情。

3.有了mvvm框架后是否还需要fragment?

需要。因为activity中肯定会有不同的ui区块,fragment既可以划分不同的ui区域,又可以做到让这些细颗粒度的ui能够被复用,因此还是需要fragment来帮忙解耦ui,给activity减负的。

4.dbinding能否支持双向绑定?

不准备支持。因为view的事件和vm的数据绑定其实是无关的,而且谷歌db的设计思路,本身就是单向的。如果非要套双向绑定,我不能确保支持所有的view,所以目前是不准备支持的。

5.p对vm的操作是否必须在ui线程中?

可以在任何线程中操作vm,再也不用切回主线程操来操作作ui了。

6.如果p中的某些操作需要通知给activity,该怎么处理?

强烈建议用回调的方式做通知,不要把activity通过构造方法传给p。如果传了,就需要注意持有activity对象的问题,小心造成内存泄漏。

顺便问一句,为什么fragment必须需要持有activity的引用呢?
首先,fragment也是ui,fragment中需要有很多和context有关的操作。比如启动activity什么的,所以需要持有activity对象来做这些事情。最重要的是,fragment本身是可以对用户行为产生事件的,而p绝对不会自己产生事件,必须通过外部触发才行。因此p完全可以走纯回调的方式,不必持有任何全局的context对象。

public String getPackageName(Activity context) {
    return context.getPackageName();
}

这个例子中,我通过参数传入context,利用return返回结果。例子虽然简单,但对于复杂的情况也是是如此。如果你觉得回调写起来很麻烦,不妨试试用rxJava的形式做。

7.在mvvm框架中应该有什么编码思路?

应该有明确的分层思路。在mvvm中我们应该把所有数据同步的事情交给框架,而不是自己去维护。将v层的逻辑(如:动画,控件A改变引起的控件B改变等)独立写出;在p中独立写出数据对viewModel产生影响的逻辑。

下面举个adapter的例子:

    /**
     * 数据改变后ui会做一些改变。
     * 应该利用对vm的字段监听的方式做处理,不应该在数据改变时,通过开发者做ui层面的更新。
     *
     * @param bind 为什么不是单一监听器,而是观察者模式?
     *             因为会有多个东西对同一个数据进行监听,如果是单一的就没办法实现这个功能。
     */
    public void notifyData(final NewsItemBinding bind) {
        mviewModel.addOnPropertyChangedCallback((sender, propertyId)-> {
                // 监听title的改变,然后设置文字
                if (propertyId == kale.db.BR.title) {
                    setSmartText(bind.title, mviewModel.getTitle());
                }
            }
        });
    }
    
    /**
     * 如果有数据,那么就显示textView;
     * 如果没数据,那么就让textView消失
     */
    public void setSmartText(TextView textView, CharSequence text) {
        textView.setVisibility(!TextUtils.isEmpty(text) ? View.VISIBLE : View.GONE);
    }

在数据来的时候,数据仅仅对vm进行绑定,不用考虑绑定后ui层面的逻辑:

    ///////////////////////////////////////////////////////////////////////////
    // 这里就仅仅做数据和ui的绑定工作了,不用想ui层面的任何逻辑,这个算是数据层面的绑定工作。
    ///////////////////////////////////////////////////////////////////////////
    
    /**
     * 将viewModel和model的数据进行同步
     * model模型可能很复杂,但viewModel的模型很简单,这里就是做二者的转换。
     */
    @Override
    public void handleData(NewsInfo data, int pos) {
        mviewModel.setTitle(data.title);
    }

8.如果我这个界面本身就没有复用价值,能不能不用viewModel?

我们知道vm算是对xml文件的一种抽象,那么如果我这个界面本身就没复用价值,能不能直接把p当作vm,直接绑定p中的字段呢?
这样做当然是可行的。但问题就在于,你真的可以确保某个xml没有复用价值么?如果你当前认为无复用价值的xml,以后却要被复用了,那么你之前偷懒做的事情,对以后的人来说就是灾难。虽然需要改一两行代码没啥问题,但对于程序设计来说,你之前的设计方案和现在的需求产生了冲突,就得重新换设计思路,这其实是有些严重的。

在android设计之初就给出了xml文件和java代码分离的编码方案。但仍旧允许在java代码中通过new出view来写ui。xml和java分离的设计方案就是强制复用的思路,而activity中手动写view的编码方案就是所谓的无复用思路。二者一类比,你就知道你需不需要写vm了。

9.在Activity中应该做什么事情?

Activity中应该做一些view的配置工作。比如配置recyclerView的layoutmanager,设置下下拉刷新,view的动画效果等等。
如果你的view的某种状态的改变会引起其他view的改变,这个逻辑操作也需要放入activity中。很常见例子的就是,输入密码框的旁边有个是否显示明文密码的按钮,点击按钮会把密码已明文的形式显示出来,再点就变成了密文。这个东西是绝对属于ui逻辑的范畴,所以应该写在activity中,并且不应影响到vm和p。

10.我们真的不需要view的id了么?

我们仍旧需要id,只是不再需要findViewById了!
我们通过监听vm的某个字段来做相应的操作,但如上一个问题所说到的,密码是否显示的按钮和输入框的相互作用是不应该用vm做的。所以,在ui层面的逻辑中肯定还会有大量的id出现。幸好,databinding帮我们自动生成了id对象,再也不用写findViewById了。

11.Adapter应该放在哪里?

adapter的位置比较尴尬,而且复用价值不高,实践了很久后,我推荐放入p中。在dbinding中,我提供了ObservableArrayList这个list做数据的处理。现在只需要对list进行操作即可,不用关心界面的更新问题。notifyDataSetChanged()会自动执行。

public MainPresenter(UserViewModel userVm, final Activity activity) {
    mList = new ObservableArrayList<>();
    mUserVm.setAdapter(new CommonRcvAdapter<NewsInfo>(mList) {
        @NonNull
        @Override
        public AdapterItem<NewsInfo> createItem(Object o) {
            return new GameItem(activity);
        }
    });
}

public void loadData() {
    List<NewsInfo> data = NetworkService.loadDataFromNetwork();
    mList.addAll(0, data); // 再也不用手动写notifyDataSetChanged()了
}

12.什么该在xml中定义vm的字段?

当数据的变化会“直接”引起view的某个属性改变,那么就应该在layout中写一个vm的字段进行绑定。如果这个view的某个状态,是根据view其他的状态改变而改变的,和数据层无关。那么就不应该定这个字段,而是用监听vm字段的方式来做。
监听的方案:

mGameVm.addOnValueChangedCallback(id -> {
    switch (id) {
        case BR.title:
            b.titleTv.setVisibility(mGameVm.getViz() ? View.VISIBLE : View.GONE);
            break;
        case BR.isLikeText:
            final int color = 
                mGameVm.getIsLikeText().equals(LIKED) ? R.color.yellow : R.color.gray;
            b.isLikeText.setTextColor(getResources().getColor(color));
            break;
    }
});

13.如果两个页面需要同步很多数据,可以直接共用vm么?

当然可以!vm自身的自动绑定特性会让两个页面共用数据变得十分简单,可以通过viewModel.toSerializable()来将其序列化,然后在接收的地方通过:
NewsviewModel vm = NewsviewModel.toviewModel(getIntent().getSerializableExtra(KEY));
得到它。得到后,你就可以方便的利用上个页面传来的vm进行layout层面的绑定了。
虽然这种方式十分简单,但不要滥用,它仅仅针对于两页面是含有共同vm的情况,其他情况我还是推荐通过回调、广播、事件总线等方式做。要记得,vm虽好,但它不是万能的。

请测试在低内存中这种方案会不会有bug。

14.如何对自动生成的vm做定制

插件仅仅能生成普通情况下的vm,它不可能知道你具体的逻辑和特殊需求。如果你要由这样的需求,可以在new出vm的时候通过重载set方法来实现。

mUserVm = new UserViewModel() {
    @Override
    public void setName(CharSequence name) {
        // 对于复杂的ui需求,可以重载对应的set方法,不应该重载get方法
        super.setName(String.format(name, "kale", "saber"););
    }
};

15.view的事件该如何绑定

因为vm仅仅是view的字段,vm的字段也应该和数据保持一致的,这时候你就会发现view的事件是不应该做vm绑定的,因为它不是数据。但,为了节约findviewById和配置监听器的代码,我提供了一个event对象来做界面的事件绑定。界面中所有view对象的事件都交给它来做就行。

layout文件:

<variable
    name="event"
    type="vm.Event"
    />
    
//……
    
<Button
    android:id="@+id/change_btn"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onClick="@{event.onClick}"
    />

java代码:

mEvent.setOnClick(v -> {
    if (v == b.changeBtn) {
        mUserVm.setName("Kale");
    } else if (v == b.headPicIv) {
        Toast.makeText(UserDetailActivity.this, "点击了头像", Toast.LENGTH_SHORT).show();
    }
});

相比起之前的做法,是必须要findviewbyid找到这个控件,然后设置监听器,为了一个页面共用listener,减少代码。就必须写成这样:

    Button btn = (Button)findViewById(R.id.btn);
    btn.setOnClickListenter(this);
    
    // ……
    
    void onClick(View v){
        //……
    }

其实这样的问题就在于view的设计监听和实现是脱离的,你必须要进行跳转才能找到监听器的实现,没有聚合好。

避免NullPointerException

自动生成的 data binding 代码会自动检查和避免 null pointer exceptions。举个例子,在表达式 @{user.name} 中,如果 user 是 null,user.name 会赋予默认值 null。如果你引用了 user.age,因为 age 是 int 类型,所以默认赋值为 0

参考自:
https://github.com/LyndonChin/MasteringAndroidDataBinding
//www.greatytc.com/p/add73330d106
//www.greatytc.com/p/e7b6ff1bc360
//www.greatytc.com/p/918719151e72
http://www.ruanyifeng.com/blog/2015/02/mvcmvp_mvvm.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容