前言
学习了 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 中的热信号作为数据源, 从而简化逻辑代码的实现, 当然这需要自己管控好生命周期