本文會不定期更新,推薦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ā)者對于一個框架最看重的是下面幾點:
- 能加快開發(fā)速度,屏蔽底層細(xì)節(jié)
- 代碼可讀性好,易維護
- 代碼量越少越好,易閱讀
- bug少,有不錯的健壯性
三、實現(xiàn)
指定明確的分層
如果一個項目有了明確的分層結(jié)構(gòu),那么代碼的可讀性和可維護性會上升很多,它也是一個架構(gòu)的基礎(chǔ)。分層良好的的優(yōu)點有很多,而且即使某天要更換框架,也不會傷筋動骨。
需要格外注意的是:只有當(dāng)一個項目的成員都能明確項目的層級后才可以談框架和模式,否則一個框架再優(yōu)秀也無法在混亂中發(fā)揮出優(yōu)勢。
層名 | 內(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提供的代碼模板功能。

下面就是創(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之外,這樣既加快了代碼的編寫速度又方便定位問題。

目前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)工具進行改名。

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

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ù)必讓你的團隊接受并了解這種機制,否則會給別人帶來困擾的。這里我仍舊是通過代碼模板的方式進行快速編寫:

利用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"));
}
禁止一切容易出錯的操作
強類型語言和弱類型語言的一個差異(僅僅是差異)就是在于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?