Android模組開發框架 LiveData+ViewModel

Charles007發表於2019-05-11

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

經典的依賴原則

clean_architecture_reloaded_main.png | center | 608x339

  • 一個框架的好壞,通常會有以下幾個衡量指標:
    • 是否可以解決當前的業務問題
    • 是否具備好的可擴充套件性
    • 是否具備好的可測試性
    • 是否遵循模組化設計原則
    • 邏輯,介面,資料是否分離
  • 當然,還有更多的衡量指標,我們在這裡不一一列舉,上圖所示的是一個圓環依賴結構,從內到外分別是:業務資料->業務邏輯層->介面適配層->介面,遵循"依賴倒置原則",內部圓圈不能依賴外部圓圈

框架介紹

模組的內部層級

clean_architecture_reloaded_layers.png | center | 482x372

  • 遵從單向依賴原則,我們的模組內部也劃分了一下三個層級,從下往上分別是:
    • 資料層:
      • 主要用來提供介面展示及互動所需要資料,通常會定義獲取資料的策略介面,選擇不同的實現(DB,記憶體,網路等)
      • 不依賴其他層級,被邏輯層依賴
    • 邏輯層(領域層)
      • 這一層跟業務強相關,包含複雜的業務邏輯,譬如:獲取資料,提交資料,資料儲存策略的選擇及資料融合等
      • 依賴資料層,被展示層依賴
    • 展示層(表現層)
      • 這一層的主要工作有以下幾個:
        • 構建使用者可見的介面
        • 為介面展示提供必要的資料
        • 接收並處理使用者互動事件
      • 複雜的業務邏輯都委派給邏輯層(領域層)來處理,這裡的ViewModel可以理解成一個介面介面卡,只負責建立與View之間的通訊渠道,然後傳遞資料或接受指令,自身並不處理複雜的業務邏輯
      • 依賴邏輯層(領域層)

各層級介紹

展示層:使用LiveData實現MVVM的單向繫結

clean_architecture_reloaded_mvvm_app.png | center | 422x202

  • 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.png | center | 434x411

  • 前面已經提到了,usecase主要用來處理複雜的業務邏輯,減輕ViewModel負擔
  • BaseUseCase可以看做是一個模板方法類(當然這個模板不一定適用所有業務場景),內部會做一些"執行緒排程""LiveData賦值"等業務無相關的操作,具體的業務邏輯交給子類實現
  • 這裡有一個Either<Failure, T>返回值,這個是java 8函數語言程式設計的一個特性,類似於c語言裡的union(共同體),主要用來以型別安全的方式返回兩個(或多個)值,感興趣的同學可以自行google

資料層:Repository策略

clean_archictecture_reloaded_repository.png | center | 373x313

  • 定義一個獲取(讀/寫)資料的策略介面,實現不同的資料讀寫策略,也可以是多個策略的組合使用,根據具體的業務場景來決定,最大的好處就是可擴充套件性好,邏輯層(領域層)不用關心資料具體從哪裡來

使用指南

如何界定一個獨立的子模組

  • 模組劃分有兩種典型的思路,"按功能用途分模組","按業務特性分模組",前者的一個常規做法就是按照Model,View,Present(Controler)等角色對檔案進行分組,這樣做最大的弊端就是不利於業務拆分及多人協作程式設計,所以,我們推薦按照"業務特性分模組",譬如:主介面,詳情頁,登入頁等都是一個相對獨立的模組
  • 然後,如何界定一個獨立的子模組,需要滿足下面幾個條件:
    • 相對獨立的介面展示(android裡的一個Activity或一個Fragment)
    • 相對獨立的資料來源(你的介面渲染所需要的資料,可以通過獨立的資料倉儲獲取,譬如:獨立的服務端api介面,獨立的資料表)
    • 使用者互動產生的影響儘可能的收斂在介面內(譬如:下拉重新整理產生的資料只用來渲染當前頁面)
    • 具備一個閉環的生命週期(模組使用的記憶體是可回收的,不建議用單例來實現跨模組記憶體共享)
  • 簡單概括就是:如果一個模組在脫離其他模組的情況下,依然能以預設的方式獨立執行,那麼它就是一個相對獨立的模組

搭建一個子模組

  • 我們以一個列表介面為例子,執行效果:

device-2018-05-23-202059.png | center | 345x613

  • 按照以下步驟開發
    • 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() {
        }
    
    }
    複製程式碼

遇到的問題

  • 複雜的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() {
                   &emsp;&emsp;//重新整理資料
                      mViewModel.refreshNew();
                  }
              });
          }
      
          @Override
          protected void onCreate() {
              mViewModel = getViewModel(new NewInstanceFactory(), HotContentViewModel.class);
            &emsp;...
              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() {
          }
      
      }
      複製程式碼

相關文章