美文网首页Android技术知识Android开发MVVM
MVVM 架构演进(三) —— 架构的搭建

MVVM 架构演进(三) —— 架构的搭建

作者: SharryChoo | 来源:发表于2019-06-12 20:45 被阅读2次

    前言

    学习了 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="SharryChooCHN@Gmail.com">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="SharryChooCHN@Gmail.com">Contact me.</a>
     * @version 1.0
     * @since 2019-05-20 16:28
     */
    public interface RemoteDataSource {
    
    }
    
    
    /**
     * 定义本地数据源(SP, 数据库...)
     *
     * @author Sharry <a href="SharryChooCHN@Gmail.com">Contact me.</a>
     * @version 1.0
     * @since 2019-05-20 16:28
     */
    public interface LocalDataSource {
    
    }
    
    /**
     * @author Sharry <a href="SharryChooCHN@Gmail.com">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="SharryChooCHN@Gmail.com">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 中的热信号作为数据源, 从而简化逻辑代码的实现, 当然这需要自己管控好生命周期

    参考文献

    相关文章

      网友评论

        本文标题:MVVM 架构演进(三) —— 架构的搭建

        本文链接:https://www.haomeiwen.com/subject/vylezqtx.html