DataBinding最佳實踐

本文會不定期更新,推薦watch下項目。如果喜歡請star,如果覺得有紕漏請?zhí)峤籭ssue,如果你有更好的點子可以提交pull request。
本文的示例代碼主要是基于作者的經(jīng)驗來編寫的,若你有其他的技巧和方法可以參與進來一起完善這篇文章。文章中大量參考和引用了DBinding權(quán)威使用指南的內(nèi)容,如果你想了解更多建議深入閱讀一下DBinding權(quán)威使用指南

說明:下文中vm是view model的縮寫

本文固定連接:https://github.com/tianzhijiexian/Android-Best-Practices

一、需求背景

開發(fā)者都希望可以更快更簡單地編寫代碼,并且還希望代碼的可維護性和健壯性能符合團隊的期望。很多初創(chuàng)團隊在發(fā)展多年后逐漸認(rèn)識到了早期代碼模式的弊端,并且在代碼的組織結(jié)構(gòu)上有了很多思考。
在模式方面,2015年大家開始爭相討論mvc,mvp,mvvm,期間谷歌也推出了自家的數(shù)據(jù)綁定框架databinding,借此來簡化代碼的編寫。在這一片百家爭鳴中,開發(fā)者十分希望能找到一個滿足項目需求并且穩(wěn)定可靠的框架來簡化開發(fā)工作。

二、需求

開發(fā)者對于一個框架最看重的是下面幾點:

  1. 能加快開發(fā)速度,屏蔽底層細(xì)節(jié)
  2. 代碼可讀性好,易維護
  3. 代碼量越少越好,易閱讀
  4. bug少,有不錯的健壯性

三、實現(xiàn)

指定明確的分層

如果一個項目有了明確的分層結(jié)構(gòu),那么代碼的可讀性和可維護性會上升很多,它也是一個架構(gòu)的基礎(chǔ)。分層良好的的優(yōu)點有很多,而且即使某天要更換框架,也不會傷筋動骨。
需要格外注意的是:只有當(dāng)一個項目的成員都能明確項目的層級后才可以談框架和模式,否則一個框架再優(yōu)秀也無法在混亂中發(fā)揮出優(yōu)勢。

mvc
層名 內(nèi)容
view層 具體的view,activity,fragment等,做ui展示、ui邏輯、ui動畫
vm層 具體的視圖模型類,是view展示的數(shù)據(jù)的java映射,能被model層直接操作
model層 非ui層面的業(yè)務(wù)邏輯的實現(xiàn)。包含網(wǎng)絡(luò)請求,數(shù)據(jù)遍歷等操作,是很多具體類的抽象載體

DBinding是一個databinding的擴展類,它提供了快速綁定vm和通過vm維持多個頁面之間數(shù)據(jù)同步等功能,并且它還有強大的as插件來做支持,因此本文將選擇它作為mvvm框架。

通過數(shù)據(jù)來更新UI

目前流行的做法都是通過數(shù)據(jù)來驅(qū)動UI,其優(yōu)點在于方便做單元測試和多人協(xié)作,對bug的定位也有比較好的幫助。mvvm是一個抽象的概念,它目前最穩(wěn)定可靠的實現(xiàn)就是databinding,在用databinding之后,我已經(jīng)很少到view層定位bug了。databinding的代碼由xml代碼和java代碼構(gòu)成。
layout:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <!-- 定義變量: private org.kale.vm.UserViewModel user -->
        <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) {
    
        DBinding.bindViewModel(this, R.layout.activity_main, mUserVm);
        mUserVm.setName("kale");  // textview中就會自動渲染出文字了
    }

layout文件中的vm取名應(yīng)該和layout文件名字有關(guān)聯(lián),layout文件的名字也應(yīng)該和activity的名字有關(guān),這樣可以方便定位問題和查找邏輯。layout中vm的參數(shù)完全可以模仿之前取id名字的思路,只不過千萬不要加view的縮寫,出現(xiàn)tv_username或username_tv就鬧笑話了。layout文件中強烈不建議寫import語句,vm類名強制寫全稱。至于java代碼就十分簡單了,沒有過多的要求,只要對vm操作即可更新ui。

通過代碼模板快速生成layout文件

為了快速產(chǎn)生mvvm的layout文件,我利用了as提供的代碼模板功能。

快速建立layout模板
快速建立layout模板

下面就是創(chuàng)建好的代碼塊:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <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>  

通過插件自動生成ViewModel

DBinding提供了強大的as插件來生成vm,這樣就強制你不能隨意修改vm的內(nèi)容,將問題屏蔽在了vm之外,這樣既加快了代碼的編寫速度又方便定位問題。

自動生成vm
自動生成vm

目前Dbinding的插件不能也永遠(yuǎn)不可能支持所有view的屬性的綁定,但是你可通過配置的方式來讓其支持更多屬性,下面會演示如何給SimpleDraweeView增加的url的屬性。

在代碼中編寫適配器:

public class NetWorkImageViewAdapter {

    @BindingAdapter({"url"})
    public static void setUrl(SimpleDraweeView view, String url) {
        view.setImageURI(url);
    }

}

在value/dbinding_config.xml中進行配置:

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

    <!-- 
        For Custom view. 
        Example: app:customAttr="name"
    -->

    <string name="drawableStart">android.graphics.Bitmap</string>
    <string name="url">java.lang.String</string>

</resources>

這樣插件便會知道url對應(yīng)的類型,然后進行生成對應(yīng)的vm的field。

歡迎你給DBinding庫提交代碼來讓庫原生支持你想要的屬性

利用ide來對vm進行重構(gòu)操作

因為目前as對于layout中的vm的補全和重構(gòu)的支持力度不足,所以推薦用下列方式進行vm的重構(gòu)工作。

1.改名和改包名
如果要改vm的包名或改vm的類名的時候,最快捷的方式是進入到這個類的實體中,通過ide的重構(gòu)工具進行修改。這樣所有的改動會自動同步到使用了這個類的xml文件中去。當(dāng)然,你也可以在這個類被調(diào)用的地方通過重構(gòu)工具進行改名。

改vm名和改包名
改vm名和改包名

2.刪除
刪除某個vm也是一樣的,仍舊是對java類進行操作。刪除的時候注意排查下用到的地方,以免出錯,這個排查工作真必須是手工做的。

刪除vm
刪除vm

3.給vm中的字段改名
我們先來看下插件會通過我們的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字段呢?最好的做法就是直接重構(gòu)name這個字段。
下面為了演示方便,減少干擾選項,我把name這個過于通用的字母先改成了nickname,現(xiàn)在我將演示如何將nickname改為name。

字段改名
字段改名

4.從vm中刪除字段
因為as對于databinding的支持力度很低(未來或許就可以通過重構(gòu)工具來做了),所以在重構(gòu)字段的時候只能我們自己去排查了。我的排查方案是通過檢索當(dāng)前類使用到的地方,來看下使用當(dāng)前類的xml中有沒有使用過我準(zhǔn)備刪除的字段,如果有就進行處理,如果沒有就直接刪除,以此來避免刪除后出現(xiàn)程序出錯的問題。

刪除字段
刪除字段

禁止在layout中寫復(fù)雜邏輯

databinding原生提供了在xml中寫java語句的能力,也就是它允許你再xml中寫邏輯。這點在DBinding中是強烈禁止的,如果你是通過dbinding的插件來生成vm的,那么你會發(fā)現(xiàn)你幾乎找不到在xml中寫java邏輯的需求。
至于這么做的原因是為了方便定位問題,一旦你將邏輯寫的四分五裂,那么出現(xiàn)了bug后開發(fā)者能否在第一時間知道具體邏輯這個先不談,就說引起bug的可能性就有多個,試錯和排查都會花很多的時間。
如果你的團隊協(xié)作,你把一些邏輯寫到了java中,一些寫到了xml中,閱讀代碼的人必須要能理解這些才能真正的了解你的意圖,此外layout文件是具備復(fù)用能力的,一旦你要復(fù)用layout,那么這些xml中的邏輯便成了其無法復(fù)用的根源,因此我強烈禁止在xml中寫java邏輯。
在實際使用中我會發(fā)現(xiàn)我們經(jīng)常會根據(jù)字段來判斷是否要讓view顯示或隱藏,如果都在java代碼中寫感覺會比較重一些。于是我嘗試在xml中寫了判斷是否顯示的邏輯,后來發(fā)現(xiàn)即使layout被復(fù)用了,這種邏輯也是必然存在的,即使遇到不存在的情況轉(zhuǎn)為java代碼實現(xiàn)也是很簡單的。在定位問題方面,如果知道xml中有這個邏輯的話也還好,所以我目前唯一能允許的就是在xml中寫控制view是否顯示的邏輯代碼,其余的邏輯代碼一律禁止。如果你也準(zhǔn)備這么寫,請務(wù)必讓你的團隊接受并了解這種機制,否則會給別人帶來困擾的。這里我仍舊是通過代碼模板的方式進行快速編寫:

快速寫visibility的邏輯
快速寫visibility的邏輯

利用b代替findViewById

在mvvm時代,我們是否需要id呢?其實,我們?nèi)耘f需要id,只是不再需要findViewById了! 這在DBinding的demo中就有這樣的體現(xiàn):

public abstract class BaseActivity<T extends ViewDataBinding> extends AppCompatActivity{

    protected EventViewModel viewEvents = new EventViewModel();

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

    protected Activity getActivity() {
        return this;
    }

    @LayoutRes
    protected abstract int getLayoutResId();

    protected void bindViews(){
        b = DBinding.bind(this, getLayoutResId());
    }

    protected abstract void beforeSetViews();
    
    protected abstract void setViews();

    protected abstract void doTransaction();
}

在子類中,只需要寫好泛型就行:

public class MainActivity extends BaseActivity<ActivityMainBinding> {}

利用ViewEvent類做事件的統(tǒng)一管理

一個頁面中會有多個view,Button和EditText肯定是會產(chǎn)生事件的,在mvvm中我采用的是事件設(shè)置的代碼在xml中,事件處理代碼在java中的思路。

 <ImageView
    android:layout_width="80dp"
    android:layout_height="80dp"
    android:src="@{user.pic , default= @drawable/speed_icon}"
    android:onClick="@{event.onClick}"
    />
viewEvents.setOnClick(v -> {
    if (v == b.userInfoInclude.headPicIv) {
       // do something
    }
});

定位問題的思路是這樣的,首先你肯定不會懷疑button不會產(chǎn)生事件(如果真的有,那么你的開發(fā)真的開小差了),一般都是按鈕被點擊后的事件觸發(fā)的代碼產(chǎn)生了問題,所以大多數(shù)情況下只需要在觸發(fā)的那段java代碼中下斷點就行了。

利用vm做全局的數(shù)據(jù)同步

兩個頁面間
vm自身的自動綁定特性會讓兩個頁面共用數(shù)據(jù)變得十分簡單,可以通過viewModel.toSerializable()來將其序列化,然后在接收的地方通過:

NewsviewModel vm = NewsviewModel.toviewModel(getIntent().getSerializableExtra(KEY)); 

得到它,現(xiàn)在你就可以方便的利用上個頁面?zhèn)鱽淼膙m進行l(wèi)ayout層面的綁定了。

注意:
雖然這種方式十分簡單,但不要濫用,它僅僅針對于兩頁面有有共同vm的情況,其他情況我還是推薦通過回調(diào)、廣播、事件總線等方式去做。要記得vm雖好,但它不是萬能的。

自動同步點贊事件
自動同步點贊事件

多個頁面間
我們經(jīng)常會有一些全局的數(shù)據(jù),比如紅點消息和用戶信息,這些數(shù)據(jù)我們通常會產(chǎn)生一個靜態(tài)的對象進行存儲,以用戶信息舉例,我們完全可以讓所有用到當(dāng)前用戶信息的頁面用同一個vm,這樣就再也不用考慮多個頁面用戶信息不同步的情況了。至于什么東西可以用這種方式做全局同步,什么不可以,這個就只能看業(yè)務(wù)和團隊成員的把控能力了。

通過注冊數(shù)據(jù)監(jiān)聽來解耦view層

在mvvm中我們應(yīng)該把所有數(shù)據(jù)同步的事情交給框架,而不是自己去維護。將view層的邏輯(如:動畫,控件A文字的改變引起的控件B改變等)獨立寫出,在model中獨立寫出數(shù)據(jù)對vm產(chǎn)生影響的邏輯,下面舉個例子:

    /**
     * 數(shù)據(jù)改變后ui會做一些改變。
     * 應(yīng)該利用對vm的字段監(jiān)聽的方式做處理,不應(yīng)該在數(shù)據(jù)改變時,通過開發(fā)者做ui層面的更新。
     *
     * @param bind 為什么不是單一監(jiān)聽器,而是觀察者模式?
     *             因為會有多個東西對同一個數(shù)據(jù)進行監(jiān)聽,如果是單一的就沒辦法實現(xiàn)這個功能。
     */
    public void notifyData(final NewsItemBinding bind) {
        mviewModel.addOnPropertyChangedCallback((sender, propertyId)-> {
                // 監(jiān)聽title的改變,然后設(shè)置文字
                if (propertyId == kale.db.BR.title) {
                    // do change view
                }
            }
        });
    }

在數(shù)據(jù)來的時候,數(shù)據(jù)僅僅對vm進行綁定,不用考慮ui層面的邏輯:

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

禁止一切容易出錯的操作

forbid

強類型語言和弱類型語言的一個差異(僅僅是差異)就是在于IDE可以幫你做很多限制,databinding本身是相當(dāng)靈活的,支持雙向綁定,支持xml中寫邏輯等操作,但是我這里利用插件或者是其他的方式強烈禁止在xml中寫方法和特殊邏輯,對于import我只允許了View這一個類的import。對于雙向綁定,我建議你在編碼的時候就應(yīng)該有所警惕,最好能有注釋,方便你的同伴進行定位問題。
如果你是一人開發(fā)一個不需要維護的應(yīng)用,那么xml中隨便你怎么寫,但如果你是團隊開發(fā),你會發(fā)現(xiàn)那些在xml中的邏輯很可能是團隊合作的災(zāi)難。當(dāng)然了,如果你已經(jīng)通過某種文檔或者是其他的標(biāo)準(zhǔn)化方式來限制和規(guī)定xml中的邏輯格式,那么我倒是覺得是可行的。

自由是在限制之中的,如果沒有限制那么就沒有社會。

四、總結(jié)

我經(jīng)歷了項目從mvc到mvp,然后變成mvvm,最后到mvpvm的各個階段,在每個階段中我也花了大量的時間去發(fā)現(xiàn)問題解決問題,為后續(xù)的擴展和靈活性做了很多的工作。在做這些事情的時候我漸漸發(fā)現(xiàn),無論你采用什么模式,你都必須有明確的分層的概念,其實大到分層小到單一職責(zé)概念,都是在提升代碼可維護性。在現(xiàn)在這個時期,我的建議是中小型公司可以放心嘗試databinding,大型公司的話因為體量和人員的問題很難會改變模式。當(dāng)然了,如果目前你的代碼本身就有很好的可維護性,我也不建議因為技術(shù)的新穎而動項目,因為我們的目的不是嘗鮮和炫技,而是為了解決問題!

話說,你寫了多少年的findViewById?

developer_kale@foxmail.com
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,362評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,013評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,346評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,421評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,146評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,534評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,585評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,767評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,318評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,074評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,258評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,828評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,486評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,916評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,156評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,993評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,234評論 2 375

推薦閱讀更多精彩內(nèi)容