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

你們好,今天給你們帶來一個我本身開發改造的 MVVM 封裝框架。代碼不難,但我更想說一些我在開發這樣一個架構過程當中的想法和思路,咱們不只要善於做一個搬運工,更要本身多多造輪子,咱們程序員就是會折騰嘛。java

思惟導圖
思惟導圖

先送上源碼地址:WeaponAppgit

多提一句,這個 App 是我和朋友最近正在努力開發的一款 app,涵蓋絕大多數使用場景和技術(RxJava+Retrofit+MVVM+插件化+組件化+全平臺分享+服務端)。儘可能使用最優雅和最高級的方式來開發業務代碼。使用這套框架能夠快速構建 app,並可以進行高效的維護。程序員

但願你們能夠 star 一下,提一些建議,幫助咱們更好地完善它!github


在講具體的實現和思路以前,咱們須要多說一些東西,能夠說是封裝的動機吧,或者能夠解釋爲何要用面向接口的思想來封裝。架構

去年的時候,MVP在移動端比較火熱,一直持續到如今,MVVM做爲更爲高雅和清晰的開發架構,使用的人不是不少。不像MVP,我在研究的時候,想搜索一些封裝的資料,發現多數只能找到dataBinding的資料,但不多有教你怎麼封裝的。 「Google」爸爸的databinding爲咱們提供好了輪子,咱們實際上按照官方的使用方式來使用MVVM已是比較簡單了,只須要在 View 裏構建VM,在VM裏維持一個Model引用,進行相關數據的綁定便可。能夠說是很是好用了。app

那麼,爲何要特別地再封裝一下呢?框架

這就和咱們設計架構的目的和思路有關了。固然了,還有做爲程序員,確定仍是但願能寫出最優雅、最簡潔、最高級的代碼,咱們都是偏執狂ide

設計思路:測試驅動、面向接口、隱蔽實現

首先,咱們要明確一點,不管是MVP仍是MVVM,它們都不必定會讓你用更少的代碼來實現一個頁面,代碼量可能會更多。它們能作到的就是作到數據、邏輯、視圖關係的解耦,提高代碼的可維護性、可讀性、設計性和可測性組件化

MVVM 中,ViewModel 層是 View 和 Model 的中轉層,View 專門用來處理 UI 的操做,Model 是一些數據實體,ViewModel 操做一些和數據處理相關的綁定操做,由於 databinding 的雙向綁定特性,最好的封裝應該是讓 View 層只有綁定 ViewModel 和一些必要的 UI 操做,總體的邏輯和思路乾淨整齊,ViewModel 是一個個功能單一方法的集合。單元測試

「單一原則」是咱們寫代碼的時候必定要養成的好習慣,它不只能幫助咱們寫出更優雅的代碼,也是代碼具備可測性、邏輯性和可維護性的要求。

MVVM 單元測試很方便,由於有了雙向綁定。只須要測一下 ViewModel 的方法,方法經過了便可驗證數據和 UI 邏輯。咱們寫代碼的時候,就應該保持好設計性,儘可能作到讓代碼的可測性很強,保持單一原則,隔離好 View 和 Model 的邏輯,讓代碼經過驗證方法而不須要真正構造 Activity 實例就能有足夠的可測性。爲了讓代碼保持可測行,要求咱們代碼須要具備設計性,而代碼的設計性和單一原則又是單元測試的一個自己要求,二者相互影響,相互驅動。

這就是測試驅動開發。

好了,如今咱們代碼寫的也設計性了,方法也夠單一了,但單元測試的時候,ViewModel 做爲 View 和 Model 的橋樑,它實際上應該持有 View 和 Model 的引用的,但是單元測試構造 Activity 對象不方便,咱們既然是要使用單元測試,就應該儘可能避免須要打開頁面這樣的操做,雖然咱們有一些很是強大的第三方單元測試框架可以構造 Activity 和 Fragment 甚至能夠驗證一些 UI 的操做,但總而言之仍是一個比較麻煩而妥協的作法,因此我根據AndroidFire這個項目上的 MVP 封裝思路,進行了 MVVM 的改造,實現了編譯期的多態,經過反射構造類型參數的具體對象,在 Contact 中定義各個層級的接口,ViewModel 進行跨層調用的時候,只關注具體接口的形式,而不關心接口的具體實現和究竟是哪一個實例實現了他。

這就是面向接口了。

同時,咱們隱藏了 databinding 的綁定操做,集成了一些ListViewRecyclerViewViewPager的 databinding 第三方使用庫,再經過自定義一些@BindAdapter幫助更好的進行 MVVM 開發。即便開發者以前不瞭解 databinding,按照咱們封裝的操做流程,開發界面就像堆磚塊同樣簡單高效。

面向接口的框架在做單元測試的時候,咱們只須要本身構建出一個空實現的接口實例,便可跳過一些 View 層的 UI 操做或者 Model 層的請求操做,作到真正意義上的單元測試。

說的很抽象,下一節咱們來看一下具體代碼。

MVVM 封裝核心實現

咱們先來看下封裝的一些基類設計思路。由於「WeaponApp」的頁面全是用 Fragment 進行開發的,只須要一個佔坑 Activity 做爲容器來展現 Fragment,因此咱們只針對 Fragment 進行了基類封裝:

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

emm...這是什麼。。看着這麼多泛型疊加,是否是有點頭暈,別急,咱們從後往前慢慢看。

BaseView 是一個接口,裏面定義了一些必需要實現的方法,好比databinding 須要的BR文件,init初始化方法等,最重要的是定義了一個基類類型,表示項目中全部的 Fragment 都是這個接口類型,輔助編譯期檢查。

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

VM extends BaseViewModel<? extends BaseViewModel<? extends BaseView,? extends BaseModel>>: VM 的泛型是比較複雜的,Android 中的列表控件都是須要一個 Adapter ,爲了管理這些列表 item 的 VM,而且作到統一處理,因此 BaseViewModel 中的兩個泛型類型都是沒有 extends 來限制範圍的,那麼爲了區分是頁面 VM 仍是 item 的 VM。在 BaseFragment 中,經過通配符來限定範圍,在編譯期提醒開發者。

由於使用了binding-collection-adapter,因此在使用像 ListView,RecyclerView 和 ViewPager 這類控件的時候,是不須要經過 adapter 來進行管理的,所有都是經過 item 的 VM,經過 MVVM 的形式來配置。

好了,看好了類的定義代碼,咱們來下最關鍵的onCreateView()方法:

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

繼續跟進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 的綁定操做,就很少細說了,咱們來看下中間的部分。

mViewModel = TUtil.getT(this,0);
M model = TUtil.getT(this,1);複製代碼

這裏的 mViewModel 的類型其實是 VM,TUtil.getT(this,0)方法的第二個參數傳入的是類上定義的泛型位置,好比 VM 在 BaseFragment 中的位置是第一個,那麼就傳入 0,M 是第二個,那麼就傳入 1 。該方法將返回具體泛型參數類型的實例。這樣作的好處就是咱們不須要手動操做構建對象並將引用保存到成員變量上了,只須要定義好具體類型參數的泛型類型,便可經過getViewModel獲取 ViewModel 的具體實例。

繼續看代碼。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方法的參數都是泛型參數,因此這裏必須經過反射來獲取具體的方法實例,再經過invoke進行調用方法。

舉個栗子??

OK,那麼咱們來看看到底怎麼就「傻瓜式」開發了,怎麼就單元測試很好使了。好比如今項目中的個人界面,用這個封裝框架來寫界面的時候,先寫一個接口定義類 Contact :

interface MineContact{
    interface View extends BaseView{
        void testType();
    }

    abstract class ViewModel extends BaseViewModel<View,MineModel>{
        abstract void onHttpResponse();//數據請求成功回調
        abstract void onHttpError();//數據請求失敗回調
    }

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

}複製代碼

這裏定義了 MVVM 三層的類型和接口。當你須要添加接口的時候,只須要在這裏添加便可。下面是MineFragmentMineViewModelMineModel的類定義:

//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();
    }
}複製代碼

咱們能夠看到咱們寫具體類中,全部類的集成格式是同樣的,而且咱們內部能夠經過咱們剛剛在 Contact 中定義的接口進行各個層級之間的通訊,在編譯期,咱們並不用關心各個接口具體的實現是什麼,具體的實現將被移步到運行期中,這極大的方便了咱們的單元測試,這也是多態和裏式替換原則的應用。同時咱們發現 MVVM 的不少操做在 ViewModel 層都被隱藏了,若是你想使用 BR 文件,就本身定義相對應的 get 方法,並不須要具體的保存一個 model 的成員變量了。下面咱們來看看具體的單元測試該怎麼寫:

好比咱們如今要測試 VM 中的 init 方法,其中的 View 接口 testType() 是一個吐司顯示,爲了經過這個方法,咱們若是構建一個 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);
    //調用 init() 方法
    vm.init();
    }
}複製代碼

咱們成功的在單元測試中調用了 VM 的 init 方法,也沒有構造真正的 MineFragment,只是本身定義了一個和 MineFragment 同類型的接口,由於面向接口的緣由,VM 仍然能對其進行調用操做,咱們依然不須要關心 testType() 方法內部究竟是不是和 MineFragment 定義的 testType() 方法是否是同樣的,由於這裏都是 UI 操做,咱們不須要在 MVVM 的單元測試中測試它。

MVVM 的強大固然不止於此,還須要讀者本身多多發掘。固然,在學習別人的輪子的時候,必定要多多思考,觸類旁通,不能一味的搬運。


個人公衆號:WeaponZhi

相關文章
相關標籤/搜索