無(wú)標(biāo)題筆記

DBinding權(quán)威使用指南

標(biāo)簽(空格分隔): 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); // 設(shè)置布局
        DBinding.setVariables(b, mUserVm); //設(shè)置vm
        
        mUserVm.setName("漩渦鳴人");  // textview中就會(huì)自動(dòng)渲染出文字了
    }

一、設(shè)計(jì)思路

1.1 三層結(jié)構(gòu)

我們的項(xiàng)目結(jié)構(gòu)里經(jīng)常會(huì)出現(xiàn)這三種東西————M/V/C,這三個(gè)東西一定要廣義理解為層,他們絕對(duì)不是狹義的類對(duì)象(因?yàn)橛行┱Z(yǔ)言中會(huì)有view、controller、model這樣的類,請(qǐng)不要混淆)。所謂各種模式其實(shí)就是這三者的不同組合和通信方式。

無(wú)標(biāo)題繪圖.png-9.2kB
無(wú)標(biāo)題繪圖.png-9.2kB

要說(shuō)明白這個(gè)問(wèn)題,就要知道哪些是v,哪些是m,哪些是c。

V:視圖層
v層是可以獨(dú)立數(shù)據(jù)而顯示的,它里面沒(méi)有什么業(yè)務(wù)邏輯,僅僅是做展現(xiàn)。簡(jiǎn)單比喻來(lái)看就是一個(gè)提線木偶,它本身并不會(huì)有生命。Android中的view,比如textview,button,自定義的view當(dāng)然也屬于此類。

除了上述這些類對(duì)象外,activity、fragment算不算v層的東西呢?
如果看前面的定義,他們貌似都處于灰色地帶,很難得到明確的定義。不過(guò)我們可以進(jìn)行思維方式的轉(zhuǎn)換,人為定義它們的意義。相比起傳統(tǒng)的思路,我反而認(rèn)為activity和fragment是屬于v層的,activity可以做視圖的綁定操作,并且可以在activity中方便的寫(xiě)視圖的動(dòng)畫(huà)和布局切換效果。如果把a(bǔ)ctivity變成別的層,那么你就很難找到一個(gè)適合的類去做這些事情了。

M:邏輯層
用于封裝業(yè)務(wù)邏輯相關(guān)的數(shù)據(jù)以及對(duì)數(shù)據(jù)的處理方法。M本身是完全獨(dú)立的個(gè)體,并且應(yīng)該能被監(jiān)聽(tīng)到執(zhí)行的結(jié)果。M不應(yīng)該知道view的存在。m層最直觀的例子就是網(wǎng)絡(luò)請(qǐng)求,網(wǎng)絡(luò)請(qǐng)求是完全獨(dú)立于視圖層的,而且做的事情也是很單一,可以很好的被復(fù)用。

C:控制層
用來(lái)控制數(shù)據(jù)、處理view和數(shù)據(jù)的交互,它主要接收來(lái)自view的交互信號(hào)和數(shù)據(jù)層的改變結(jié)果,然后做相應(yīng)的操作。早期的c層是鍵盤(pán)和鼠標(biāo),所以是可以直接面向用戶進(jìn)行操作的,但是在移動(dòng)時(shí)代它慢慢變成了一個(gè)純的控制對(duì)象。

如果說(shuō)c層是用來(lái)做控制的,adapter算不算c呢?
adapter的意義大家都心知肚明,是用來(lái)做數(shù)據(jù)和view的綁定工作的,順便更新下ui。如果說(shuō)數(shù)據(jù)是血液,view是皮囊,那么adapter就是一個(gè)賦予view生命的輸血機(jī)器。它本該屬于的層最初我是很難把控的,我嘗試過(guò)把它放入c層,也嘗試過(guò)把它放入activity這樣的view層,但都沒(méi)能得到很好的答案。
最終,在commonAdapter這個(gè)項(xiàng)目的啟發(fā)下,明白了adapter最合適的位置應(yīng)該是c層。因?yàn)閍dapter中會(huì)做很多數(shù)據(jù)的處理,比如根據(jù)數(shù)據(jù)類型選擇item這樣的操作。而這樣的操作如果放入view層,就會(huì)讓view層一下子失去復(fù)用的能力,因此adapter放入c層是最合適的選擇。
順便說(shuō)一句,c層是最不容易被復(fù)用的。因此如果一個(gè)東西是和當(dāng)前頁(yè)面獨(dú)有的,不可能被復(fù)用的。那么它就應(yīng)該死在c里,不要把它放出來(lái)。

1.2 MVC

無(wú)標(biāo)題繪圖 (2).png-11.3kB
無(wú)標(biāo)題繪圖 (2).png-11.3kB

我剛接觸android的時(shí)候就聽(tīng)過(guò)android是MVC模式的,以為android的view層可以理解為layout(xml)層。但之后發(fā)現(xiàn)很多項(xiàng)目中竟會(huì)在自定義view中處理了很多業(yè)務(wù)邏輯,而且activity經(jīng)常被用來(lái)做了view和controller的事情,慢慢的android項(xiàng)目就成了下面這樣:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        setContentView(R.layout.activity_main); // 設(shè)置布局(v層的事情)
        
        final Button button = (Button) findViewById(R.id.button); // 綁定view(v層的事情)
        
        button.setOnClickListener(new View.OnClickListener() { // 設(shè)置監(jiān)聽(tīng)器(v層的事情)
            @Override
            public void onClick(View v) {
                // 發(fā)請(qǐng)求,做數(shù)據(jù)的處理(c層的事情)
                HttpUtil.doPostAsync("http://www.baidu.com", "kale", new CallBack() {
                    @Override
                    public void onRequestComplete(String result) {
                        button.setText(result); // 更新視圖(v層的事情)
                    }
                }); 
            }
        });
    }
}

分析
在這段代碼中我們認(rèn)為xml文件就是view層,activity做view和model的綁定操作,看起來(lái)activity又像c又像v。而且,在這種情況下activity會(huì)越來(lái)越臃腫,即便有fragment的加入,也無(wú)濟(jì)于事。這種糟糕的情況就是android設(shè)計(jì)之初對(duì)activity定義不明的一個(gè)惡果。

我先拋幾個(gè)問(wèn)題:

  1. activity該做view的綁定工作么?
  2. activity要做view的動(dòng)畫(huà)操作么?
  3. activity應(yīng)處理從網(wǎng)絡(luò)返回的結(jié)果么?
  4. activity需要做不同狀態(tài)下view的狀態(tài)的控制么?

我相信大家都有了答案吧。為了解決activity臃腫和含義不清的矛盾,慢慢出現(xiàn)了mvp規(guī)范。

1.3 MVP

無(wú)標(biāo)題繪圖 (3).png-9.4kB
無(wú)標(biāo)題繪圖 (3).png-9.4kB

mvp做的事情也相當(dāng)簡(jiǎn)單,僅僅是把mvc做了一個(gè)小小的改造,產(chǎn)生了清晰的封層。
【流程】view操作p,p會(huì)去調(diào)用model執(zhí)行操作,p中接收到結(jié)果后去調(diào)用v來(lái)更新界面。

下面是某個(gè)使用mvp的項(xiàng)目中activity的代碼:

presenter = new AppInfoPresenter(); // p層

mShowPackageNameBtn.setOnClickListener(v -> {
    v.setEnabled(false); // activity變成v層,這里控制view的相關(guān)狀態(tài)
    
    // 點(diǎn)擊后的事情交給p做,p做完后應(yīng)該給v一個(gè)回調(diào)。為了說(shuō)明簡(jiǎn)單,這里是同步回調(diào)。
    string name = presenter.getPackageInfo(getApplication()); // p層將最終的結(jié)果交給v層
    
    mShowPackageNameBtn.setText(name); // 得到回調(diào)后更新視圖
});

這種方式將activity和xml文件變成了一個(gè)v,那么所有邏輯都交由p處理。這樣的好處就是model對(duì)外層不知情,p對(duì)view不知情。成為了這樣的一個(gè)蛋形結(jié)構(gòu):

無(wú)標(biāo)題繪圖 (4).png-19.3kB
無(wú)標(biāo)題繪圖 (4).png-19.3kB

內(nèi)層對(duì)外層不知情的好處就是內(nèi)層可以隨意地做復(fù)用,壞處就是需要建立相互通信的機(jī)制,會(huì)帶來(lái)各種回調(diào)。當(dāng)然,你可以用Rx的方式很簡(jiǎn)單地做回調(diào),但是我們是否真的應(yīng)該采用這種到處拋出回調(diào)的方式呢?

先別急,我們先看看如果上面的例子變復(fù)雜的情況:
我希望點(diǎn)擊按鈕后,出現(xiàn)一個(gè)loading界面,p去做網(wǎng)絡(luò)請(qǐng)求。無(wú)論成功與否,請(qǐng)求回來(lái)后都停止loading。如果成功得到了網(wǎng)絡(luò)數(shù)據(jù),才更新頁(yè)面。

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

可以看出mvp中一個(gè)很蛋疼的后遺癥————各種回調(diào)。而且這些回調(diào)都是要自己根據(jù)不同頁(yè)面寫(xiě)的,而且每寫(xiě)一個(gè)回調(diào)就要寫(xiě)一個(gè)接口,接口的參數(shù)也要根據(jù)需求變。我敢保證絕對(duì)沒(méi)辦法一次性知道這個(gè)回調(diào)方法需要幾個(gè)參數(shù),而參數(shù)的類型也不能一次性就確定。

我覺(jué)得太麻煩了,我要簡(jiǎn)單一些。比如讓P對(duì)v知曉,v也知道p的存在,會(huì)不會(huì)簡(jiǎn)單?趕快來(lái)看看這種方案是什么樣的。

首先讓activity實(shí)現(xiàn)某個(gè)接口比如IAppInfoUi,然后讓P調(diào)用這個(gè)接口對(duì)象進(jìn)行交互。
Activity:

presenter = new AppInfoPresenter(); // p層

protected void onCreate(){}
mShowPackageNameBtn.setOnClickListener(v -> {
    v.setEnabled(false);
    // 點(diǎn)擊后的事情交給p做,不會(huì)給view回調(diào)
    presenter.getPackageInfo(getApplication());
});
}

/**
 * public方法,讓p去調(diào)用
 */
public void onGotPkgInfo(String name){
    // 得到結(jié)果
}

Presenter:

public class AppInfoPresenter extends BasePresenter<IAppInfoUi> implements IAppInfoP {

    @Override
    public void getPackageInfo(Context context) {
        // p對(duì)v知情,直接調(diào)用v中的public方法(onGotPkgInfo())。
        
        // getView其實(shí)得到的就是activity的接口對(duì)象,即IAppInfoUi
        getView().onGotPkgInfo(context.getPackageName()); 
    }
}

現(xiàn)在,p和v的交互也不用各種回調(diào)了(將activity整體變成了一個(gè)回調(diào)接口)。那,這種方式有不會(huì)有什么問(wèn)題?
p知道了v,那么p的復(fù)用性就喪失了。正如你所見(jiàn),我利用接口來(lái)降低了二者互相知情造成的影響。但這樣,你就必須在寫(xiě)view的時(shí)候定義接口,但如果你這個(gè)頁(yè)面根本沒(méi)復(fù)用價(jià)值,你還要做接口么。如果不做接口,你怎么知道這個(gè)頁(yè)面真的沒(méi)復(fù)用價(jià)值呢?而且接口的名字和參數(shù)你真的可以一次確定么?

好,我們偷懶一下,先看看能否不定義接口。
Presenter:

public class Presenter {
    // 注意這里的activity的類型是具體的而不是接口
    public MainActivity mActivity;

    public void loadData() {
        // request network 做數(shù)據(jù)處理
        HttpUtil.doPostAsync("http://www.baidu.com", "kale", (result)-> {
                mActivity.onGotData(result); // 拋數(shù)據(jù)給v層
            }
        });
    }
}

Activity:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        final Presenter presenter = new Presenter(this); // presenter
        button.setOnClickListener(v-> 
            presenter.loadData(); // p完全不知道這是因?yàn)辄c(diǎn)擊而觸發(fā)的動(dòng)作,只知道要加載數(shù)據(jù)了
        );
    }

    /**
     * 被presenter調(diào)用的方法
     */
    public void onGotData(String name) {
        button.setText(name); // 更新視圖
    }
}

用這種方式p中包含了v對(duì)象,可以不寫(xiě)任何回調(diào)就能直接觸發(fā)v的動(dòng)作,而且不用寫(xiě)接口和回調(diào),甚至還可以支持一個(gè)v對(duì)應(yīng)多個(gè)p的需求。但是如果你這個(gè)view被復(fù)用了,就得改一改。這種方案的一大壞處就是靈活性會(huì)比較低,但實(shí)際運(yùn)用下來(lái)還不錯(cuò),算是一種偷懶的做法。

順便提及一下mvp的重要優(yōu)點(diǎn):

  1. 當(dāng)activity意外重啟時(shí)presenter不會(huì)被重啟。
  2. activity重啟時(shí),presenter與activity會(huì)重新綁定,根據(jù)數(shù)據(jù)恢復(fù)activity狀態(tài)。
  3. 而當(dāng)activity真正銷毀時(shí),對(duì)應(yīng)地presenter應(yīng)該隨之銷毀

這樣的好處可以解決以下2個(gè)很實(shí)際的問(wèn)題:
不會(huì)每次翻轉(zhuǎn)屏幕都去顯示進(jìn)度條,重新加載數(shù)據(jù)。
某些低端機(jī),內(nèi)存不足時(shí)activity被銷毀,但p會(huì)持有數(shù)據(jù),避免數(shù)據(jù)丟失。

1.4 谷歌的MVVM

我們看到了上述mvp的兩種實(shí)現(xiàn)方案:
第一種靈活但是寫(xiě)起來(lái)復(fù)雜;第二種簡(jiǎn)單,但是靈活性不足。
mvvm模式利用數(shù)據(jù)綁定的特性,自動(dòng)化實(shí)現(xiàn)了第一種的回調(diào)模式,很棒對(duì)不?但也僅此而已。

先來(lái)看看谷歌的dataBinding是怎么做分層處理的吧:
1.先定義一個(gè)User類

public class User { 

    private final String firstName;
    private final String lastName;
 
    public String getFirstName() {
        return firstName;
    } 
 
    public String getLastName() {
        return lastName;
    }
    
    void onSomething(){
        // 這里去加載網(wǎng)絡(luò)
    }
} 

2.在layout文件中綁定user

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

3.在java代碼中設(shè)置實(shí)現(xiàn)和數(shù)據(jù)

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

    binding.setUser(new User());
} 

看完之后,是不是感覺(jué)很精妙。對(duì),就是這么“精妙”,精妙到view都沒(méi)辦法被復(fù)用了。來(lái)看看這行代碼:

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

天哪!你怎么知道這個(gè)布局文件不會(huì)被復(fù)用,如果復(fù)用了,這里如果展示的是一個(gè)ad.info你該怎么處理。這個(gè)先不說(shuō),databinding還支持xml中寫(xiě)java代碼,比如引入靜態(tài)方法和簡(jiǎn)易判斷什么的。

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

干的漂亮,直接讓layout文件和java融為一體?,F(xiàn)在請(qǐng)你告訴我我該怎么復(fù)用這個(gè)layout文件。如果我在這里也寫(xiě)了java的簡(jiǎn)單判斷,java代碼中也寫(xiě)了一些邏輯,我怎么快速定位問(wèn)題。
這么寫(xiě)帶來(lái)的嚴(yán)重后果就是,layout文件和java類強(qiáng)耦合,而且很難定位問(wèn)題。你就永遠(yuǎn)不知道這個(gè)邏輯是在java代碼中寫(xiě)的還是xml中寫(xiě)的了。

注意:我強(qiáng)烈不建議在xml中寫(xiě)java代碼,它會(huì)增加定位問(wèn)題的難度。

1.5 理想化的MVVM

理想化的mvvm最好是只用寫(xiě)少量代碼就完成了具體需求的東西,超級(jí)酷!我希望我加載網(wǎng)絡(luò)成功后自動(dòng)會(huì)更新到視圖上。
之前的做法:
建立xml,在activity中找到view,建立數(shù)據(jù)模型,寫(xiě)好網(wǎng)絡(luò)回調(diào),回調(diào)成功后依次設(shè)置view的狀態(tài)。
理想化的做法:
寫(xiě)好數(shù)據(jù)模型,建立xml時(shí)直接綁定數(shù)據(jù)模型,在activity中寫(xiě)好回調(diào)就行。
代碼如下:

數(shù)據(jù)模型:

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

如果真的可以這么寫(xiě),那么一切都會(huì)簡(jiǎn)單很多。三個(gè)文件搞定了model,vm,v層。只是如果真的這么寫(xiě)就會(huì)出現(xiàn)很多問(wèn)題。比如你直接對(duì)json的數(shù)據(jù)模型做了很多的處理,讓model的set和get方法不在純粹。如果你今天不用dataBinding了,以后要改就會(huì)相當(dāng)困難。現(xiàn)在再來(lái)復(fù)雜一點(diǎn),我希望對(duì)name進(jìn)行判斷,根據(jù)name的數(shù)據(jù)不同讓view呈現(xiàn)View.VISIBLE, View.INVISIBLE, View.GONE三種狀態(tài)。

數(shù)據(jù)模型:

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}" // 僅僅添加了這一行
    />

其余的文件不變。

我們?cè)黾恿艘粋€(gè)需求,只需要?jiǎng)觾蓚€(gè)文件,是不是很贊。那么壞處就是對(duì)model的操作太多了,它已經(jīng)不再純粹,可能未來(lái)還會(huì)做更多的事情。為了解決這個(gè)問(wèn)題,谷歌說(shuō)不要再動(dòng)model了,我允許你在xml中寫(xiě)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}"
    />

這樣的做法就可以讓我們不動(dòng)model類,只需要在xml中做邏輯操作。只可惜xml中肯定不能完成所有的視圖操作(比如動(dòng)畫(huà)),而且xml是有復(fù)用價(jià)值的。所以我給出的結(jié)論就是,理想很豐滿,現(xiàn)實(shí)很骨感。

1.6 理想妥協(xié)于現(xiàn)實(shí)后的MVVM

我們不希望一個(gè)框架對(duì)現(xiàn)有的項(xiàng)目結(jié)構(gòu)做太多的影響,框架影響到用于json解析的model是不能容忍的!這樣的話,我們就要建立一個(gè)給框架用的數(shù)據(jù)對(duì)象,所以vm現(xiàn)在就變成了一個(gè)和前面的json的model無(wú)關(guān)的獨(dú)立類。
需要注意的是:
vm僅僅處理和視圖展示內(nèi)容有關(guān)的邏輯,比如對(duì)顯示的內(nèi)容做格式化這樣的事情。除此之外不應(yīng)處理其他的視圖邏輯。

數(shù)據(jù)模型:

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

    ///////////////////////////////////////////////////////////////////////////
    // 事務(wù)操作
    ///////////////////////////////////////////////////////////////////////////

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

布局文件:

// 將之前的userinfo換為vm來(lái)綁定
<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(); // 加載網(wǎng)絡(luò)請(qǐng)求
    }
}

這樣的做法就沒(méi)啥問(wèn)題了,vm處理了邏輯,model變成純粹的類。而且也不用在xml中寫(xiě)什么java代碼了,只要綁定vm的字段即可。view相關(guān)的java代碼都在activity中完成,如果view的邏輯出錯(cuò)了直接進(jìn)入activity中定位即可。

我們注意到了vm中做了加載數(shù)據(jù)的操作,應(yīng)不應(yīng)該這么寫(xiě)呢?
不應(yīng)該!viewModel就是view的數(shù)據(jù)模型,所以不應(yīng)該圖省事,在vm中寫(xiě)和view無(wú)關(guān)的業(yè)務(wù)邏輯。所以下面的這塊代碼不應(yīng)該出現(xiàn)在vm中,而是應(yīng)該放在別的地方。至于放在哪里合適,這就是后話了。

///////////////////////////////////////////////////////////////////////////
    // 事務(wù)操作
    ///////////////////////////////////////////////////////////////////////////

    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的不足和問(wèn)題,但我們也不能否認(rèn)它的好處。它優(yōu)雅的幫我們搞定了回調(diào),而且支持了數(shù)據(jù)的自動(dòng)綁定。基于以上的分析,我做了一個(gè)小小的改造,讓layout和viewModel緊密聯(lián)系,如圖所示:


無(wú)標(biāo)題繪圖 (8).png-11.9kB
無(wú)標(biāo)題繪圖 (8).png-11.9kB

view層:具體的view,activity,fragment等,做ui展示、ui邏輯、ui動(dòng)畫(huà)。
viewModel層:由插件自動(dòng)生成的具體類,是view展示的數(shù)據(jù)的java抽象,僅能被model層直接操作。
model層:非ui層面的業(yè)務(wù)邏輯的實(shí)現(xiàn)。包含網(wǎng)絡(luò)請(qǐng)求,數(shù)據(jù)遍歷等操作,是很多具體類的抽象載體。

這里除了model層的定義很抽象外,其余的v、vm層我都給出了具體類做載體,下面詳細(xì)說(shuō)下model層包含些什么。

model層:
因?yàn)閙odel層是很多具體類的聚合,主要是做和展示無(wú)關(guān)的業(yè)務(wù)邏輯,在其中也會(huì)包含很多具體類。

無(wú)標(biāo)題繪圖 (9).png-20.2kB
無(wú)標(biāo)題繪圖 (9).png-20.2kB

要實(shí)現(xiàn)一個(gè)頁(yè)面的需求,我們可能會(huì)用到很多類,包括工具類和各種庫(kù)。這些類都提供了一個(gè)或多個(gè)功能,我們利用這些功能便可最終實(shí)現(xiàn)需求。而所有的調(diào)用應(yīng)該是由一個(gè)類來(lái)進(jìn)行的,這個(gè)類你可以叫做presenter,也可以叫做別的名字??傊?,它就是一個(gè)執(zhí)行非ui層面邏輯的個(gè)體。
需要注意的是,這個(gè)p和mvp中的p是無(wú)關(guān)的,我僅僅是沒(méi)有找到很好的名字,才稱之為presenter。

二、如何使用Dbinding

2.1 編寫(xiě)layout文件

首先我們先建立一個(gè)xml文件,這里推薦通過(guò)改模板的方式快速建立一個(gè)dbinding風(fēng)格的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>標(biāo)簽里可以由<variable/>標(biāo)簽來(lái)定義viewModel,這個(gè)對(duì)象之后會(huì)和view的屬性進(jìn)行綁定。目前,我們不用管這個(gè)類是否存在,我們只需要定義你想要的類和其對(duì)象的名字即可,比如:

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

定義好了變量名,我們就可以將其內(nèi)部的字段與view的attribute進(jìn)行綁定了:

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

這里我將user中的name字段與text這個(gè)屬性進(jìn)行了綁定。(需要注意,這個(gè)text字段現(xiàn)在我們還沒(méi)創(chuàng)建,僅僅是寫(xiě)了出來(lái))。
現(xiàn)在這個(gè)textview展示的文字就是我們定義好的UserviewModel中的name的值了。通過(guò)對(duì)textview的了解,我們可以推測(cè)出name這個(gè)字段肯定是CharSequence類型的。

2.2 編寫(xiě)layout文件時(shí)的問(wèn)題

1.如何建立layout模板

idea中有個(gè)功能就是可以編輯文件模板,下面演示下如何定義這個(gè)模板。

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

2.為什么數(shù)據(jù)塊是用<data>包裹,為什么叫這個(gè)名字呢?

因?yàn)檫@里的xml變成了兩部分,一個(gè)就是傳統(tǒng)的viewgroup包裹的布局文件,其余就是需要和布局文件綁定的vm。而vm的區(qū)塊用data做命名是比較合適的,因?yàn)関m本身就是數(shù)據(jù)。
這里需要格外注意的是,現(xiàn)在的layout文件已經(jīng)不僅僅是一個(gè)單純的布局文件了,更合理的叫法是視圖+數(shù)據(jù)文件。

3.為什么這里定義變量是用<variable/>標(biāo)簽,而不是別的名字呢?

variable本身就是變量的意思,這個(gè)名稱是很合適的。當(dāng)然如果寫(xiě)過(guò)js的同學(xué),會(huì)更加熟悉它的縮寫(xiě)var。但這里的起名上,大家應(yīng)該不會(huì)有什么爭(zhēng)議和容易理解錯(cuò)誤的地方。

4.為什么是通過(guò)type和name這兩個(gè)屬性來(lái)定義一個(gè)vm?

在java世界中我們是通過(guò):

private org.kale.viewModel vm

來(lái)做變量的定義的。在xml文件中不存在什么相互調(diào)用的情況,定義的變量都是private的,所以省略了private這個(gè)關(guān)鍵字。至于,為什么變量的對(duì)象名用type,名字用name,這也是有原因的。

為了說(shuō)清楚這個(gè)問(wèn)題,我們先來(lái)看看jdk中Field這個(gè)類的部分代碼。

package java.lang.reflect;

public final class Field extends AccessibleObject implements Member {
    private Class<?> clazz;
    private int slot;
    private String name; // 參數(shù)的名字【name】
    private Class<?> type;  // 參數(shù)的類型【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;  
    // 省略...  
}

可見(jiàn),這里的命名是參照了java中field的命名來(lái)的。這種命名方式會(huì)比較符合大眾直覺(jué),明白了這個(gè)原因,相信以后定義的時(shí)候就不會(huì)有什么疑惑了。

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

既然我們的viewModel會(huì)和view進(jìn)行綁定,而且view是有可能被復(fù)用的。所以這里的取名我強(qiáng)烈建議和view的意義有關(guān)。千萬(wàn)不可脫離view的意義隨便取名字,這樣以后你用的時(shí)候就會(huì)很麻煩。簡(jiǎn)單來(lái)說(shuō),你完全可以參考之前給view取id的做法來(lái)給viewModel取名字。

6.viewModel的type應(yīng)該怎么寫(xiě)?

既然我們名字搞定了,那么類名基本就出來(lái)了。這里需要注意的是,這里的類名是包含完整包名的。我強(qiáng)烈建議所有的viewModel都在一個(gè)包中,不要隨便放。因?yàn)関iewModel以后可能會(huì)出現(xiàn)重命名和被刪除的情況,放在一個(gè)包下面方便管理。這里我是在vm這個(gè)包下面放所有的viewModel,所以就取了org.kale.vm.UserviewModel這樣一個(gè)名字。至于其他的viewModel的包名前綴我們也可以規(guī)范為org.kale.vm

7.可以在同一個(gè)xml中寫(xiě)兩個(gè)相同類型的viewModel么?

可以,但沒(méi)必要!
在java中會(huì)出現(xiàn)這樣的情況:

String firstName;
String lastName;

在一個(gè)類文件中定義了兩個(gè)相同類型(String)的field。但是在xml中這種情況是嚴(yán)格禁止的,也是完全沒(méi)必要的。因?yàn)関iewModel是一個(gè)有明確含義的對(duì)象,并不是基本類型。而且其綁定地view也是特定的,完全沒(méi)必要定義兩個(gè)相同類型的viewModel。順便提一下,兩個(gè)不同類型的viewModel的name必須是不同的,這個(gè)和java的規(guī)則完全一致的。

8.真的不用寫(xiě)具體的viewModel類么?

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

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

后,這個(gè)UserviewModel會(huì)通過(guò)插件(或通過(guò)快捷鍵)出現(xiàn)在相應(yīng)的包下,你定義好就可以直接用了,沒(méi)必要關(guān)心具體的實(shí)現(xiàn)。這也是dbinding插件的一個(gè)強(qiáng)大之處!

9.如何在別的xml中復(fù)用已經(jīng)定義好的類?

viewModel的一大特性就是可復(fù)用,viewModel和view都可以是多對(duì)多的關(guān)系。比如你這個(gè)UserviewModel中包含了username這個(gè)字段,而別的xml文件中正好需要這個(gè)UserviewModel,你完全可以把username定義到那個(gè)xml文件中。這樣兩個(gè)xml文件就會(huì)共用一個(gè)共同類型的數(shù)據(jù)。復(fù)用的方式很簡(jiǎn)單,就是在別的xml文件中寫(xiě)上

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

就行。

10.如何知道一個(gè)vm是否已經(jīng)存在?

目前如果已經(jīng)存在的vm會(huì)有代碼提示的,如果沒(méi)有代碼提示,并且報(bào)紅的就是之前沒(méi)有的vm。

11.可以在一個(gè)xml文件中定義多個(gè)viewModel么?

當(dāng)然可以,正如我所說(shuō)的:viewModel是和view綁定的。一個(gè)界面中有多個(gè)不同模塊的view是很常見(jiàn)的,遇到這樣的情況你完全可以在xml中定義多個(gè)viewModel。要知道viewModel和view都是多對(duì)多的關(guān)系。

12.viewModel如何做全局改名、刪除、移動(dòng)這樣的重構(gòu)操作?

1.改名和改包名

這個(gè)就是和重構(gòu)相關(guān)的問(wèn)題了。
我們?cè)趚ml中通過(guò)插件產(chǎn)生了viewModel,但如果要改包名和改變viewModel的類名的時(shí)候,最簡(jiǎn)單快捷的方式是,進(jìn)入到這個(gè)類的實(shí)體中,通過(guò)idea的重構(gòu)工具進(jìn)行修改。這樣所有的改動(dòng)會(huì)自動(dòng)同步到使用這個(gè)類的xml文件中了。當(dāng)然,你也可以在這個(gè)類被調(diào)用的地方通過(guò)重構(gòu)工具進(jìn)行改名。

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

2.刪除

至于刪除某個(gè)viewModel也是一樣的,仍舊是對(duì)java類進(jìn)行操作。刪除的時(shí)候注意排查下用到的地方,以免出錯(cuò)。


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

13.viewModel中的字段如何做改名、刪除這樣的操作?

1.改名

我們先來(lái)看下插件會(huì)給我們通過(guò)我們的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方法。如果我們突然想把這個(gè)“name”改名為“nickname”,或者是刪除這個(gè)name字段呢?做法就是直接重構(gòu)name這個(gè)字段。
下面為了演示方便,減少干擾選項(xiàng)。我把name這個(gè)過(guò)于通用的字母先改為了nickname。我將演示如何將nickname通過(guò)idea的重構(gòu)工具改為name。

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

2.刪除

因?yàn)閕dea對(duì)于databinding的支持力度很低(未來(lái)或許就可以通過(guò)重構(gòu)工具做了),所以在重構(gòu)字段的時(shí)候只能我們自己去排查了。我的排查方案是通過(guò)檢索當(dāng)前類使用過(guò)的地方,來(lái)看下使用當(dāng)前類的xml中有沒(méi)有使用過(guò)我要?jiǎng)h除的字段,如果有就進(jìn)行處理,如果沒(méi)有就直接刪除。以此來(lái)避免刪除后出現(xiàn)程序出錯(cuò)的問(wèn)題。


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

14.如果我不想通過(guò)插件生成vm,可以手動(dòng)寫(xiě)么?

當(dāng)然可以的,只需要在xml中加入ignore="true"就好。這樣插件就會(huì)忽略這個(gè)vm,交給開(kāi)發(fā)者自己去建立。

<variable
    name="vm"
    ignore="true"  // 加上這個(gè)屬性就會(huì)被插件無(wú)視掉
    type="kale.db.ignore.IgnoredviewModel"
    />

15.如果插件生成的viewModel的屬性不能滿足我的要求,我可以自己配置生成規(guī)則么?

可以的,只需要做下面兩步:
1.在項(xiàng)目中的values下的dbinding_config.xml文件中,增加插件生成的規(guī)則:

<?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"
        
        下面這個(gè)就是我自定義的規(guī)則,如果屬性名是customAttr,那么vm中的字段類型是CharSequence
    -->

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

</resources>

2.在隨便一個(gè)類中寫(xiě)入如下的java代碼:

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

這樣插件在自動(dòng)生成代碼的時(shí)候就會(huì)讀取你新加入的規(guī)則,生成你想要的字段類型了。

2.3 編寫(xiě)java代碼

上面說(shuō)了那么多的layout文件的編寫(xiě)策略,現(xiàn)在該說(shuō)下如何寫(xiě)java代碼了。java代碼主要做的工作就是綁定vm和layout文件,其余的操作就直接對(duì)vm進(jìn)行就行了。對(duì)于復(fù)雜的界面,可以把ui無(wú)關(guān)的邏輯放入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;
    }

    /**
     * 這個(gè)當(dāng)然可以放在構(gòu)造方法中進(jìn)行,我這里為了說(shuō)明view層調(diào)用vm的方法,強(qiáng)制加入了一個(gè)回調(diào)。
     */
    public boolean init(Context context) {
        mUserviewModel.setPic(BitmapFactory.decodeResource(context.getResources(),
            R.drawable.mingren));
        mUserviewModel.setName("漩渦鳴人");  // textview中就會(huì)自動(dòng)渲染出文字了
        return true;
    }

    public void loadData(){
        // 模擬加載網(wǎng)絡(luò)成功,將名字更新的操作
        mUserviewModel.setName("kale");
    }

    // java層面的測(cè)試,不用安裝apk
    public static void main(String[] args) {
        UserviewModel userviewModel = new UserviewModel();
        MainPresenter model = new MainPresenter(userviewModel);
    }
}

2.4 編寫(xiě)java代碼時(shí)的問(wèn)題

1.presenter會(huì)被復(fù)用么?

會(huì),但是極少。因?yàn)閜是和某個(gè)特定的邏輯極其相關(guān)的,因此復(fù)用p的情況十分少見(jiàn)。

2.v和p的關(guān)系是什么樣的?

p可以做很多和業(yè)務(wù)邏輯相關(guān)的操作,但是vm必須是純粹的,vm對(duì)p完全不知情。
一個(gè)p會(huì)操作多個(gè)vm,一個(gè)vm也會(huì)被多個(gè)p使用。
注意:在本框架中vm是由框架自動(dòng)生成的,強(qiáng)烈不建議對(duì)vm直接做增添與業(yè)務(wù)邏輯相關(guān)的事情。

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

需要。因?yàn)閍ctivity中肯定會(huì)有不同的ui區(qū)塊,fragment既可以劃分不同的ui區(qū)域,又可以做到讓這些細(xì)顆粒度的ui能夠被復(fù)用,因此還是需要fragment來(lái)幫忙解耦ui,給activity減負(fù)的。

4.dbinding能否支持雙向綁定?

不準(zhǔn)備支持。因?yàn)関iew的事件和vm的數(shù)據(jù)綁定其實(shí)是無(wú)關(guān)的,而且谷歌db的設(shè)計(jì)思路,本身就是單向的。如果非要套雙向綁定,我不能確保支持所有的view,所以目前是不準(zhǔn)備支持的。

5.p對(duì)vm的操作是否必須在ui線程中?

可以在任何線程中操作vm,再也不用切回主線程操來(lái)操作作ui了。

6.如果p中的某些操作需要通知給activity,該怎么處理?

強(qiáng)烈建議用回調(diào)的方式做通知,不要把a(bǔ)ctivity通過(guò)構(gòu)造方法傳給p。如果傳了,就需要注意持有activity對(duì)象的問(wèn)題,小心造成內(nèi)存泄漏。

順便問(wèn)一句,為什么fragment必須需要持有activity的引用呢?
首先,fragment也是ui,fragment中需要有很多和context有關(guān)的操作。比如啟動(dòng)activity什么的,所以需要持有activity對(duì)象來(lái)做這些事情。最重要的是,fragment本身是可以對(duì)用戶行為產(chǎn)生事件的,而p絕對(duì)不會(huì)自己產(chǎn)生事件,必須通過(guò)外部觸發(fā)才行。因此p完全可以走純回調(diào)的方式,不必持有任何全局的context對(duì)象。

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

這個(gè)例子中,我通過(guò)參數(shù)傳入context,利用return返回結(jié)果。例子雖然簡(jiǎn)單,但對(duì)于復(fù)雜的情況也是是如此。如果你覺(jué)得回調(diào)寫(xiě)起來(lái)很麻煩,不妨試試用rxJava的形式做。

7.在mvvm框架中應(yīng)該有什么編碼思路?

應(yīng)該有明確的分層思路。在mvvm中我們應(yīng)該把所有數(shù)據(jù)同步的事情交給框架,而不是自己去維護(hù)。將v層的邏輯(如:動(dòng)畫(huà),控件A改變引起的控件B改變等)獨(dú)立寫(xiě)出;在p中獨(dú)立寫(xiě)出數(shù)據(jù)對(duì)viewModel產(chǎn)生影響的邏輯。

下面舉個(gè)adapter的例子:

    /**
     * 數(shù)據(jù)改變后ui會(huì)做一些改變。
     * 應(yīng)該利用對(duì)vm的字段監(jiān)聽(tīng)的方式做處理,不應(yīng)該在數(shù)據(jù)改變時(shí),通過(guò)開(kāi)發(fā)者做ui層面的更新。
     *
     * @param bind 為什么不是單一監(jiān)聽(tīng)器,而是觀察者模式?
     *             因?yàn)闀?huì)有多個(gè)東西對(duì)同一個(gè)數(shù)據(jù)進(jìn)行監(jiān)聽(tīng),如果是單一的就沒(méi)辦法實(shí)現(xiàn)這個(gè)功能。
     */
    public void notifyData(final NewsItemBinding bind) {
        mviewModel.addOnPropertyChangedCallback((sender, propertyId)-> {
                // 監(jiān)聽(tīng)title的改變,然后設(shè)置文字
                if (propertyId == kale.db.BR.title) {
                    setSmartText(bind.title, mviewModel.getTitle());
                }
            }
        });
    }
    
    /**
     * 如果有數(shù)據(jù),那么就顯示textView;
     * 如果沒(méi)數(shù)據(jù),那么就讓textView消失
     */
    public void setSmartText(TextView textView, CharSequence text) {
        textView.setVisibility(!TextUtils.isEmpty(text) ? View.VISIBLE : View.GONE);
    }

在數(shù)據(jù)來(lái)的時(shí)候,數(shù)據(jù)僅僅對(duì)vm進(jìn)行綁定,不用考慮綁定后ui層面的邏輯:

    ///////////////////////////////////////////////////////////////////////////
    // 這里就僅僅做數(shù)據(jù)和ui的綁定工作了,不用想ui層面的任何邏輯,這個(gè)算是數(shù)據(jù)層面的綁定工作。
    ///////////////////////////////////////////////////////////////////////////
    
    /**
     * 將viewModel和model的數(shù)據(jù)進(jìn)行同步
     * model模型可能很復(fù)雜,但viewModel的模型很簡(jiǎn)單,這里就是做二者的轉(zhuǎn)換。
     */
    @Override
    public void handleData(NewsInfo data, int pos) {
        mviewModel.setTitle(data.title);
    }

8.如果我這個(gè)界面本身就沒(méi)有復(fù)用價(jià)值,能不能不用viewModel?

我們知道vm算是對(duì)xml文件的一種抽象,那么如果我這個(gè)界面本身就沒(méi)復(fù)用價(jià)值,能不能直接把p當(dāng)作vm,直接綁定p中的字段呢?
這樣做當(dāng)然是可行的。但問(wèn)題就在于,你真的可以確保某個(gè)xml沒(méi)有復(fù)用價(jià)值么?如果你當(dāng)前認(rèn)為無(wú)復(fù)用價(jià)值的xml,以后卻要被復(fù)用了,那么你之前偷懶做的事情,對(duì)以后的人來(lái)說(shuō)就是災(zāi)難。雖然需要改一兩行代碼沒(méi)啥問(wèn)題,但對(duì)于程序設(shè)計(jì)來(lái)說(shuō),你之前的設(shè)計(jì)方案和現(xiàn)在的需求產(chǎn)生了沖突,就得重新?lián)Q設(shè)計(jì)思路,這其實(shí)是有些嚴(yán)重的。

在android設(shè)計(jì)之初就給出了xml文件和java代碼分離的編碼方案。但仍舊允許在java代碼中通過(guò)new出view來(lái)寫(xiě)ui。xml和java分離的設(shè)計(jì)方案就是強(qiáng)制復(fù)用的思路,而activity中手動(dòng)寫(xiě)view的編碼方案就是所謂的無(wú)復(fù)用思路。二者一類比,你就知道你需不需要寫(xiě)vm了。

9.在Activity中應(yīng)該做什么事情?

Activity中應(yīng)該做一些view的配置工作。比如配置recyclerView的layoutmanager,設(shè)置下下拉刷新,view的動(dòng)畫(huà)效果等等。
如果你的view的某種狀態(tài)的改變會(huì)引起其他view的改變,這個(gè)邏輯操作也需要放入activity中。很常見(jiàn)例子的就是,輸入密碼框的旁邊有個(gè)是否顯示明文密碼的按鈕,點(diǎn)擊按鈕會(huì)把密碼已明文的形式顯示出來(lái),再點(diǎn)就變成了密文。這個(gè)東西是絕對(duì)屬于ui邏輯的范疇,所以應(yīng)該寫(xiě)在activity中,并且不應(yīng)影響到vm和p。

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

我們?nèi)耘f需要id,只是不再需要findViewById了!
我們通過(guò)監(jiān)聽(tīng)vm的某個(gè)字段來(lái)做相應(yīng)的操作,但如上一個(gè)問(wèn)題所說(shuō)到的,密碼是否顯示的按鈕和輸入框的相互作用是不應(yīng)該用vm做的。所以,在ui層面的邏輯中肯定還會(huì)有大量的id出現(xiàn)。幸好,databinding幫我們自動(dòng)生成了id對(duì)象,再也不用寫(xiě)findViewById了。

11.Adapter應(yīng)該放在哪里?

adapter的位置比較尷尬,而且復(fù)用價(jià)值不高,實(shí)踐了很久后,我推薦放入p中。在dbinding中,我提供了ObservableArrayList這個(gè)list做數(shù)據(jù)的處理?,F(xiàn)在只需要對(duì)list進(jìn)行操作即可,不用關(guān)心界面的更新問(wèn)題。notifyDataSetChanged()會(huì)自動(dòng)執(zhí)行。

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); // 再也不用手動(dòng)寫(xiě)notifyDataSetChanged()了
}

12.什么該在xml中定義vm的字段?

當(dāng)數(shù)據(jù)的變化會(huì)“直接”引起view的某個(gè)屬性改變,那么就應(yīng)該在layout中寫(xiě)一個(gè)vm的字段進(jìn)行綁定。如果這個(gè)view的某個(gè)狀態(tài),是根據(jù)view其他的狀態(tài)改變而改變的,和數(shù)據(jù)層無(wú)關(guān)。那么就不應(yīng)該定這個(gè)字段,而是用監(jiān)聽(tīng)vm字段的方式來(lái)做。
監(jiān)聽(tīng)的方案:

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.如果兩個(gè)頁(yè)面需要同步很多數(shù)據(jù),可以直接共用vm么?

當(dāng)然可以!vm自身的自動(dòng)綁定特性會(huì)讓兩個(gè)頁(yè)面共用數(shù)據(jù)變得十分簡(jiǎn)單,可以通過(guò)viewModel.toSerializable()來(lái)將其序列化,然后在接收的地方通過(guò):
NewsviewModel vm = NewsviewModel.toviewModel(getIntent().getSerializableExtra(KEY));
得到它。得到后,你就可以方便的利用上個(gè)頁(yè)面?zhèn)鱽?lái)的vm進(jìn)行l(wèi)ayout層面的綁定了。
雖然這種方式十分簡(jiǎn)單,但不要濫用,它僅僅針對(duì)于兩頁(yè)面是含有共同vm的情況,其他情況我還是推薦通過(guò)回調(diào)、廣播、事件總線等方式做。要記得,vm雖好,但它不是萬(wàn)能的。

請(qǐng)測(cè)試在低內(nèi)存中這種方案會(huì)不會(huì)有bug。

14.如何對(duì)自動(dòng)生成的vm做定制

插件僅僅能生成普通情況下的vm,它不可能知道你具體的邏輯和特殊需求。如果你要由這樣的需求,可以在new出vm的時(shí)候通過(guò)重載set方法來(lái)實(shí)現(xiàn)。

mUserVm = new UserViewModel() {
    @Override
    public void setName(CharSequence name) {
        // 對(duì)于復(fù)雜的ui需求,可以重載對(duì)應(yīng)的set方法,不應(yīng)該重載get方法
        super.setName(String.format(name, "kale", "saber"););
    }
};

15.view的事件該如何綁定

因?yàn)関m僅僅是view的字段,vm的字段也應(yīng)該和數(shù)據(jù)保持一致的,這時(shí)候你就會(huì)發(fā)現(xiàn)view的事件是不應(yīng)該做vm綁定的,因?yàn)樗皇菙?shù)據(jù)。但,為了節(jié)約findviewById和配置監(jiān)聽(tīng)器的代碼,我提供了一個(gè)event對(duì)象來(lái)做界面的事件綁定。界面中所有view對(duì)象的事件都交給它來(lái)做就行。

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, "點(diǎn)擊了頭像", Toast.LENGTH_SHORT).show();
    }
});

相比起之前的做法,是必須要findviewbyid找到這個(gè)控件,然后設(shè)置監(jiān)聽(tīng)器,為了一個(gè)頁(yè)面共用listener,減少代碼。就必須寫(xiě)成這樣:

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

其實(shí)這樣的問(wèn)題就在于view的設(shè)計(jì)監(jiān)聽(tīng)和實(shí)現(xiàn)是脫離的,你必須要進(jìn)行跳轉(zhuǎn)才能找到監(jiān)聽(tīng)器的實(shí)現(xiàn),沒(méi)有聚合好。

避免NullPointerException

自動(dòng)生成的 data binding 代碼會(huì)自動(dòng)檢查和避免 null pointer exceptions。舉個(gè)例子,在表達(dá)式 @{user.name} 中,如果 user 是 null,user.name 會(huì)賦予默認(rèn)值 null。如果你引用了 user.age,因?yàn)?age 是 int 類型,所以默認(rèn)賦值為 0

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容