前言
學習了 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 中的熱信號作爲數據源, 從而簡化邏輯代碼的實現, 當然這需要自己管控好生命週期