1. 程式人生 > >關於Android MvvM的一些體會

關於Android MvvM的一些體會

關於Android MvvM的一些體會

前言

由於我司專案較老有很多歷史包袱程式碼結構也比較混亂,需求複雜的頁面動輒activity中1000多行,看著很是頭疼,於是趁著加班提前做完需求餘下的時間學習了mvvm對專案部分功能進行了改造,目前已經使用3個版本了,本篇博文分享下我使用的感受。

準備

這裡先說說關於mvvm的幾個問題(如有不對請輕噴 (╹▽╹))

  1. 首先說說我為啥選擇mvvm而不是熟知的mvp。

    主要原因是我覺得mvp介面寫起來有點麻煩,針對ui和model都得寫介面,然後這個粒度不好控制如果太細了就得寫一堆介面,太粗了又沒有複用性,並且presenter持有了ui引用在更新ui的時候還得考慮生命週期,還有activity引用的處理防止記憶體洩露這些問題我都覺得挺麻煩的而MvvM中databinding框架處理好了這些問題,所以我選擇了更加方便的mvvm,當然mvvm也不是沒有缺點下面會說到。

  2. mvvm優缺點

    • 優點:
      1. 資料來源被強化,利用databinding框架實現雙向繫結技術,當資料變化的時候ui自動更新,ui上使用者操作資料自動更新,很好的做到資料的一致性。
      2. xml和activity處理ui操作、model提供資料、vm處理業務邏輯,各個層級分工明確,activity中程式碼大大減少專案整體結構更加清晰。
      3. 很方便做ui的a/b測試可以共用同一個vm。
      4. 方便單元測試ui和vm邏輯完全分離。
    • 缺點:
      1. bug很難被除錯,資料繫結使得一個bug被傳遞到別的位置,要找到bug的原始位置不太容易。
      2. 由於要遵守模式的規範呼叫流程變得複雜。
      3. vm中會有很多被觀察者變數如果業務邏輯非常複雜會消耗更多記憶體。
  3. mvvm一定要用databinding麼?

    答案是 否。首先我們要了解到mvvm是資料驅動的架構,所以著眼點是資料的變化,那麼我們需要實現一套ui和資料雙向繫結的邏輯,當資料修改的時候通知ui改變,ui輸入或者點選的時候觸發資料修改,而databinding就是幫你實現這個雙向繫結過程的框架,在xml中按它的語法去寫佈局,然後他會根據你在xml中所寫的生成對應的類幫你實現這個繫結過程,當然你也可以自己手動實現這個繫結過程,所以databinding是非必須的。

專案結構圖

上面是mvvm基本的結構圖,act/fra和xml是v處理ui操作、viewmodel是vm處理業務邏輯、repository是m提供資料,他們之間是一種單項的持有關係activity/fragment持有vm,vm持有model。

對於Repository不太理解的可以看看這篇文章Repository模式

實際使用

專案中我使用的是retrofit+rxjava+livedata+viewmodel+databinding+kotlin實現的mvvm

retrofit+rejava用來在model層從網路獲取資料通知到vm

livedata是vm通知ui時使用可以感知生命週期防止記憶體洩漏npe問題(主要用在事件傳遞上)

viewmodel是vm可以在act/frg因配置修改銷燬的情況下複用

databinding實現ui和vm的雙向繫結

這裡來個具體例子,activity可見通知vm獲取資料,vm從model拿到資料然後更新被觀察者,ui自動重新整理的流程。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="vm"
            type="rocketly.mvvmdemo.viewmodel.HotCityListVM" />
    </data>

    <android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/srl"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:onRefreshListener="@{()->vm.onRefresh()}"//重新整理自動觸發vm.onRefresh()方法
        tools:context="rocketly.mvvmdemo.ui.MainActivity">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/rv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:multiTypeItem="@{vm.cityList}"//這裡rv與vm中cityList繫結 />

    </android.support.v4.widget.SwipeRefreshLayout>
</layout>

xml中Recyclerview和vm的cityList繫結

class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main).apply {
            vm = ViewModelProviders.of(this@MainActivity).get(HotCityListVM::class.java)
        }
    }

    override fun onResume() {
        super.onResume()
        binding.vm?.onFirstLoad()//onResume呼叫vm.onFirstLoad()載入資料
    }

}

activity在onResume通知vm載入資料

class HotCityListVM : BaseVM() {
    val cityList = ObservableArrayList<Basic>()
    val hotCityItemEvent = SingleLiveEvent<String>()

    override fun onFirstLoad() {
        super.onFirstLoad()
        load()
    }

    override fun onRefresh() {
        super.onRefresh()
        load()
    }

    private fun load() {
        CityRepository.getHotCityList(num = 50)
                .subscribe(ApiObserver(success = {
                    resetLoadStatus()
                    cityList.clear()
                    cityList.addAll(it.HeWeather6[0].basic)
                }, error = {
                    resetLoadStatus()
                }))
    }

    fun hotCityItemClick(s: String) {
        hotCityItemEvent.value = s
    }
}

vm從model CityRepository獲取資料修改被觀察者物件cityList,然後ui監聽到資料修改執行recyclerview重新整理,這一套流程就走完了,具體例子在MvvmDeno

除了正常的請求資料顯示邏輯,這裡再演示下點選事件的流程,彈dialog或者其他需要context的事件也是同樣方式。

recyclerview中item點選事件傳遞到vm然後vm通知activty執行對應的邏輯。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="vm"
            type="rocketly.mvvmdemo.viewmodel.HotCityListVM" />

        <variable
            name="data"
            type="rocketly.mvvmdemo.model.Basic" />
    </data>

    <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:onClick="@{()->vm.hotCityItemClick(data.location)}"//呼叫vm的方法通知點選了>

        <TextView
            android:id="@+id/tv_city_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{data.location}"
            android:textColor="@android:color/black"
            android:textSize="18sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintHorizontal_chainStyle="packed"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toLeftOf="@+id/tv_lon"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="上海" />

        <TextView
            android:id="@+id/tv_lon"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="5dp"
            android:text="@{data.lon}"
            android:textColor="@android:color/black"
            android:textSize="14sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toRightOf="@+id/tv_city_name"
            app:layout_constraintRight_toLeftOf="@+id/tv_lat"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="(經度:555" />

        <TextView
            android:id="@+id/tv_lat"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="2dp"
            android:text="@{data.lat}"
            android:textColor="@android:color/black"
            android:textSize="14sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toRightOf="@+id/tv_lon"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="緯度:555)" />

    </android.support.constraint.ConstraintLayout>
</layout>
class HotCityListVM : BaseVM() {
    val hotCityItemEvent = SingleLiveEvent<String>()//給activty監聽的被觀察者livedata物件
    fun hotCityItemClick(s: String) {//點選方法
        hotCityItemEvent.value = s
    }
}
class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main).apply {
            vm = ViewModelProviders.of(this@MainActivity).get(HotCityListVM::class.java)
        }
        initListener()
    }

    private fun initListener() {
        binding.vm?.apply {
            hotCityItemEvent.observe(this@MainActivity) {//監聽item點選事件
                it ?: return@observe
                Toast.makeText(this@MainActivity, "點選了:$it", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

以前我們都是在item中直接執行點選事件的,但為了遵守mvvm的規範,邏輯都在vm處理又因為vm不能持有context所以需要context的事件在通過livedata傳遞到activity執行。

那麼一般的資料請求和點選事件的流程就講完了,接下來說說databinding原理。

DataBinding原始碼淺析

通過前面的例子可以發現對於databinding的使用一般可以分為如下幾步

  1. 在xml中按databinding的語法書寫佈局
  2. activity中使用DataBindingUtil.setContentView()繫結View並獲取binding物件
  3. 把xml中宣告的變數通過第二步得到的binding物件設定進去

第一步按databinding語法書寫xml,然後聲明瞭一個變數vm,並將rv與vm的cityList繫結,重新整理監聽與vm的onRefresh方法繫結

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="vm"
            type="rocketly.mvvmdemo.viewmodel.HotCityListVM" />
    </data>

    <android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/srl"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:onRefreshListener="@{()->vm.onRefresh()}"
        tools:context="rocketly.mvvmdemo.ui.MainActivity">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/rv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:multiTypeItem="@{vm.cityList}" />

    </android.support.v4.widget.SwipeRefreshLayout>
</layout>

然後make project會生成一個佈局名稱+Binding的類,生成的路徑如下

這個類就是按我們xml中所寫生成的,這裡把完整的生成類貼出來可以大概的看下。

public class ActivityMainBinding extends android.databinding.ViewDataBinding implements android.databinding.generated.callback.OnRefreshListener.Listener {

    @Nullable
    private static final android.databinding.ViewDataBinding.IncludedLayouts sIncludes;
    @Nullable
    private static final android.util.SparseIntArray sViewsWithIds;
    static {
        sIncludes = null;
        sViewsWithIds = null;
    }
    // views
    @NonNull
    public final android.support.v7.widget.RecyclerView rv;
    @NonNull
    public final android.support.v4.widget.SwipeRefreshLayout srl;
    // variables
    @Nullable
    private rocketly.mvvmdemo.viewmodel.HotCityListVM mVm;
    @Nullable
    private final android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener mCallback2;
    // values
    // listeners
    // Inverse Binding Event Handlers

    public ActivityMainBinding(@NonNull android.databinding.DataBindingComponent bindingComponent, @NonNull View root) {
        super(bindingComponent, root, 1);
        final Object[] bindings = mapBindings(bindingComponent, root, 2, sIncludes, sViewsWithIds);
        this.rv = (android.support.v7.widget.RecyclerView) bindings[1];
        this.rv.setTag(null);
        this.srl = (android.support.v4.widget.SwipeRefreshLayout) bindings[0];
        this.srl.setTag(null);
        setRootTag(root);
        // listeners
        mCallback2 = new android.databinding.generated.callback.OnRefreshListener(this, 1);
        invalidateAll();
    }

    @Override
    public void invalidateAll() {
        synchronized(this) {
                mDirtyFlags = 0x4L;
        }
        requestRebind();
    }

    @Override
    public boolean hasPendingBindings() {
        synchronized(this) {
            if (mDirtyFlags != 0) {
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean setVariable(int variableId, @Nullable Object variable)  {
        boolean variableSet = true;
        if (BR.vm == variableId) {
            setVm((rocketly.mvvmdemo.viewmodel.HotCityListVM) variable);
        }
        else {
            variableSet = false;
        }
            return variableSet;
    }

    public void setVm(@Nullable rocketly.mvvmdemo.viewmodel.HotCityListVM Vm) {//根據我們xml中宣告的vm變數生成的set方法
        this.mVm = Vm;
        synchronized(this) {
            mDirtyFlags |= 0x2L;
        }
        notifyPropertyChanged(BR.vm);
        super.requestRebind();
    }
    @Nullable
    public rocketly.mvvmdemo.viewmodel.HotCityListVM getVm() {
        return mVm;
    }

    @Override
    protected boolean onFieldChange(int localFieldId, Object object, int fieldId) {
        switch (localFieldId) {
            case 0 :
                return onChangeVmCityList((android.databinding.ObservableArrayList<rocketly.mvvmdemo.model.Basic>) object, fieldId);
        }
        return false;
    }
    private boolean onChangeVmCityList(android.databinding.ObservableArrayList<rocketly.mvvmdemo.model.Basic> VmCityList, int fieldId) {
        if (fieldId == BR._all) {
            synchronized(this) {
                    mDirtyFlags |= 0x1L;
            }
            return true;
        }
        return false;
    }

    @Override
    protected void executeBindings() {
        long dirtyFlags = 0;
        synchronized(this) {
            dirtyFlags = mDirtyFlags;
            mDirtyFlags = 0;
        }
        rocketly.mvvmdemo.viewmodel.HotCityListVM vm = mVm;
        android.databinding.ObservableArrayList<rocketly.mvvmdemo.model.Basic> vmCityList = null;

        if ((dirtyFlags & 0x7L) != 0) {



                if (vm != null) {
                    // read vm.cityList
                    vmCityList = vm.getCityList();
                }
                updateRegistration(0, vmCityList);
        }
        // batch finished
        if ((dirtyFlags & 0x7L) != 0) {
            // api target 1

            rocketly.mvvmdemo.utils.databinding.DataBindingExKt.setItem(this.rv, vmCityList);
        }
        if ((dirtyFlags & 0x4L) != 0) {
            // api target 1

            this.srl.setOnRefreshListener(mCallback2);
        }
    }
    // Listener Stub Implementations
    // callback impls
    public final void _internalCallbackOnRefresh(int sourceId ) {
        // localize variables for thread safety
        // vm != null
        boolean vmJavaLangObjectNull = false;
        // vm
        rocketly.mvvmdemo.viewmodel.HotCityListVM vm = mVm;



        vmJavaLangObjectNull = (vm) != (null);
        if (vmJavaLangObjectNull