MVVM 架構演進(三) —— 架構的搭建

前言

學習了 MVVM 的 Demo, 翻閱了 DataBinding 的實現源碼, 讓我們對 MVVM 框架有了一個整體上的瞭解, 用一句話來概括就是, MVVM 即通過 DataBinding 來解除 Presenter 與 View 依賴的 MVP 架構, 這樣的 Presenter 稱之爲 ViewModel

不過 Demo 中的示例, 真實放到項目中, 會發現很多應用場景使用起來非常的困難, 甚至會讓我們覺得沒有 MVP 好用, 這篇文章記錄了筆者在 MVP 向 MVVM 演進過程中的所見所想, 希望對看到這篇文章的人有所幫助

一. 設計要點

MVVM 架構的搭建核心重點是爲了解決 View 與 ViewModel 的通信問題

結構設計

  • View 層可以持有 ViewModel 的引用
    • 當數據不方便 xml 中通過 DataBinding 綁定時, 需要用代碼指定
  • ViewModel 中不存在 View 的引用
    • ViewModel 中可以持有 Application 的引用, 以保證功能的易用性
  • 考慮後續功能拓展性

功能的易用性

  • 需要在 ViewModel 中快捷的通知 view 狀態變更
    • 通知彈窗, 空數據, 網絡異常, 加載框等
  • 考慮數據的雙向綁定, 如 EditText
  • 考慮內存泄漏出現的常見場景
    • LiveData 去維護生命週期

二. 技術選取

View + DataBinding(LiveData/ObservableField) + ViewModel

  • View 層定義 View 常用操作
  • DataBinding 處理 View 與 ViewModel 的雙向綁定
    • 使用InverseBindingAdapter 處理數據反向注入
  • LiveData 解決數據推送時聲明週期的問題
  • ViewModel 處理數據邏輯

三. 框架搭建的實施

一) View 層

1. BaseView 的創建

View 層的搭建, 與 MVP 中的 View 基本一致

public interface BaseView<T extends ViewDataBinding> {

    /**
     * Do init operation when data binding created.
     *
     * @param dataBinding the data binding that need init.
     */
    void initDataBinding(@NonNull T dataBinding);

}

與 MVP 不同的是這裏的 BaseView 關聯的泛型是 ViewDataBinding 類型, 爲什麼不直接使用 ViewModel 呢?

  • 這是因爲 ViewModel 是不允許持有 View 引用的, 所以 ViewModel 的可移植性遠遠高於 Presenter, 我們可以所以的在 XML 中聲明多個 ViewModel, 因此這裏並沒有讓 View 層直接關聯 ViewModel 的泛型

這裏的 BaseView 中只有一個方法 initDataBinding, 即在獲取到 ViewDataBinding 的實例之後, 執行 ViewDataBinding 初始化的操作, 我們可以在這個方法中, 爲 DataBinding 的生產類, 關聯對應的 View 和 ViewModel

最基礎的 BaseView 實現了, 不過這個功能似乎太過於簡單了一些, 我們在 Activity, Fragment 等頁面搭建的過程中 Toast、 Tips、 EmptyData 幾乎是必用的功能, 因此我們這裏再定義一個 BaseView 的增強版

/**
 * The View provider more function.
 *
 * @author Sharry <a href="[email protected]">Contact me.</a>
 * @version 1.0
 * @since 2018/8/28 22:21
 */
public interface SupportView<T extends ViewDataBinding> extends BaseView<T> {

    /**
     * Show simple tips.
     */
    void tip(@Nullable String msg);

    /**
     * Show toast.
     */
    void toast(@Nullable String msg);

    /**
     * Show snack bar.
     */
    void snackBar(@Nullable String msg);

    /* ============================== Progress Bar =======================================*/

    /**
     * Show progress view associated with current page
     * Use default attach view {@code R.android.id.cåontent}.
     */
    void progress(boolean isShow);

    /**
     * Show progress view associated with current page.
     */
    void progress(@NonNull View attached, boolean isShow);

    /* ============================== Empty data =======================================*/

    /**
     * Show empty data without msg associated with current page.
     * Use default attach view {@code R.android.id.content}.
     */
    void showEmptyData();

    /**
     * Show empty data without msg associated with current page.
     */
    void showEmptyData(@NonNull View attached);

    /* ============================== Network Error =======================================*/

    /**
     * Show network disconnected associated with current page.
     * Use default attach view {@code R.android.id.content}.
     */
    void showNetworkError(OnNetworkErrorListener listener);

    /**
     * Show network disconnected associated with current page.
     */
    void showNetworkError(@NonNull View attached, OnNetworkErrorListener listener);

    /**
     * Callback associated with disconnected view.
     */
    interface OnNetworkErrorListener {
        void onNetworkError();
    }

}

好的, 可以看到這個 SupportView 幾乎涵蓋了我們開發中最常用的 View 層的通用方法, 只需要讓我們的 BaseActivity/BaseFragment 實現這個 SupportView 就可以了

接下來我們以 Activity 爲例, 看看 BaseView 的實現

2. BaseView 的實現

我們先定義一個模板 Activity, 然後再此基礎上進行 MVVM 的實現類拓展

public abstract class BaseActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 1. Parse intent from other activity.
        Intent data = getIntent();
        if (null != data) {
            parseIntent(data);
        }
        // 2. Inject layout resource to content view.
        createView(getLayoutResId());
        // 3. Initialize view
        initViews();
        // 4. Initialize data after view display on screen.
        new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
                initData();
            }
        });
    }

    /**
     * U can parse intent that transfer from other activity.
     *
     * @param intent data that from request Activity.
     */
    protected void parseIntent(@NonNull Intent intent) {
    }

    /**
     * Get layout resource associated with this activity.
     *
     * @return layout id.
     */
    protected abstract int getLayoutResId();

    /**
     * Create view by u custom.
     */
    protected void createView(int layoutResId) {
        setContentView(layoutResId);
    }
    ......
}

可以看到這裏簡單的定義了一些模板方法, 用戶可以按照需求自己去重寫實現, 接下來我們看看 BaseMvvmActivity 的實現

public abstract class BaseMvvmActivity<DataBinding extends ViewDataBinding> extends BaseActivity
        implements SupportView<DataBinding> {

    protected DataBinding dataBinding;

    @Override
    protected void createView(int layoutResId) {
        dataBinding = DataBindingUtil.setContentView(this, layoutResId);
        if (dataBinding == null) {
            throw new NullPointerException("Cannot find ViewDataBinding that layout id is: " + layoutResId);
        }
        initDataBinding(dataBinding);
    }

    @Override
    public void tip(@Nullable String msg) {
        // TODO: Custom u simple tip display.
    }

    @Override
    public void toast(@Nullable String msg) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void snackBar(@Nullable String msg) {
        // TODO: Custom u snackbar display.
    }
    ......
}

可以看到支持 MVVM 架構的 Activity 只需要重寫 createView 這個方法就可以實現其功能了

  • SupportView 中定義的接口, 根據當前 App 的 UI 進行通用展示

好的, 從這裏就可以看到多一個 BaseActivity 的好處了, 定義一個基礎的模板, 我們可以在其基礎上進行拓展, 在不改變使用方式的前提下實現對 MVP, MVVM 架構的支持, 遵守了開閉原則(對拓展開放, 對修改封閉), 也對日後新架構的拓展提供了可能

思考

在前面的文章我們瞭解到 View 層數據的變更是通過在 xml 中指定了 ViewModel 中數據源之後, 由數據源通知的, 如下所示

......
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAllCaps="false"
        // 這裏綁定了 ViewModel 中的數據源
        android:text="@{viewmodel.messageText}"
    />
......

但我們在 BaseMvvmActivity 中實現的 SupportView 接口(如 toast), 很明顯有些是無法在 xml 中與 ViewModel 中的數據源綁定的, 但 ViewModel 中是 沒有 View 引用的, 因它如何如同 MVP 一樣定義一個 showMsg, 在 Presetner 中愉快的調用它讓 View 彈出一個吐司, 那麼 MVVM 應該它如何讓 View 彈吐司呢?

想清楚這個問題, MVVM 架構就已經完全難不倒你了, 接下來我們看看 ViewModel 層的定義

二) ViewModel 層

public abstract class SupportViewModel extends AndroidViewModel {

    /**
     * The viewStatusSource associated with the special view that using this ViewModel.
     */
    protected final SingleLiveData<SupportViewStatus> viewStatusSource = new SingleLiveData<>();

    /**
     * The tip message associated with the special view that using this ViewModel.
     */
    protected final SingleLiveData<String> tipMsgSource = new SingleLiveData<>();

    /**
     * The toast message associated with the special view that using this ViewModel.
     */
    protected final SingleLiveData<String> toastMsgSource = new SingleLiveData<>();

    public SupportViewModel(@NonNull Application application) {
        super(application);
    }

    /**
     * Set a observer for toastMsgSource.
     */
    public void setToastMsgSourceObserver(@NonNull LifecycleOwner owner,
                                          @NonNull ToastObserver toastObserver) {
        Preconditions.checkNotNull(owner);
        Preconditions.checkNotNull(toastObserver);
        toastMsgSource.observe(owner, toastObserver);
    }
    ......
}

是否有種恍然大悟的感覺, 筆者在 SupportViewModel 中定義了一組數據源, 可以看到一個 SingleLiveData 類型的 toastMsg, 這裏的 SingleLiveData 可以先當做一個 LiveData, 那麼 LiveData 的作用是什麼呢?

  • LiveData 與 Observable 類似, 也是一個可被觀察的數據源, 不過它的優勢在於, 它可以幫助我們管控生命週期, 這正是它迷人的地方

除了定義數據源之外, 還爲數據源提供了添加觀察者的方法, 如 setToastMsgSourceObserver 等, 其他數據源的添加觀察者的方式與之類似

  • 只需要在 view 層調用這個方法, 將 view 層實現的觀察者加入, 便可以實現數據推送了

接下來看看 Model 層的定義

三) Model 層

筆者項目中 Model 層的設計, 可能有些不同, 它異常的簡單

/**
 * 定義網絡數據源
 *
 * @author Sharry <a href="[email protected]">Contact me.</a>
 * @version 1.0
 * @since 2019-05-20 16:28
 */
public interface RemoteDataSource {

}


/**
 * 定義本地數據源(SP, 數據庫...)
 *
 * @author Sharry <a href="[email protected]">Contact me.</a>
 * @version 1.0
 * @since 2019-05-20 16:28
 */
public interface LocalDataSource {

}

/**
 * @author Sharry <a href="[email protected]">Contact me.</a>
 * @version 1.0
 * @since 2019-05-20 16:28
 */
public interface DataSource extends LocalDataSource, RemoteDataSource {

    DataSource INSTANCE = new DataSourceRepository();

}


/**
 * 數據源實現類
 *
 * @author Sharry <a href="[email protected]">Contact me.</a>
 * @version 1.0
 * @since 2019-05-20 16:30
 */
class DataSourceRepository implements DataSource {

}

可以看到 Model 層的設計非常簡單, 這是一個全局的數據源, 所有的 ViewModel 都可以通過 DataSource.INSTANCE 獲取實現類, 從中獲取數據

這個設計我第一次看到時, 也非常的震驚, 因爲在之前的印象中 Model 與 Presenter 是一一對應的, 所以看到一個單一的數據源時有些難以接受, 不過用下來之後卻發現異常的舒服

  • 不用考慮一個 Presetner/ViewModel 對應多個 Model 的苦惱
  • 通過單一數據源對上層提供, 能夠取到所有的數據, 組件化落實時也可以減輕跨模塊獲取數據的困擾
  • 最後, 這個設計師從 Goggle

到這裏 MVVM 架構的搭建基本上就結束了, 最後再看一個數據雙向綁定的問題

四) 數據的雙向綁定

因爲 ViewModel 層與 View 完成隔離, 所以 ViewModel 層只能夠通過提供數據源, 讓 View 層觀察的方式進行通信(DataBinding 的實現原理也是如此), 不過我們不能忽略的是, 有些數據是在 View 層主動產生的, 如 EditText 的主動輸入, 這種場景下我們如何將數據反向注入到 ViewModel 中的數據源呢?

當然, 可以在 ViewModel 中定義一個方法, 當 View 層數據主動變更時, 通過調用 ViewModel 中的方法, 將數據注入, 似乎有些不太優雅, 這個時候 @BindingAdapter/@InverseBindingAdapter 就派上用場了

public class Sample1BindingAdapters {


    /**
     * 數據的正向推送
     * <p>
     * {@code app:text="@{viewmodel.xxx}"} viewmodel.xxx 發生變更時, 將數據推送給觀察者
     */
    @BindingAdapter("text")
    public static void setEditTextContent(EditText editText, String newStr) {
        String oldStr = editText.getText().toString();
        // 解決正向推送與反向注入的死循環
        if (!oldStr.equals(newStr)) {
            editText.setText(newStr);
        }
    }

    /**
     * 獲取反向注入的數據
     * <p>
     * {@code app:text="@={viewmodel.xxx}"} app:text 發生變更時, 將數據反向注入給被觀察者
     */
    @InverseBindingAdapter(
            attribute = "text",
            event = "onEditTextChanged"
    )
    public static String getEditTextContent(EditText editText) {
        return editText.getText().toString();
    }

    /**
     * 反向注入發起
     */
    @BindingAdapter(value = "onEditTextChanged", requireAll = false)
    public static void onEditTextChanged(EditText editText, final InverseBindingListener textAttrChanged) {
        if (textAttrChanged != null) {
            editText.addTextChangedListener(new TextWatcher() {
                @Override
                public void beforeTextChanged(CharSequence s, int start, int count, int after) {

                }

                @Override
                public void onTextChanged(CharSequence s, int start, int before, int count) {

                }

                @Override
                public void afterTextChanged(Editable s) {
                    // 文本變更之後, 發起數據的反向注入
                    textAttrChanged.onChange();
                }
            });
        }
    }

}

上面的註釋也非常的清晰, 這是 DataBinding 提供的拓展功能的實現, 其使用語法爲 app:text = "@={viewmodel.xxx}", 描述的是 text 屬性與 viewmodel.xxx 的雙向綁定

  • 當 ViewModel 中的數據源變更時, 會調用 setEditTextContent 方法做出相應的 UI 變更
  • 當 View 層發生變更時, 會調用 getEditTextContent 獲取數據注入到 ViewModel 的數據源中
    • 推送的時機在 onEditTextChanged 中自行定義

好的, 到這裏我們 MVVM 的框架的搭建便進入尾聲了, 接下來做個總結

總結

不知道大家是否有這樣的感覺, MVVM 框架的搭建比起 MVP 要簡單的多, 我認爲這是因爲系統幫我們做了最重要的事情, 那便是 DataBinding, 初始寫 MVVM 架構的時候, 可能會因爲 ViewModel 中沒有 View 而手足無措, 這個時候只需要將思維轉變, 讓 View 主動訂閱 ViewModel 中的數據源即可實現最終目標

這是一個響應式的過程, 筆者把這裏的內容整理成了 Demo, 希望能夠幫助大家進一步理解 MVVM 架構

展望

這樣的 MVVM 架構, 已經能夠滿足日常開發需求了, 不過因爲在 ViewModel 中含有對 ObservableField, LiveData 等 Android 依賴庫, 讓 ViewModel 層的單元測試變得比 MVP 中 Presenter 要困難的多, 有興趣的小夥伴, 可以研究一下如何改進

面對複雜的邏輯關係控制, LiveData 和 ObservableField 可能難以勝任, 熟悉 RxJava 的開發者們可以在 ViewModel 中使用 RxJava 中的熱信號作爲數據源, 從而簡化邏輯代碼的實現, 當然這需要自己管控好生命週期

參考文獻

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章