安卓 MVVM 之禪

點擊數: 【字體: 列印文章 查看評論

  我之前在多個 android 應用中採用過多種途徑來實現 MVP 設計模式,並且過程中經歷了反覆迭代。在歷經多個項目后,我決定嘗試以 android Data Binding 類庫為基礎來實現 MVVM。這次嘗試彷彿讓我陷入了android 編程的極樂世界一般。

  在帶你嘗試這些讓我涅槃的步驟之前,我想先與你分享我在之前給自己設定的一些目標:

 
  • 一個 MVVM 單元應當僅由 ViewModel(VM)、ViewModel 的狀態(M)以及一個綁定的布局資源文件(V)構成。

  • MVVM 單元應當是模塊化的,並且支持嵌套。每個 MVVM 單元應支持包含一個或多個子單元,其中每個子單元仍可能包含自己的子單元。

  • 不需要擴展 Activity類、Fragment類,或者自定義視圖。

  • 每個 ViewModel 的行為應當是可接受和可預期的,並且不依賴任何特殊的 Android 類庫。應該可以使用 Vanilla JUnit 對其進行單元測試。

  • ViewModel 間的關係應當通過依賴注入來實現。

  • 應在布局文件中聲明對 ViewModel 屬性或者方法單向和雙向的數據綁定。

  • ViewModel 不應了解其所支持的 View 的細節。ViewModel 中不應當包含來自 theandroid.view 或者 android.widgetpackages 的任何引用。

  • ViewModel 應當自動綁定到與其配對的 View 的生命周期,並在生命周期結束后自動解除綁定。

  • ViewModel 應當獨立於 Activity 的生命周期,但是當 Activity 需要的時候也可以訪問到 ViewModel。

  • 這個模式需要支持單個或者多個 Activity 的情況。

  寫在前面的話

  在開始的時候,我選擇了一些不出名(但是同樣好用的)工具:用於管理依賴注入的 Toothpick,以及用於導航和管理棧回退(back-stack)的 Okuki(我自己寫的)。我猜別人可能喜歡使用 Dagger 來管理依賴注入(DI),也可能喜歡使用 Intents、EnentBus 來完成導航功能,甚至於使用自定義的導航管理機制。你也可能傾向於使用 Activity 和 Fragments 來進行棧回退的管理。* 以上完全取決於個人。我僅推薦你遵循中心化和松耦合的原則來實現上述功能。只要保證這兩個原則不變,採用了什麼設計模式,如 MVP、MVVM,還是其他 UI 框架都不重要。

  • 在文章最後包含了一種建議的棧回退的管理方式:FragmentManager。

  基礎 ViewModel 及其生命周期

  接下來的步驟里,為了實現依賴注入、導航和棧回退,我定義了一個 ViewModel 基礎介面,並規定了附加、分離相關 View 生命周期的方法。

  首先我定義了一個 ViewModel 介面:

public interface ViewModel {
    void onAttach();
    void onDetach();
}

  下一步,我使用了 data binding 庫中的 View.OnAttachStateListener 來實現綁定,然後將 android:onViewAttachedToWindow 和 android:onViewDetachedFromWindow 映射到我的 ViewModel 類的對應方法當中。我實現了這些方法,並將其關聯到 ViewModel 介面的 onAttach 和 onDetach 方法上。通過這種方式,我可以在相應的擴展類當中隱藏所必需的 View 參數。此外,我還在 View 的生命周期中集成了依賴注入和 Rx 自動訂閱機制。

  我實現的 ViewModel 基礎類:

public abstract class BaseViewModel implements ViewModel {
    private final CompositeDISPosable compositeDISPosable = new CompositeDISPosable();
    @OverrIDE
    public void onAttach() {
    }
    @OverrIDE
    public void onDetach() {
    }
    public final void onViewAttachedToWindow(View view) {
        onAttach();
    }
    public final void onViewDetachedFromWindow(View view) {
        compositeDisposable.clear();
        onDetach();
    }
    protected void addToAutoDispose(Disposable... disposables) {
        compositeDisposable.addAll(disposables);
    }
}

  現在,就可以直接使用該基類的任意 ViewModel 擴展了。你只需要將相應的 ViewModel 綁定到這個布局當中,同時把附加、分離屬性映射到根 ViewGroup 即可。就像下面這樣:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
>  <data>
    <variable name="vm" type="MyViewModel"/>
  </data>
<FrameLayout
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:onViewAttachedToWindow="@{vm::onViewAttachedToWindow}"
  android:onViewDetachedFromWindow="@{vm::onViewDetachedFromWindow}"
>
</FrameLayout>
</layout>

  模塊化單元

  到現在,我已經能夠實現將 ViewModel 綁定到一個視圖以及視圖的生命周期。下一步我需要一種一致的、模塊化的方式將 MVVM 單元載入到容器當中。首先我定義了一個介面,在這個介面中規定了 ViewModel 和布局資源的關聯關係。

public interface MvvmComponent {
    int getLayoutResId();
    ViewModel getViewModel();
}

  接下來,我在 MvvmComponent 中定義了一個自定義的數據綁定關係。這個綁定幫助完成布局的渲染、ViewModel 的綁定,並載入到一個 ViewGroup 當中。

@BindingAdapter("component")
public static void loadComponent(ViewGroup viewGroup, MvvmComponent component) {
  ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), component.getLayoutResId(), viewGroup, false);
  View view = binding.getRoot();
  binding.setVariable(BR.vm, component.getViewModel());
  binding.executePendingBindings();
  viewGroup.removeAllViews();
  viewGroup.addView(view);
}

  需要注意的是,我在渲染的過程中將 attachToParent 參數設置為 false,然後在綁定完成後通過顯式地執行 addView(view) 方法來完成附加。我這樣做的原因是為了 ViewModel 的 onViewAttachedToWindow 方法能夠正常被調用,因為這個方法需要 View 在渲染之前就綁定 ViewModel。

  現在我可以使用新的綁定關係了。在我的布局文件中,我通過新增 component 屬性的方式來添加一個 ViewGroup 容器。

<layout xmlns:android="http://schemas.android.com/apk/res/android"         xmlns:app="http://schemas.android.com/apk/res-auto">   <data>     <variable       name="vm"       type="MyViewModel"/>   </data>   <FrameLayout     android:id="@+id/main_container"     android:layout_width="match_parent"     android:layout_height="match_parent"     android:onViewAttachedToWindow="@{vm::onViewAttachedToWindow}"     android:onViewDetachedFromWindow="@{vm::onViewDetachedFromWindow}"     app:component="@{vm.myComponent}"   />
</layout>

  我通過使用 ObservableField<MvvmComponent> 來在我的 ViewModel 中提供斷開組件的方式。

public class MyViewModel extends BaseViewModel {
  public final ObservableField myComponent 
     = new ObservableField<>();
  @OverrIDE
  public void onAttach() {
    myComponent.set(new HelloWorldComponent("World"));
  }
}

  組件類本身通過對父 ViewModel 的調用,提取出了資源 ID 和子 ViewModel 的定義,並且在父 ViewModel 傳遞過來的數據中,只接受那些子 ViewModel 初始化過程需要的參數。

public class HelloWorldComponent implements MvvmComponent {
private final String name;
  public HelloWorldComponent(String name){
    this.name = name;
  }
  @Override
  public int getLayoutResId() {
    return R.layout.hello_world;
  }
  @Override
  public ViewModel getViewModel() {
    return new HelloWorldViewModel(name);
  }
}

  到現在,子組件可以輕鬆在 ViewModel 狀態的基礎上載入。而這個過程並不需要 ViewModel 對布局、View 或者其他 ViewModel 有任何的了解。

  Activity 生命周期

  按照開始的計劃,我的 MVVM 單元獨立於 Activity 生命周期之外。但有時候我們又需要訪問它。我們可以通過在 Bundle 實例中保存、恢復的方式來實現,也可以通過實現對暫停、恢復事件的響應的辦法來完成。這些都可以根據實際需求來選擇,並且比較簡單。只需要把這些事件委託給一個繼承了 Application.ActivityLifecycleCallbacks 的單例類,就能實現。當然這個單例類需要註冊到當前應用之上。這樣這個單例類就能通過 Listeners 或者 Observables 來暴露出這些事件,並把他們注入到任何需要響應這些事件的 ViewModel當中。

  使用 Fragments 完成棧回退

  我在本帖一開始就提到過,我的棧回退是通過自定義的庫來實現的。但是僅需要一些簡單的改動,你就能將其替換為 Android 自帶的 FragmentManager。為了實現這個目標,需要向 MvvmComponent 介面中增加額外的方法:

public interface MvvmComponent {
    int getLayoutResId();
    ViewModel getViewModel();
    String getTag();
    boolean addToBackStack();
}

  下一步,創建一個 Fragment 來對你的 MVVM 單元進行包裝,像下面這樣:

public class MvvmFragment extends Fragment {
  private int layoutResId;
  private ViewModel vm;
public MvvmFragment newInstance(int layoutResId, ViewModel vm){
    MvvmFragment fragment = new MvvmFragment();
    fragment.layoutResId = layoutResId;
    fragment.vm = vm;
    return fragment;
  }
@Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    ViewDataBinding binding = DataBindingUtil.inflate(inflater, layoutResId, container, false);
    binding.setVariable(BR.vm, vm);
    binding.setVariable(BR.fm, getChildFragmentManager());
    return binding.getRoot();
  }
  public void setLayoutResId(int layoutResId){
    this.layoutResId = layoutResId;
  }
  public void setViewModel(ViewModel vm){
    this.vm = vm;
  }
}

  注意布局文件中需要聲明 fm 數據變數,並且將其設置為 ViewGroup 容器的屬性。同時,需要關注的還有:配置變化時造成的關聯影響、layoutResId 進程僵死,以及你的 MvvmFragment 的 vm 成員屬性。適當的調整你的 Fragment 參數也很有必要。

  現在你可以通過修改自定義組件的方式來使用你的 MvvmFragment,而不是直接渲染並綁定 ViewModel。

@BindingAdapter({"component", "fm"})
public static void loadComponent(ViewGroup viewGroup, MvvmComponent component, FragmentManager fm) {
  MvvmFragment fragment = fm.findFragmentByTag(component.getTag()); 
  if(fragment == null) { 
    fragment = MvvmFragment.newInstance(component.getLayoutResId, component.getViewModel());
  }
  FragmentTransaction ft = beginTransaction();
  ft.replace(viewGroup.getId, fragment, component.getTag());
  if(component.addToBackStack()){
    ft.addToBackStack(component.getTag());
  }
  ft.commit();
}

  示例應用

  如果你想參考一個完整的、使用 MVVM 來實現的(沒有 Fragments)應用示例,可以在 這裡 參考我的例子。

  編程愉快!


建站不易,感謝您的打賞,數額隨意,我會更加努力的^O^
支付寶打賞微信打賞
關於我們 |  站點地圖 |  聯繫方式 |  投稿指南 |  廣告服務 

Copyright © 2005 dn580.com Inc. All rights reserved. 永乐国际 版權所有

手機:15871451580 郵編:430000 EMail:fuanping@163.com

鄂ICP備12003384