Android模組開發框架 LiveData+ViewModel
前言
為何選擇LiveData+ViewModel
- LiveData+ViewModel是Android Architecture Component開發元件的一部分,主要的目的是為了解決android開發過程中的因為Activity及Fragment生命週期引起的一些常見問題,譬如:記憶體洩露,非同步任務引起空指標,橫豎屏切換介面重新整理問題,當然它的作用遠不止於此,比如:LiveData的觀察者模型可以保障介面在第一時間更新到最新的資料(當然你的LifecycleOwner必須是Alive狀態),解決了多端寫入資料的同步問題;使用LiveData實現View和VM的自動繫結(通常這個繫結的資料流向是單向的,VM->View).另外值得一提的是,AAC框架內部維護了一個ViewModel的記憶體快取池,並且會監聽Activity或Fragment的生命週期,在destory的時候自動清空快取.因此,對於開發者而言,只需要聚焦在業務開發,幾乎不用對接生命週期介面.
- 感興趣的同學可以去看看官方的詳細文件和Demo
MVP還是MVVM?
- mvvm相比mvp最大的區別就是實現了v和vm(p)的自動繫結,mvp中的v和p之間存在較多的介面依賴,不利於擴充套件及測試,mvvm通常存在一個Binding中介層,通過註解+apt(或反射)的方式,解除v和vm直接的介面依賴,當然mvvm相比於mvp的進步不僅僅是程式碼解耦,也是從"面向功能介面程式設計"到"響應式程式設計"的思想轉變,一切皆是資料(指令)流(ui<->資料<->model)
- 官方推薦使用MVVM框架,結合DataBinding依賴注入框架實現View和VM的雙向繫結,考慮到使用DataBinding依賴於xml佈局配置,且有較大的理解成本,我們這次沒有采用嚴格意義上的MVVM框架,而是選擇折中方案:
- VM->View:通過LiveData實現資料的單向流動
- View->VM:依然採用傳統的介面實現,但是所有的執行結果都依賴LiveData回傳給View
經典的依賴原則
- 一個框架的好壞,通常會有以下幾個衡量指標:
- 是否可以解決當前的業務問題
- 是否具備好的可擴充套件性
- 是否具備好的可測試性
- 是否遵循模組化設計原則
- 邏輯,介面,資料是否分離
- 當然,還有更多的衡量指標,我們在這裡不一一列舉,上圖所示的是一個圓環依賴結構,從內到外分別是:業務資料->業務邏輯層->介面適配層->介面,遵循"依賴倒置原則",內部圓圈不能依賴外部圓圈
框架介紹
模組的內部層級
- 遵從單向依賴原則,我們的模組內部也劃分了一下三個層級,從下往上分別是:
- 資料層:
- 主要用來提供介面展示及互動所需要資料,通常會定義獲取資料的策略介面,選擇不同的實現(DB,記憶體,網路等)
- 不依賴其他層級,被邏輯層依賴
- 邏輯層(領域層)
- 這一層跟業務強相關,包含複雜的業務邏輯,譬如:獲取資料,提交資料,資料儲存策略的選擇及資料融合等
- 依賴資料層,被展示層依賴
- 展示層(表現層)
- 這一層的主要工作有以下幾個:
- 構建使用者可見的介面
- 為介面展示提供必要的資料
- 接收並處理使用者互動事件
- 複雜的業務邏輯都委派給邏輯層(領域層)來處理,這裡的ViewModel可以理解成一個介面介面卡,只負責建立與View之間的通訊渠道,然後傳遞資料或接受指令,自身並不處理複雜的業務邏輯
- 依賴邏輯層(領域層)
- 這一層的主要工作有以下幾個:
- 資料層:
各層級介紹
展示層:使用LiveData實現MVVM的單向繫結
- View與VM之間的通訊有兩種
- View->VM,通常是使用者互動行為產生的一些指令(可能攜帶一些資料,譬如:使用者登入行為會攜帶賬號密碼)
- VM->View,通常是介面展示所需要的資料(也可能是狀態,譬如:載入資料失敗,展示一個Toast提示等)
- 我們來舉一個簡單的案例,一個列表介面,需要重新整理資料並展示,會有以下幾個必要步驟:
- 首先,View持有一個ViewModel例項(自己例項化,或則外部傳參都可以)
- 通過ViewModel獲取一個LiveData物件(同一類LiveData在ViewModel內只能有一個例項),並開始觀察這個LiveData物件(俗稱subscribe)
- ViewModel接收到"重新整理資料"的指令,委派給具體的UseCase來執行
- UseCase從資料來源獲取到資料,寫入到LiveData
- LiveData通知所有觀察者(當然,會先判斷observer依附的LifecycleOwner是否alive),其中就包括View
- View從LIveData中獲取到最新的完整的資料列表,重新整理展示介面
邏輯層:UseCase處理複雜邏輯
- 前面已經提到了,usecase主要用來處理複雜的業務邏輯,減輕ViewModel負擔
- BaseUseCase可以看做是一個模板方法類(當然這個模板不一定適用所有業務場景),內部會做一些"執行緒排程""LiveData賦值"等業務無相關的操作,具體的業務邏輯交給子類實現
- 這裡有一個Either<Failure, T>返回值,這個是java 8函數語言程式設計的一個特性,類似於c語言裡的union(共同體),主要用來以型別安全的方式返回兩個(或多個)值,感興趣的同學可以自行google
資料層:Repository策略
- 定義一個獲取(讀/寫)資料的策略介面,實現不同的資料讀寫策略,也可以是多個策略的組合使用,根據具體的業務場景來決定,最大的好處就是可擴充套件性好,邏輯層(領域層)不用關心資料具體從哪裡來
使用指南
如何界定一個獨立的子模組
- 模組劃分有兩種典型的思路,"按功能用途分模組","按業務特性分模組",前者的一個常規做法就是按照Model,View,Present(Controler)等角色對檔案進行分組,這樣做最大的弊端就是不利於業務拆分及多人協作程式設計,所以,我們推薦按照"業務特性分模組",譬如:主介面,詳情頁,登入頁等都是一個相對獨立的模組
- 然後,如何界定一個獨立的子模組,需要滿足下面幾個條件:
- 相對獨立的介面展示(android裡的一個Activity或一個Fragment)
- 相對獨立的資料來源(你的介面渲染所需要的資料,可以通過獨立的資料倉儲獲取,譬如:獨立的服務端api介面,獨立的資料表)
- 使用者互動產生的影響儘可能的收斂在介面內(譬如:下拉重新整理產生的資料只用來渲染當前頁面)
- 具備一個閉環的生命週期(模組使用的記憶體是可回收的,不建議用單例來實現跨模組記憶體共享)
- 簡單概括就是:如果一個模組在脫離其他模組的情況下,依然能以預設的方式獨立執行,那麼它就是一個相對獨立的模組
搭建一個子模組
- 我們以一個列表介面為例子,執行效果:
- 按照以下步驟開發
- Step1 資料層:資料倉儲實現
- 定義資料Bean
public class HotContentItem { public String id; public String name; public String desc; public long timeStamp; } 複製程式碼
- 資料倉儲策略實現(資料是本地mock的)
public class HotContentNetRepository { //mock資料 public Either<? extends Failure, List<HotContentItem>> refreshNew() { try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } Either<? extends Failure, List<HotContentItem>> result; Random random = new Random(); boolean success = random.nextInt(10) > 3; if (success) { result = Either.right((mockItemList(0))); } else if (random.nextInt(10) > 3) { result = Either.right(Collections.<HotContentItem>emptyList()); } else { result = Either.left(new NetworkFailure()); } return result; } } 複製程式碼
- Step2 邏輯層:資料倉儲選擇及使用
- 省略列這一步,按照業務需求實現不同的資料倉儲組合使用
- Step3 邏輯層:實現UseCase(示例程式碼:重新整理資料)
public class HotContentRefreshNew extends BaseUseCase<List<HotContentItem>, Void> { private HotContentNetRepository mNetRepository; public HotContentRefreshNew( MutableLiveData<List<HotContentItem>> data, MutableLiveData<Failure> failure) { super(data, failure); mNetRepository = new HotContentNetRepository(); } @Override protected Either<? extends Failure, List<HotContentItem>> loadData(Void aVoid) { //從網路獲取資料 Either<? extends Failure, List<HotContentItem>> result = mNetRepository.refreshNew(); if (result.isRight() && CollectionUtil.isEmpty(result.right())) { Failure failure = new RefreshNewFailure(RefreshNewFailure.CODE_DATA_EMPTY, "Data is empty!"); result = Either.left(failure); } return result; } @Override protected Failure processFailure(Failure failure) { ... } } 複製程式碼
- Step4 展示層:UI框架選擇
- 示例介面是作為一個TabLayout的一個Page頁,因此這裡選擇"具備生命週期View"作為的UI框架,這是個自定的View,實現了LifecycleOwner介面(參考了LifecycleActivity和LifecycleFragment的實現邏輯)
public abstract class BaseLifecycleView extends FrameLayout implements LifecycleOwner { private final LifecycleRegistry mRegistry = new LifecycleRegistry(this); private ViewModelStore mViewModelStore = new ViewModelStore(); public BaseLifecycleView(@NonNull Context context) { super(context); } protected abstract void onCreate(); protected abstract void onDestroy(); @Override public Lifecycle getLifecycle() { return mRegistry; } @Override @CallSuper protected void onAttachedToWindow() { super.onAttachedToWindow(); mRegistry.handleLifecycleEvent(Event.ON_CREATE); onCreate(); if (getVisibility() == View.VISIBLE) { mRegistry.handleLifecycleEvent(Event.ON_START); } } @Override @CallSuper protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mRegistry.handleLifecycleEvent(Event.ON_DESTROY); mViewModelStore.clear(); onDestroy(); } @Override @CallSuper protected void onVisibilityChanged(@NonNull View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); Event event = visibility == View.VISIBLE ? Event.ON_RESUME : Event.ON_PAUSE; mRegistry.handleLifecycleEvent(event); } @Override @CallSuper public void onStartTemporaryDetach() { super.onStartTemporaryDetach(); State state = mRegistry.getCurrentState(); if (state == State.RESUMED) { mRegistry.handleLifecycleEvent(Event.ON_STOP); } } @Override @CallSuper public void onFinishTemporaryDetach() { super.onFinishTemporaryDetach(); State state = mRegistry.getCurrentState(); if (state == State.CREATED) { mRegistry.handleLifecycleEvent(Event.ON_START); } } protected <T extends ViewModel> T getViewModel(@NonNull ViewModelProvider.NewInstanceFactory modelFactory, @NonNull Class<T> modelClass) { return new ViewModelProvider(mViewModelStore, modelFactory).get(modelClass); } } 複製程式碼
- Step5 展示層:定義自己的LiveData和ViewModel
public class HotContentViewModel extends BaseViewModel<List<HotContentItem>> { private HotContentRefreshNew mRefreshNew; public HotContentViewModel() { refreshNew(); } public void refreshNew() { AssertUtil.mustInUiThread(); if (mRefreshNew == null) { mRefreshNew = new HotContentRefreshNew(getMutableLiveData(), getMutableFailure()); } //通過usecase執行具體的重新整理操作 mRefreshNew.executeOnAsyncThread(null); } ... } 複製程式碼
- Step6 展示層:關聯V和VM
public class HotContentView extends BaseLifecycleView { private HotContentViewModel mViewModel; private SwipeRefreshLayout mSwipeRefreshLayout; private AutoLoadMoreRecycleView mRecyclerView; private HotContentAdapter mContentAdapter; public HotContentView(@NonNull Context context) { super(context); 檢視物件初始化 ... mRecyclerView.setLoadMoreListener(new LoadMoreListener() { @Override public void onLoadMore() { HotContentItem lastOne = CollectionUtil.lastOne(mViewModel.getData().getValue()); if (lastOne == null) { mRecyclerView.completeLoadMore("No more data"); } else { mViewModel.loadHistory(lastOne); } } }); ... mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { //重新整理資料 mViewModel.refreshNew(); } }); } @Override protected void onCreate() { mViewModel = getViewModel(new NewInstanceFactory(), HotContentViewModel.class); mViewModel.getData().observe(this, new Observer<List<HotContentItem>>() { @Override public void onChanged(@Nullable List<HotContentItem> hotContentItems) { //重新整理資料成功 mContentAdapter.setItemList(hotContentItems); mSwipeRefreshLayout.setRefreshing(false); ... } }); ... } @Override protected void onDestroy() { } } 複製程式碼
- Step1 資料層:資料倉儲實現
遇到的問題
- 複雜的UI互動指令如何傳達給ViewModel
- 在本文開頭"MVP還是MVVM"框架選型中我們已經提過,目前並沒有使用到MVVM的精髓"DataBinding",而是通過LiveData觀察者模式實現V->VM的單向繫結(即:資料可以從VM自動流向V,但是V的操作指令無法自動傳遞給VM),因此,複雜互動(譬如:下拉重新整理,滾動載入更多)還是需要通過傳統的MVP思維在VM中定義功能介面提供給V來呼叫
- 除了資料之外,還有狀態會影響介面展示
- 理想狀態下,VM提供一個LiveData給View使用,這個LiveData包含了View渲染需要的全部資料,但是很多情況下View並不會只依賴單一型別資料,譬如:下拉重新整理操作,會有以下三種結果返回:列表資料,空資料,失敗.對於"列表資料"我們可以通過LiveData通知View做整體重新整理,但是"空資料""失敗"的情況也需要在介面上有所提示,而這兩個返回值是不能影響當前的"列表資料"(即:不影響當前的列表展示),而應該看做是獨立與資料之外的"指令"更合適,它們最大的特徵就是"一次性",不需要像"列表資料"那樣儲存處理(可以理解成是給介面消費的一次性事件)
- 再回到LiveData,LiveData主要用來儲存相對持久的資料,並且任何時候View從LiveData獲取的資料都必須是"完整的"可以用來直接渲染介面的,回到上面"下拉重新整理"的例子,如果我們將"空資料""失敗"也通過LiveData封裝,然後由View來觀察這個LiveData(自定義一個Observer),在收到對應的"指令"通知的時候處理"介面提示",這樣似乎也能滿足VM->View的狀態通知需求,問題來了,由於Observer的生命週期很可能會比LiveData的生命週期更短(取決於Observer依賴的LifecycleOwner)(比如:Observer的生命週期和ViewPager裡的某一個View一致,LiveData的生命週期和Activity一致),那麼當View被複用的時候會再次觀察同一個LiveData,然後自動收到LiveData的通知,獲取LiveData最新的資料(譬如:"失敗"指令),重新整理介面(提示"重新整理失敗"),這樣就會很奇怪了,明明沒有重新整理動作,平白無故提示"重新整理失敗"
- 解決辦法還是回到"指令"的特徵"一次性",定義一個DisposableLiveData,每次執行setData(會通知觀察者,也就是View)之後立即將data置空,這樣下次再getData時候就會返回null,而不是一個"未預期的資料"
- 程式碼實現很簡單
public class DisposableLiveData<T> extends MutableLiveData<T> { @Override public void postValue(T value) { super.postValue(value); if (value != null) { super.postValue(null); } } @Override public void setValue(T value) { super.setValue(value); if (value != null) { super.postValue(null); } } 複製程式碼
- 示例程式碼:
public class HotContentView extends BaseLifecycleView { private HotContentViewModel mViewModel; private SwipeRefreshLayout mSwipeRefreshLayout; public HotContentView(@NonNull Context context) { super(context); ... mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() {   //重新整理資料 mViewModel.refreshNew(); } }); } @Override protected void onCreate() { mViewModel = getViewModel(new NewInstanceFactory(), HotContentViewModel.class);  ... mViewModel.getFailure().observe(this, new Observer<Failure>() { @Override public void onChanged(@Nullable Failure failure) { //處理失敗提示 if (failure instanceof RefreshNewFailure) { mSwipeRefreshLayout.setRefreshing(false); ToastManager.getInstance().showToast(getContext(), ((RefreshNewFailure)failure).getMessage(), Toast.LENGTH_SHORT); } ... } }); } @Override protected void onDestroy() { } } 複製程式碼