小之的架構(gòu)之路——Android MVVM 面向接口型框架封裝和單元測試

大家好瑞侮,今天給大家?guī)硪粋€我自己開發(fā)改造的 MVVM 封裝框架丐枉。代碼不難,但我更想說一些我在開發(fā)這樣一個架構(gòu)過程中的想法和思路苞轿,我們不僅要善于作一個搬運工,更要自己多多造輪子逗物,我們程序員就是會折騰嘛搬卒。

思維導(dǎo)圖

先送上源碼地址:WeaponApp

多提一句,這個 App 是我和朋友最近正在努力開發(fā)的一款 app翎卓,涵蓋絕大多數(shù)使用場景和技術(shù)(RxJava+Retrofit+MVVM+插件化+組件化+全平臺分享+服務(wù)端)契邀。盡量使用最優(yōu)雅和最高級的方式來開發(fā)業(yè)務(wù)代碼。使用這套框架可以快速構(gòu)建 app失暴,并能夠進(jìn)行高效的維護(hù)坯门。

希望大家可以 star 一下,提一些建議逗扒,幫助我們更好地完善它古戴!


在講具體的實現(xiàn)和思路之前,我們需要多說一些東西矩肩,可以說是封裝的動機(jī)吧现恼,或者可以解釋為什么要用面向接口的思想來封裝。

去年的時候黍檩,MVP在移動端比較火熱叉袍,一直持續(xù)到現(xiàn)在,MVVM作為更為高雅和清晰的開發(fā)架構(gòu)刽酱,使用的人不是很多喳逛。不像MVP,我在研究的時候棵里,想搜索一些封裝的資料润文,發(fā)現(xiàn)多數(shù)只能找到dataBinding的資料姐呐,但很少有教你怎么封裝的。 「Google」爸爸的databinding為我們提供好了輪子转唉,我們實際上按照官方的使用方式來使用MVVM已經(jīng)是比較簡單了皮钠,只需要在 View 里構(gòu)建VM稳捆,在VM里維持一個Model引用赠法,進(jìn)行相關(guān)數(shù)據(jù)的綁定即可∏呛唬可以說是非常好用了砖织。

那么,為什么要特別地再封裝一下呢末荐?

這就和我們設(shè)計架構(gòu)的目的和思路有關(guān)了侧纯。當(dāng)然了,還有作為程序員甲脏,肯定還是希望能寫出最優(yōu)雅眶熬、最簡潔、最高級的代碼块请,我們都是偏執(zhí)狂娜氏。

設(shè)計思路:測試驅(qū)動、面向接口墩新、隱蔽實現(xiàn)

首先贸弥,我們要明確一點,不論是MVP還是MVVM海渊,它們都不一定會讓你用更少的代碼來實現(xiàn)一個頁面绵疲,代碼量可能會更多。它們能做到的就是做到數(shù)據(jù)臣疑、邏輯盔憨、視圖關(guān)系的解耦,提升代碼的可維護(hù)性讯沈、可讀性般渡、設(shè)計性和可測性

MVVM 中芙盘,ViewModel 層是 View 和 Model 的中轉(zhuǎn)層驯用,View 專門用來處理 UI 的操作,Model 是一些數(shù)據(jù)實體儒老,ViewModel 操作一些和數(shù)據(jù)處理相關(guān)的綁定操作蝴乔,因為 databinding 的雙向綁定特性,最好的封裝應(yīng)該是讓 View 層只有綁定 ViewModel 和一些必要的 UI 操作驮樊,整體的邏輯和思路干凈整齊薇正,ViewModel 是一個個功能單一方法的集合片酝。

「單一原則」是我們寫代碼的時候一定要養(yǎng)成的好習(xí)慣,它不僅能幫助我們寫出更優(yōu)雅的代碼挖腰,也是代碼具有可測性雕沿、邏輯性和可維護(hù)性的要求。

MVVM 單元測試很方便猴仑,因為有了雙向綁定审轮。只需要測一下 ViewModel 的方法,方法通過了即可驗證數(shù)據(jù)和 UI 邏輯辽俗。我們寫代碼的時候疾渣,就應(yīng)該保持好設(shè)計性,盡量做到讓代碼的可測性很強(qiáng)崖飘,保持單一原則榴捡,隔離好 View 和 Model 的邏輯,讓代碼通過驗證方法而不需要真正構(gòu)造 Activity 實例就能有足夠的可測性朱浴。為了讓代碼保持可測行吊圾,要求我們代碼需要具有設(shè)計性,而代碼的設(shè)計性和單一原則又是單元測試的一個本身要求翰蠢,兩者相互影響项乒,相互驅(qū)動。

這就是測試驅(qū)動開發(fā)躏筏。

好了板丽,現(xiàn)在我們代碼寫的也設(shè)計性了,方法也夠單一了趁尼,但單元測試的時候埃碱,ViewModel 作為 View 和 Model 的橋梁,它實際上應(yīng)該持有 View 和 Model 的引用的酥泞,可是單元測試構(gòu)造 Activity 對象不方便砚殿,我們既然是要使用單元測試,就應(yīng)該盡量避免需要打開頁面這樣的操作芝囤,雖然我們有一些非常強(qiáng)大的第三方單元測試框架能夠構(gòu)造 Activity 和 Fragment 甚至可以驗證一些 UI 的操作似炎,但總而言之還是一個比較麻煩而妥協(xié)的做法,所以我根據(jù)AndroidFire這個項目上的 MVP 封裝思路悯姊,進(jìn)行了 MVVM 的改造羡藐,實現(xiàn)了編譯期的多態(tài),通過反射構(gòu)造類型參數(shù)的具體對象悯许,在 Contact 中定義各個層級的接口仆嗦,ViewModel 進(jìn)行跨層調(diào)用的時候,只關(guān)注具體接口的形式先壕,而不關(guān)心接口的具體實現(xiàn)和到底是哪個實例實現(xiàn)了他瘩扼。

這就是面向接口了谆甜。

同時,我們隱藏了 databinding 的綁定操作集绰,集成了一些ListView规辱,RecyclerViewViewPager的 databinding 第三方使用庫栽燕,再通過自定義一些@BindAdapter幫助更好的進(jìn)行 MVVM 開發(fā)罕袋。即使開發(fā)者之前不了解 databinding,按照我們封裝的操作流程纫谅,開發(fā)界面就像堆磚塊一樣簡單高效炫贤。

面向接口的框架在作單元測試的時候溅固,我們只需要自己構(gòu)建出一個空實現(xiàn)的接口實例付秕,即可跳過一些 View 層的 UI 操作或者 Model 層的請求操作,做到真正意義上的單元測試侍郭。

說的很抽象询吴,下一節(jié)我們來看一下具體代碼。

MVVM 封裝核心實現(xiàn)

我們先來看下封裝的一些基類設(shè)計思路亮元。因為「WeaponApp」的頁面全是用 Fragment 進(jìn)行開發(fā)的猛计,只需要一個占坑 Activity 作為容器來展示 Fragment,所以我們只針對 Fragment 進(jìn)行了基類封裝:

public abstract class BaseFragment<VM extends BaseViewModel<? extends BaseView, ? extends BaseModel>,
        M extends BaseModel>
        extends Fragment
        implements BaseView {}

emm...這是什么爆捞。奉瘤。看著這么多泛型疊加煮甥,是不是有點頭暈盗温,別急,我們從后往前慢慢看成肘。

BaseView 是一個接口卖局,里面定義了一些必須要實現(xiàn)的方法,比如databinding 需要的BR文件双霍,init初始化方法等砚偶,最重要的是定義了一個基類類型孕索,表示項目中所有的 Fragment 都是這個接口類型飘千,輔助編譯期檢查。

M extends BaseModel:定義具體的 Model 類型倚评。

VM extends BaseViewModel<? extends BaseViewModel<? extends BaseView,? extends BaseModel>>: VM 的泛型是比較復(fù)雜的丘逸,Android 中的列表控件都是需要一個 Adapter 单鹿,為了管理這些列表 item 的 VM,并且做到統(tǒng)一處理鸣个,所以 BaseViewModel 中的兩個泛型類型都是沒有 extends 來限制范圍的羞反,那么為了區(qū)分是頁面 VM 還是 item 的 VM布朦。在 BaseFragment 中,通過通配符來限定范圍昼窗,在編譯期提醒開發(fā)者是趴。

因為使用了binding-collection-adapter,所以在使用像 ListView澄惊,RecyclerView 和 ViewPager 這類控件的時候唆途,是不需要通過 adapter 來進(jìn)行管理的,全部都是通過 item 的 VM掸驱,通過 MVVM 的形式來配置肛搬。

好了,看好了類的定義代碼毕贼,我們來下最關(guān)鍵的onCreateView()方法:

 @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);
        return initFragment(inflater, container);
    }

繼續(xù)跟進(jìn)initFragment方法:

private View initFragment(LayoutInflater inflater, ViewGroup container) {
    if (mViewDataBinding == null) {
        mContext = getActivity();
        mViewDataBinding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false);

       //反射生成泛型類對象
        mViewModel = TUtil.getT(this, 0);
        M model = TUtil.getT(this, 1);

       //VM 和 View 綁定
       if (mViewModel != null) {
           mViewModel.setContext(mContext);
           try {
               Method setModel = mViewModel.getClass().getMethod("setModel",Object.class);
               Method attachView = mViewModel.getClass().getMethod("attachView", Object.class);
               setModel.invoke(mViewModel, model);
               attachView.invoke(mViewModel, this);
           } catch (Exception e) {
               e.printStackTrace();
           }
      }

       //Model 和 VM 綁定
       if (model != null) {
           model.attachViewModel(mViewModel);
       }

       //DataBinding 綁定
       mViewDataBinding.setVariable(getBR(), mViewModel);

       initView();
 }

這里有一些 databinding 的綁定操作温赔,就不多細(xì)說了,我們來看下中間的部分鬼癣。

mViewModel = TUtil.getT(this,0);
M model = TUtil.getT(this,1);

這里的 mViewModel 的類型實際上是 VM陶贼,TUtil.getT(this,0)方法的第二個參數(shù)傳入的是類上定義的泛型位置,比如 VM 在 BaseFragment 中的位置是第一個待秃,那么就傳入 0拜秧,M 是第二個,那么就傳入 1 章郁。該方法將返回具體泛型參數(shù)類型的實例枉氮。這樣做的好處就是我們不需要手動操作構(gòu)建對象并將引用保存到成員變量上了,只需要定義好具體類型參數(shù)的泛型類型暖庄,即可通過getViewModel獲取 ViewModel 的具體實例聊替。

繼續(xù)看代碼。model.attachViewModel將 ViewModel 綁定到 Model雄驹,ViewModel 和 View 的綁定以及將 Model 綁定到 ViewModel 是中間一段代碼做到的:

Method setModel = mViewModel.getClass().getMethod("setModel",Object.class);
Method attachView = mViewModel.getClass().getMethod("attachView", Object.class);
setModel.invoke(mViewModel, model);
attachView.invoke(mViewModel, this);

通配符實際上是一種具體但未知類型的類型佃牛。ViewModel 的attachViewsetModel方法的參數(shù)都是泛型參數(shù),所以這里必須通過反射來獲取具體的方法實例医舆,再通過invoke進(jìn)行調(diào)用方法俘侠。

舉個栗子?蔬将?

OK爷速,那么我們來看看到底怎么就「傻瓜式」開發(fā)了,怎么就單元測試很好使了霞怀。比如現(xiàn)在項目中的我的界面惫东,用這個封裝框架來寫界面的時候,先寫一個接口定義類 Contact :

interface MineContact{
    interface View extends BaseView{
        void testType();
    }
    
    abstract class ViewModel extends BaseViewModel<View,MineModel>{
        abstract void onHttpResponse();//數(shù)據(jù)請求成功回調(diào)
        abstract void onHttpError();//數(shù)據(jù)請求失敗回調(diào)
    }

    abstract class Model extends BaseModel<ViewModel>{
        abstract void loadData();//請求數(shù)據(jù)
    }

}

這里定義了 MVVM 三層的類型和接口。當(dāng)你需要添加接口的時候廉沮,只需要在這里添加即可颓遏。下面是MineFragmentMineViewModel滞时、MineModel的類定義:

//View
public class MineFragment extends BaseFragment<MineViewModel,MineModel> implements MineContact.View{

    private ShareView mShareView;
    @Override
    public int getLayoutId() {
        return R.layout.fragment_mine;
    }

    @Override
    public void initView() {
     
    }

    @Override
    public int getBR() {
        return com.weapon.joker.app.mine.BR.model;
    }

    @Override
    public void testType(){
        
    }
}

//ViewModel
public class MineViewModel extends MineContact.ViewModel{

    public void init(){
        setTestString("反射封裝測試成功");
        getView().testType();
        getModel.loadData();
    }

    @Bindable
    public String getTestString(){
        return getModel().testString;
    }

    public void setTestString(String testString){
        getModel().testString = testString;
        notifyPropertyChanged(BR.testString);
    }

    public void onHttpResponse(){}
    public void onHttpError(){}
}

//Model
public class MineModel extends MineContact.Model{
    @Bindable
    public String testString;

    public void loadData(){
        getViewModel().onHttpResponse();
        getViewModel().onHttpError();
    }
}

我們可以看到我們寫具體類中叁幢,所有類的集成格式是一樣的,并且我們內(nèi)部可以通過我們剛剛在 Contact 中定義的接口進(jìn)行各個層級之間的通信坪稽,在編譯期曼玩,我們并不用關(guān)心各個接口具體的實現(xiàn)是什么,具體的實現(xiàn)將被移步到運行期中窒百,這極大的方便了我們的單元測試黍判,這也是多態(tài)和里式替換原則的應(yīng)用。同時我們發(fā)現(xiàn) MVVM 的很多操作在 ViewModel 層都被隱藏了篙梢,如果你想使用 BR 文件顷帖,就自己定義相對應(yīng)的 get 方法,并不需要具體的保存一個 model 的成員變量了庭猩。下面我們來看看具體的單元測試該怎么寫:

比如我們現(xiàn)在要測試 VM 中的 init 方法窟她,其中的 View 接口 testType() 是一個吐司顯示陈症,為了通過這個方法蔼水,我們?nèi)绻麡?gòu)建一個 MineFragment 實例,無疑非常麻煩录肯,但在我們這套封裝中趴腋,我們只需要這樣寫即可:

public class Test{
    @Test
    public void main(){
        MineContact.View view = new MineContact.View(){
             @Override
             public void testType() {}
             
             @Override
             public int getLayoutId() {
             return 0;
             }
             
             @Override
             public void initView() {}
             
             @Override
             public int getBR() {
             return 0;
             }  
        };
        
    MineContact.Model model = new MineContact.Model(){
        @Override
        void loadData() {}
    };
    
    MineViewModel vm = new MineViewModel();
    vm.attachView(view);
    vm.setModel(model);
    //調(diào)用 init() 方法
    vm.init();
    }
}

我們成功的在單元測試中調(diào)用了 VM 的 init 方法,也沒有構(gòu)造真正的 MineFragment论咏,只是自己定義了一個和 MineFragment 同類型的接口优炬,因為面向接口的原因,VM 仍然能對其進(jìn)行調(diào)用操作厅贪,我們依然不需要關(guān)心 testType() 方法內(nèi)部到底是不是和 MineFragment 定義的 testType() 方法是不是一樣的蠢护,因為這里都是 UI 操作,我們不需要在 MVVM 的單元測試中測試它养涮。

MVVM 的強(qiáng)大當(dāng)然不止于此葵硕,還需要讀者自己多多發(fā)掘。當(dāng)然贯吓,在學(xué)習(xí)別人的輪子的時候懈凹,一定要多多思考,舉一反三悄谐,不能一味的搬運介评。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市爬舰,隨后出現(xiàn)的幾起案子们陆,更是在濱河造成了極大的恐慌寒瓦,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件坪仇,死亡現(xiàn)場離奇詭異孵构,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)烟很,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進(jìn)店門颈墅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人雾袱,你說我怎么就攤上這事恤筛。” “怎么了芹橡?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵毒坛,是天一觀的道長。 經(jīng)常有香客問我林说,道長煎殷,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任腿箩,我火速辦了婚禮豪直,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘珠移。我一直安慰自己弓乙,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布钧惧。 她就那樣靜靜地躺著暇韧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪浓瞪。 梳的紋絲不亂的頭發(fā)上懈玻,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天,我揣著相機(jī)與錄音乾颁,去河邊找鬼涂乌。 笑死,一個胖子當(dāng)著我的面吹牛钮孵,可吹牛的內(nèi)容都是我干的骂倘。 我是一名探鬼主播,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼巴席,長吁一口氣:“原來是場噩夢啊……” “哼历涝!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤荧库,失蹤者是張志新(化名)和其女友劉穎堰塌,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體分衫,經(jīng)...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡场刑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蚪战。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片牵现。...
    茶點故事閱讀 39,953評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖邀桑,靈堂內(nèi)的尸體忽然破棺而出瞎疼,到底是詐尸還是另有隱情,我是刑警寧澤壁畸,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布贼急,位于F島的核電站,受9級特大地震影響捏萍,放射性物質(zhì)發(fā)生泄漏太抓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一令杈、第九天 我趴在偏房一處隱蔽的房頂上張望走敌。 院中可真熱鬧,春花似錦这揣、人聲如沸悔常。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至矫户,卻和暖如春片迅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背皆辽。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工柑蛇, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人驱闷。 一個月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓耻台,卻偏偏與公主長得像,于是被迫代替她去往敵國和親空另。 傳聞我的和親對象是個殘疾皇子盆耽,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,901評論 2 355

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

  • 1、概述 Databinding 是一種框架,MVVM是一種模式摄杂,兩者的概念是不一樣的坝咐。我的理解DataBindi...
    Kelin閱讀 76,797評論 68 521
  • 概述 說到MVVM,大家都會想起前端的MVVM框架析恢,相較于前端MVVM的火熱墨坚,它在移動開發(fā)領(lǐng)域就不那么熱門了。Go...
    ditclear閱讀 20,265評論 13 67
  • 路過的風(fēng)景映挂, 看到的人事泽篮。 小小的本子, 記錄著每天柑船。 日常的繁瑣咪辱, 消耗著身心。 伸開手揚起椎组, 握不住殘沙油狂。 夜...
    淚盡成陌閱讀 193評論 0 0
  • 2016年學(xué)習(xí)的 2016年學(xué)習(xí)提升編程技能。 2016年更新自己的觀念寸癌。 2016年明白了要學(xué)會愛自己专筷。 201...
    劉摯珂閱讀 227評論 0 0