本文會(huì)不定期更新妄辩,推薦watch下項(xiàng)目吟榴。如果喜歡請(qǐng)star,如果覺得有紕漏請(qǐng)?zhí)峤籭ssue乒验,如果你有更好的點(diǎn)子可以提交pull request尿贫。
本文的示例代碼主要是基于作者的經(jīng)驗(yàn)來(lái)編寫的电媳,若你有其他的技巧和方法可以參與進(jìn)來(lái)一起完善這篇文章。文章中大量參考和引用了DBinding權(quán)威使用指南的內(nèi)容庆亡,如果你想了解更多建議深入閱讀一下DBinding權(quán)威使用指南匾乓。
說(shuō)明:下文中vm是view model的縮寫
本文固定連接:https://github.com/tianzhijiexian/Android-Best-Practices
一、需求背景
開發(fā)者都希望可以更快更簡(jiǎn)單地編寫代碼又谋,并且還希望代碼的可維護(hù)性和健壯性能符合團(tuán)隊(duì)的期望拼缝。很多初創(chuàng)團(tuán)隊(duì)在發(fā)展多年后逐漸認(rèn)識(shí)到了早期代碼模式的弊端,并且在代碼的組織結(jié)構(gòu)上有了很多思考彰亥。
在模式方面咧七,2015年大家開始爭(zhēng)相討論mvc,mvp任斋,mvvm继阻,期間谷歌也推出了自家的數(shù)據(jù)綁定框架databinding,借此來(lái)簡(jiǎn)化代碼的編寫仁卷。在這一片百家爭(zhēng)鳴中穴翩,開發(fā)者十分希望能找到一個(gè)滿足項(xiàng)目需求并且穩(wěn)定可靠的框架來(lái)簡(jiǎn)化開發(fā)工作犬第。
二锦积、需求
開發(fā)者對(duì)于一個(gè)框架最看重的是下面幾點(diǎn):
- 能加快開發(fā)速度,屏蔽底層細(xì)節(jié)
- 代碼可讀性好歉嗓,易維護(hù)
- 代碼量越少越好丰介,易閱讀
- bug少,有不錯(cuò)的健壯性
三鉴分、實(shí)現(xiàn)
指定明確的分層
如果一個(gè)項(xiàng)目有了明確的分層結(jié)構(gòu)哮幢,那么代碼的可讀性和可維護(hù)性會(huì)上升很多,它也是一個(gè)架構(gòu)的基礎(chǔ)志珍。分層良好的的優(yōu)點(diǎn)有很多橙垢,而且即使某天要更換框架,也不會(huì)傷筋動(dòng)骨伦糯。
需要格外注意的是:只有當(dāng)一個(gè)項(xiàng)目的成員都能明確項(xiàng)目的層級(jí)后才可以談框架和模式柜某,否則一個(gè)框架再優(yōu)秀也無(wú)法在混亂中發(fā)揮出優(yōu)勢(shì)嗽元。
層名 | 內(nèi)容 |
---|---|
view層 | 具體的view,activity喂击,fragment等剂癌,做ui展示、ui邏輯翰绊、ui動(dòng)畫 |
vm層 | 具體的視圖模型類佩谷,是view展示的數(shù)據(jù)的java映射,能被model層直接操作 |
model層 | 非ui層面的業(yè)務(wù)邏輯的實(shí)現(xiàn)监嗜。包含網(wǎng)絡(luò)請(qǐng)求谐檀,數(shù)據(jù)遍歷等操作,是很多具體類的抽象載體 |
DBinding是一個(gè)databinding的擴(kuò)展類秤茅,它提供了快速綁定vm和通過(guò)vm維持多個(gè)頁(yè)面之間數(shù)據(jù)同步等功能稚补,并且它還有強(qiáng)大的as插件來(lái)做支持,因此本文將選擇它作為mvvm框架框喳。
通過(guò)數(shù)據(jù)來(lái)更新UI
目前流行的做法都是通過(guò)數(shù)據(jù)來(lái)驅(qū)動(dòng)UI课幕,其優(yōu)點(diǎn)在于方便做單元測(cè)試和多人協(xié)作,對(duì)bug的定位也有比較好的幫助五垮。mvvm是一個(gè)抽象的概念乍惊,它目前最穩(wěn)定可靠的實(shí)現(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中就會(huì)自動(dòng)渲染出文字了
}
layout文件中的vm取名應(yīng)該和layout文件名字有關(guān)聯(lián)诞挨,layout文件的名字也應(yīng)該和activity的名字有關(guān)莉撇,這樣可以方便定位問(wèn)題和查找邏輯。layout中vm的參數(shù)完全可以模仿之前取id名字的思路惶傻,只不過(guò)千萬(wàn)不要加view的縮寫棍郎,出現(xiàn)tv_username或username_tv就鬧笑話了。layout文件中強(qiáng)烈不建議寫import語(yǔ)句银室,vm類名強(qiáng)制寫全稱涂佃。至于java代碼就十分簡(jiǎn)單了,沒有過(guò)多的要求蜈敢,只要對(duì)vm操作即可更新ui辜荠。
通過(guò)代碼模板快速生成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>
通過(guò)插件自動(dòng)生成ViewModel
DBinding提供了強(qiáng)大的as插件來(lái)生成vm伯病,這樣就強(qiáng)制你不能隨意修改vm的內(nèi)容,將問(wèn)題屏蔽在了vm之外否过,這樣既加快了代碼的編寫速度又方便定位問(wèn)題午笛。
目前Dbinding的插件不能也永遠(yuǎn)不可能支持所有view的屬性的綁定膨蛮,但是你可通過(guò)配置的方式來(lái)讓其支持更多屬性,下面會(huì)演示如何給SimpleDraweeView
增加的url的屬性季研。
在代碼中編寫適配器:
public class NetWorkImageViewAdapter {
@BindingAdapter({"url"})
public static void setUrl(SimpleDraweeView view, String url) {
view.setImageURI(url);
}
}
在value/dbinding_config.xml中進(jìn)行配置:
<?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>
這樣插件便會(huì)知道url對(duì)應(yīng)的類型敞葛,然后進(jìn)行生成對(duì)應(yīng)的vm的field。
歡迎你給DBinding庫(kù)提交代碼來(lái)讓庫(kù)原生支持你想要的屬性
利用ide來(lái)對(duì)vm進(jìn)行重構(gòu)操作
因?yàn)槟壳癮s對(duì)于layout中的vm的補(bǔ)全和重構(gòu)的支持力度不足与涡,所以推薦用下列方式進(jìn)行vm的重構(gòu)工作惹谐。
1.改名和改包名
如果要改vm的包名或改vm的類名的時(shí)候,最快捷的方式是進(jìn)入到這個(gè)類的實(shí)體中驼卖,通過(guò)ide的重構(gòu)工具進(jìn)行修改氨肌。這樣所有的改動(dòng)會(huì)自動(dòng)同步到使用了這個(gè)類的xml文件中去。當(dāng)然酌畜,你也可以在這個(gè)類被調(diào)用的地方通過(guò)重構(gòu)工具進(jìn)行改名怎囚。
2.刪除
刪除某個(gè)vm也是一樣的,仍舊是對(duì)java類進(jìn)行操作桥胞。刪除的時(shí)候注意排查下用到的地方恳守,以免出錯(cuò),這個(gè)排查工作真必須是手工做的贩虾。
3.給vm中的字段改名
我們先來(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舰始,現(xiàn)在我將演示如何將nickname改為name。
4.從vm中刪除字段
因?yàn)閍s對(duì)于databinding的支持力度很低(未來(lái)或許就可以通過(guò)重構(gòu)工具來(lái)做了)咽袜,所以在重構(gòu)字段的時(shí)候只能我們自己去排查了丸卷。我的排查方案是通過(guò)檢索當(dāng)前類使用到的地方,來(lái)看下使用當(dāng)前類的xml中有沒有使用過(guò)我準(zhǔn)備刪除的字段酬蹋,如果有就進(jìn)行處理及老,如果沒有就直接刪除抽莱,以此來(lái)避免刪除后出現(xiàn)程序出錯(cuò)的問(wèn)題范抓。
禁止在layout中寫復(fù)雜邏輯
databinding原生提供了在xml中寫java語(yǔ)句的能力,也就是它允許你再xml中寫邏輯食铐。這點(diǎn)在DBinding中是強(qiáng)烈禁止的匕垫,如果你是通過(guò)dbinding的插件來(lái)生成vm的,那么你會(huì)發(fā)現(xiàn)你幾乎找不到在xml中寫java邏輯的需求虐呻。
至于這么做的原因是為了方便定位問(wèn)題象泵,一旦你將邏輯寫的四分五裂寞秃,那么出現(xiàn)了bug后開發(fā)者能否在第一時(shí)間知道具體邏輯這個(gè)先不談,就說(shuō)引起bug的可能性就有多個(gè)偶惠,試錯(cuò)和排查都會(huì)花很多的時(shí)間春寿。
如果你的團(tuán)隊(duì)協(xié)作,你把一些邏輯寫到了java中忽孽,一些寫到了xml中绑改,閱讀代碼的人必須要能理解這些才能真正的了解你的意圖,此外layout文件是具備復(fù)用能力的兄一,一旦你要復(fù)用layout厘线,那么這些xml中的邏輯便成了其無(wú)法復(fù)用的根源,因此我強(qiáng)烈禁止在xml中寫java邏輯出革。
在實(shí)際使用中我會(huì)發(fā)現(xiàn)我們經(jīng)常會(huì)根據(jù)字段來(lái)判斷是否要讓view顯示或隱藏造壮,如果都在java代碼中寫感覺會(huì)比較重一些。于是我嘗試在xml中寫了判斷是否顯示的邏輯骂束,后來(lái)發(fā)現(xiàn)即使layout被復(fù)用了耳璧,這種邏輯也是必然存在的,即使遇到不存在的情況轉(zhuǎn)為java代碼實(shí)現(xiàn)也是很簡(jiǎn)單的展箱。在定位問(wèn)題方面楞抡,如果知道xml中有這個(gè)邏輯的話也還好,所以我目前唯一能允許的就是在xml中寫控制view是否顯示的邏輯代碼析藕,其余的邏輯代碼一律禁止召廷。如果你也準(zhǔn)備這么寫,請(qǐng)務(wù)必讓你的團(tuán)隊(duì)接受并了解這種機(jī)制账胧,否則會(huì)給別人帶來(lái)困擾的竞慢。這里我仍舊是通過(guò)代碼模板的方式進(jìn)行快速編寫:
利用b代替findViewById
在mvvm時(shí)代,我們是否需要id呢治泥?其實(shí)筹煮,我們?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)一管理
一個(gè)頁(yè)面中會(huì)有多個(gè)view,Button和EditText肯定是會(huì)產(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
}
});
定位問(wèn)題的思路是這樣的狸膏,首先你肯定不會(huì)懷疑button不會(huì)產(chǎn)生事件(如果真的有沟饥,那么你的開發(fā)真的開小差了),一般都是按鈕被點(diǎn)擊后的事件觸發(fā)的代碼產(chǎn)生了問(wèn)題,所以大多數(shù)情況下只需要在觸發(fā)的那段java代碼中下斷點(diǎn)就行了贤旷。
利用vm做全局的數(shù)據(jù)同步
兩個(gè)頁(yè)面間
vm自身的自動(dòng)綁定特性會(huì)讓兩個(gè)頁(yè)面共用數(shù)據(jù)變得十分簡(jiǎn)單广料,可以通過(guò)viewModel.toSerializable()來(lái)將其序列化,然后在接收的地方通過(guò):
NewsviewModel vm = NewsviewModel.toviewModel(getIntent().getSerializableExtra(KEY));
得到它,現(xiàn)在你就可以方便的利用上個(gè)頁(yè)面?zhèn)鱽?lái)的vm進(jìn)行l(wèi)ayout層面的綁定了幼驶。
注意:
雖然這種方式十分簡(jiǎn)單艾杏,但不要濫用,它僅僅針對(duì)于兩頁(yè)面有有共同vm的情況盅藻,其他情況我還是推薦通過(guò)回調(diào)糜颠、廣播、事件總線等方式去做萧求。要記得vm雖好其兴,但它不是萬(wàn)能的。
多個(gè)頁(yè)面間
我們經(jīng)常會(huì)有一些全局的數(shù)據(jù)夸政,比如紅點(diǎn)消息和用戶信息元旬,這些數(shù)據(jù)我們通常會(huì)產(chǎn)生一個(gè)靜態(tài)的對(duì)象進(jìn)行存儲(chǔ),以用戶信息舉例守问,我們完全可以讓所有用到當(dāng)前用戶信息的頁(yè)面用同一個(gè)vm匀归,這樣就再也不用考慮多個(gè)頁(yè)面用戶信息不同步的情況了。至于什么東西可以用這種方式做全局同步耗帕,什么不可以穆端,這個(gè)就只能看業(yè)務(wù)和團(tuán)隊(duì)成員的把控能力了。
通過(guò)注冊(cè)數(shù)據(jù)監(jiān)聽來(lái)解耦view層
在mvvm中我們應(yīng)該把所有數(shù)據(jù)同步的事情交給框架仿便,而不是自己去維護(hù)体啰。將view層的邏輯(如:動(dòng)畫,控件A文字的改變引起的控件B改變等)獨(dú)立寫出嗽仪,在model中獨(dú)立寫出數(shù)據(jù)對(duì)vm產(chǎn)生影響的邏輯荒勇,下面舉個(gè)例子:
/**
* 數(shù)據(jù)改變后ui會(huì)做一些改變。
* 應(yīng)該利用對(duì)vm的字段監(jiān)聽的方式做處理闻坚,不應(yīng)該在數(shù)據(jù)改變時(shí)沽翔,通過(guò)開發(fā)者做ui層面的更新。
*
* @param bind 為什么不是單一監(jiān)聽器窿凤,而是觀察者模式仅偎?
* 因?yàn)闀?huì)有多個(gè)東西對(duì)同一個(gè)數(shù)據(jù)進(jìn)行監(jiān)聽,如果是單一的就沒辦法實(shí)現(xiàn)這個(gè)功能雳殊。
*/
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ù)來(lái)的時(shí)候,數(shù)據(jù)僅僅對(duì)vm進(jìn)行綁定相种,不用考慮ui層面的邏輯:
///////////////////////////////////////////////////////////////////////////
// 這里就僅僅做數(shù)據(jù)和ui的綁定工作了威恼,不用想ui層面的任何邏輯
///////////////////////////////////////////////////////////////////////////
/**
* 將ViewModel和model的數(shù)據(jù)進(jìn)行同步
* model模型可能很復(fù)雜,但viewModel的模型很簡(jiǎn)單寝并,這里就是做二者的轉(zhuǎn)換箫措。
*/
@Override
public void handleData(NewsInfo data, int pos) {
mviewModel.setTitle(String.format(data.title,"kale"));
}
禁止一切容易出錯(cuò)的操作
強(qiáng)類型語(yǔ)言和弱類型語(yǔ)言的一個(gè)差異(僅僅是差異)就是在于IDE可以幫你做很多限制,databinding本身是相當(dāng)靈活的衬潦,支持雙向綁定斤蔓,支持xml中寫邏輯等操作,但是我這里利用插件或者是其他的方式強(qiáng)烈禁止在xml中寫方法和特殊邏輯镀岛,對(duì)于import我只允許了View這一個(gè)類的import弦牡。對(duì)于雙向綁定,我建議你在編碼的時(shí)候就應(yīng)該有所警惕漂羊,最好能有注釋驾锰,方便你的同伴進(jìn)行定位問(wèn)題。
如果你是一人開發(fā)一個(gè)不需要維護(hù)的應(yīng)用走越,那么xml中隨便你怎么寫椭豫,但如果你是團(tuán)隊(duì)開發(fā),你會(huì)發(fā)現(xiàn)那些在xml中的邏輯很可能是團(tuán)隊(duì)合作的災(zāi)難旨指。當(dāng)然了赏酥,如果你已經(jīng)通過(guò)某種文檔或者是其他的標(biāo)準(zhǔn)化方式來(lái)限制和規(guī)定xml中的邏輯格式,那么我倒是覺得是可行的谆构。
自由是在限制之中的裸扶,如果沒有限制那么就沒有社會(huì)。
四搬素、總結(jié)
我經(jīng)歷了項(xiàng)目從mvc到mvp呵晨,然后變成mvvm,最后到mvpvm的各個(gè)階段熬尺,在每個(gè)階段中我也花了大量的時(shí)間去發(fā)現(xiàn)問(wèn)題解決問(wèn)題何荚,為后續(xù)的擴(kuò)展和靈活性做了很多的工作。在做這些事情的時(shí)候我漸漸發(fā)現(xiàn)猪杭,無(wú)論你采用什么模式餐塘,你都必須有明確的分層的概念,其實(shí)大到分層小到單一職責(zé)概念皂吮,都是在提升代碼可維護(hù)性戒傻。在現(xiàn)在這個(gè)時(shí)期,我的建議是中小型公司可以放心嘗試databinding蜂筹,大型公司的話因?yàn)轶w量和人員的問(wèn)題很難會(huì)改變模式需纳。當(dāng)然了,如果目前你的代碼本身就有很好的可維護(hù)性艺挪,我也不建議因?yàn)榧夹g(shù)的新穎而動(dòng)項(xiàng)目不翩,因?yàn)槲覀兊哪康牟皇菄L鮮和炫技,而是為了解決問(wèn)題!
話說(shuō)口蝠,你寫了多少年的findViewById器钟?