1. 程式人生 > >MVVM 架構與數據綁定庫

MVVM 架構與數據綁定庫

利用 gin ase dbi 返回值 efault 不用 lns oid

Model-View-Presenter(MVP),即模型-視圖-表示層,架構被廣泛應用於 Android 應用程序,通過引入表示層將視圖與表示邏輯和模型分離。Model-View-ViewModel(MVVM),即模型-視圖-視圖模型,與 MVP 非常相似,視圖模型充當增強的表示層,使用數據綁定器保持視圖模型和視圖同步。通過將視圖綁定到視圖模型屬性上,數據綁定程序可以處理視圖更新而無需手動更改數據來設置視圖(例如,不用再設置控件 TextView 的setTest() 或者 setVisibility() 屬性)。與 MVP 中的表示層一樣,視圖模型可以很容易地進行單元測試。本文介紹了數據綁定庫和 MVVM 架構模式,以及它們在 Android 上協同工作方式。
數據綁定
什麽是數據綁定?
技術分享圖片
數據綁定是一種把數據綁定到用戶界面元素(控件)的通用機制。通常,數據綁定會將數據從本地存儲或者網絡綁定到顯示層,其特征是數據的改變會自動在數據源和用戶界面之間同步。

數據綁定庫的好處

TextView textView = (TextView) findViewById(R.id.label);
EditText editText = (EditText) findViewById(R.id.userinput);
ProgressBar progressBar = (ProgressBar) findViewById(R.id.progress);

editText.addTextChangedListener(new TextWatcher() {
   @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
   @Override public void afterTextChanged(Editable s) { }
   @Override public void onTextChanged(CharSequence s, int start, int before, int count) {
       model.setText(s.toString());
   }
});

textView.setText(model.getLabel());
progressBar.setVisibility(View.GONE);

如上述代碼所示,大量的 findViewById() 調用之後,又是一大堆 setter/listener 之類的調用。 即使使用 ButterKnife 註入庫也沒有使情況改善。而數據綁定庫就能很好地解決這個問題。

在編譯時創建一個綁定類,它為所有視圖提供一個 ID 字段,因此不再需要調用 findViewById() 方法。實際上,這種方式比調用 findViewById() 方法快數倍,因為數據綁定庫創建代碼僅需要遍歷視圖結構一次。

綁定類中也實現了視圖文件的綁定邏輯,因此所有 setter 會在綁定類中被調用,你無須為之操心。總之,它能讓你的代碼變得更簡潔。

如何設置數據綁定?

android {
   compileSdkVersion 25
   buildToolsVersion "25.0.1"
   ...
   dataBinding {
       enabled = true
   }
   ...
}

首先在 app 的 build.gradle 中添加 dataBinding { enabled = true }。之後構建系統會收到提示對數據綁定啟用附加處理,如,從布局文件創建綁定類。

<layout xmlns:android="http://schemas.android.com/apk/res/android">
  <data>
    <variable name="vm" type="com.example.ui.main.MainViewModel" />
    <import type="android.view.View" />
  </data>
  ...
</layout>

接下來,在 <layout> 標簽中包裝下布局中的頂層元素,以便為此布局創建綁定類。綁定類具有和布局 xml 文件相同的名稱,只是在結尾添加 Binding,例如, Activity_main.xml 的綁定類名字是 ActivityMainBinding。 如上所示,命名空間的聲明也移到布局標記中。然後,在布局標記內聲明將需要綁定的數據作為變量,並設置好名稱和類型。示例中,唯一的變量是視圖模型,但後續變量會增加。你可以選擇導入類,以便能使用 View.VISIBLE 或靜態方法等常量。
如何綁定數據?

<TextView
    android:id="@+id/my_layout"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:visibility="@{vm.visible ? View.VISIBLE : View.GONE}">
    android:padding="@{vm.bigPadding ? @dimen/paddingBig : @dimen/paddingNormal}"
    android:text=‘@{vm.text ?? @string/defaultText + "Additional text."}‘ />

視圖屬性上的數據綁定指令以@開頭,以大括號結束。你可以使用任何變量在數據段中導入你之前聲明的變量。這些表達式基本支持你在代碼中的所有操作,例如算術運算符或字符串連接。

Visibility 屬性中還支持 if-then-else 三元運算符。還提供了合並運算符 ??,如果左邊的值為空,則返回右操作數。在上述代碼中,你可以像在正常布局中一樣訪問資源,因此你可以根據布爾變量的取值選擇不同的 dimension 資源,也可以使用 padding 屬性查看這些資源。

即使你在代碼中使用 getters 和 setters,你所聲明的變量的屬性也可以用字段訪問語法的形式訪問。你可以在 slide 上的文本屬性中看到此部分,其中 vm.text 調用視圖模型的 getText() 方法。最後,一些小的限制也適用,例如,不能創建新對象,但是數據綁定庫仍然非常強大。
哪些屬性是可以綁定的?

android:text="@{vm.text}"
android:visibility="@{vm.visibility}"
android:paddingLeft="@{vm.padding}"
android:layout_marginBottom="@{vm.margin}"
app:adapter="@{vm.adapter}"

實際上,標準視圖的大多數屬性已經被數據綁定庫支持。在數據綁定庫內部,當你使用數據綁定時,庫按照視圖類型查找屬性名稱的 setter。例如,當你把數據綁定到 text 屬性時,綁定庫會在視圖類中使用合適的參數類型查找 setText() 方法,上述示例是 String。

當沒有對應的布局屬性時,你也可以使用數據綁定的 setter。例如,你可以在 xml 布局中的 recycleler 視圖上使用 app:adapter 屬性,以利用數據綁定設置適配器參數。

對於標準屬性,不是所有的都在 View 上有對應的 setter 方法。例如,paddingLeft 情況下,數據綁定庫支持自定義的 setter,以便將綁定轉移到 padding 屬性上。但是,遇到 layout_marginBottom 的情況,當綁定庫沒有提供自定義 setter 時我們要怎麽處理呢?
自定義 Setter

@BindingAdapter("android:layout_marginBottom")
public static void setLayoutMarginBottom(View v, int bottomMargin) {
   ViewGroup.MarginLayoutParams layoutParams =
           (ViewGroup.MarginLayoutParams) v.getLayoutParams();

   if (layoutParams != null) {
       layoutParams.bottomMargin = bottomMargin;
   }
}

對於上述情況,自定義 setter 可以被重寫。Setter 是使用 @BindingAdapter 註解來實現的,布局屬性使用參數命名,使得綁定適配器被調用。上面示例提供了一個用於綁定 layout_marginBottom 的適配器。

方法必須是 public static void ,而且必須接受綁定適配器調用的首個視圖類型作為參數,然後將數據強綁定到你需要的類型。在這個例子中,我們使用一個 int 類型為類型 View(子類型)定義一個綁定適配器。最後,實現綁定適配器接口。對於 layout_marginBottom,我們需要獲取布局參數,並且設置底部間隔:

@BindingAdapter({"imageUrl", "placeholder"})
public static void setImageFromUrl(ImageView v, String url, int drawableId) {
   Picasso.with(v.getContext().getApplicationContext())
           .load(url)
           .placeholder(drawableId)
           .into(v);
}

也可能需要設置多種屬性以綁定適配器調用。為了達到此目的,MMVM 會提供你的屬性名稱列表並用於 @BindingAdapter 實現註解。另外,在現有方法中,每個屬性都有自己的名稱。只有在所有聲明的屬性被設置後,這些 BindingAdapter 才會被調用。

在加載圖片過程中,我想為加載圖片定義一個綁定適配器來綁定 URL 與 placeHolder。如你所見,通過使用 Picasso image loading library,綁定適配器非常容易實現。你可以在自定義綁定適配器中使用任何你想要的方法。
在代碼中使用綁定

MyBinding binding;

// For Activity
binding = DataBindingUtil.setContentView(this, R.layout.layout);
// For Fragment
binding = DataBindingUtil.inflate(inflater, R.layout.layout, container, false);
// For ViewHolder
binding = DataBindingUtil.bind(view);

// Access the View with ID text_view
binding.textView.setText(R.string.sometext);

// Setting declared variables
binding.set<VariableName>(variable);

現在我們在 xml 文件中定義了綁定,並且編寫了自定義 setter,那我們如何在代碼中使用綁定呢? 數據綁定庫通過生成綁定類為我們完成所有的工作。要獲取布局的相應綁定類的實例,就要用到庫提供的輔助方法。Activity 對應使用 DataBindingUtil.setContentView(),fragment 對應使用 inflate(),視圖擁有者請使用 bind()。 如前所述,綁定類為定義 final 字段的 ID 提供了所有視圖。同樣,您可以在綁定對象的布局文件中設置你所聲明的變量。
自動更新布局
如果使用數據綁定,在數據發生變化時,庫代碼可以控制布局自動更新。然而,庫仍然需要獲得關於數據變化的通知。如果綁定的變量實現了 Observable 接口(不要跟 RxJava 的 Observable混淆了)就能解決這個問題。

對於像 int 和 boolean 這樣的簡單數據類型,庫已經提供了合適的實現 Observable 的類型,比如 ObservableBoolean。還有一個 ObservableField 類型用於其它對象,比如字符串。

public class MyViewModel extends BaseObservable {
   private Model model = new Model();

   public void setModel(Model model) {
       this.model = model;
       notifyChange();
   }

   public void setAmount(int amount) {
       model.setAmount(amount);
       notifyPropertyChanged(BR.amount);
   }

   @Bindable public String getText() { return model.getText(); }
   @Bindable public String getAmount() { return Integer.toString(model.getAmount()); }
}

在更復雜的情況下,比如視圖模型,有一個 BaseObservable 類提供了工具方法在變化時通知布局。就像上面在 setModel() 方法中看到那樣,我們可以在模型變化之後通過調用 notifyChange() 來更新整個布局。

再看看 setAmount(),你會看到模型中只有一個屬性發生了變化。這種情況下,我們不希望更新整個布局,只更新用到了這個屬性的部分。為達此目的,可以在屬性對應的 getter 上添加 @Bindable 註解。然後 BR 類中會產生一個字段,用於傳遞給 notifyPropertyChanged() 方法。這樣,綁定庫可以只更新確實依賴變化屬性的部分布局。
匯總
? 在布局文件中申明變量並將之與視圖中的屬性綁定。

? 在代碼中創建綁定來設置變量。

? 確保你的變量類型實現了 Observable 接口 —— 可以從 BaseObservable 繼承 —— 這樣數據變化時會自動反映到布局上。

模型、視圖、視圖模型(MVVM)架構

技術分享圖片
現在來看看 MVVM 架構,以及它的三個組成部分是如何一起工作的。

視圖是用戶界面,即布局。在 Android 中通常是指 Activity、Fragment 或者 ViewHolder 以及配合它們使用的 XML 布局文件。

模型就是業務邏輯層,提供方法與數據進行互動。

視圖模型就像是視圖和模型的中間人,它既能訪問模型的數據,又包含 UI 狀態。它也定義了一些命令可以被事件,比如單擊事件調用。視圖模型包含了應用中的呈現邏輯。

在 MVVM 架構模式中,模型和視圖模型主要通過數據綁定來進行互動。理想情況下,視圖和視圖模型不必相互了解。綁定應該是視圖和視圖模型之間的膠水,並且處理兩個方向的大多數東西。然而,在Anroid中它們不能真實的分離:

你要保存和恢復狀態,但現在狀態在視圖模型中。

你需要讓視圖模型知道生命周期事件。

你可能會遇到需要直接調用視圖方法的情況。

在這些情況下,視圖和視圖模型應該實現接口,然後在需要的時候通過命令通信。視圖模型的接口在任何情況都是需要的,因為數據綁定庫會處理與視圖的交互,並在上下文需要的時候使用自定義組件。

視圖模型還會更新模型,比如往數據庫添加新的數據,或者更新一個現有數據。它也用於從模型獲取數據。理想情況下,模型也應該在變化的時候通知視圖模型,但這取決於實現。

一般來說,視圖和視圖模型的分離會讓呈現邏輯易於測試,也有助於維持長期運行。與數據綁定庫一起會帶來更少更簡潔的代碼。
示例

<layout xmlns:android="...">
  <data>
    <variable name="vm" type="pkg.MyViewModel" />
  </data>

  <FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <EditText
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:visibility="@{vm.shouldShowText}"
      android:text="@={vm.text}" />

    <Button
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:onClick="@{vm::onButtonClick}"
      android:text="@string/button"/>
  </FrameLayout>
</layout>

使用 MVVM 的時候,布局只引用一個變量,即這個視圖的視圖模型,在這個示例中是 MyViewModel。在視圖模型中,你需要提供布局所需要的屬性,其簡單復雜程度取決於你的用例。

public class MyViewModel extends BaseObservable {
   private Model model = new Model();

   public void setModel(Model model) {
       this.model = model;
       notifyChange();
   }

   public boolean shouldShowText() {
       return model.isTextRequired();
   }

   public void setText(String text) {
       model.setText(text);
   }

   public String getText() {
       return model.getText();
   }

   public void onButtonClick(View v) {
       // Save data
   }
}

這裏有一個 text 屬性。將 EditText 用於用戶輸入的時候,可以使用雙向綁定,同時,數據綁定庫將輸入反饋回視圖模型。為此,我們創建一個 setter 和 getter 並將屬性綁定到 EditText 的 text 屬性,這時候大括號前面的 = 號標誌著我們要在這裏進行雙向綁定。

另外,我們只想在模型需要輸入 text 的時候顯示 EditText。這種情況下,我們會在視圖模型中提供一個布爾屬性將其與 visibility 屬性綁定。為了讓它工作,我們還要創建一個綁定適配器(BindingAdapter),在值為 false 的時候設置 visibility 為 GONE,在值為 true 的時候設置為 VISIBLE。

@BindingAdapter("android:visibility")
public static void setVisibility(View view, boolean visible) {
   view.setVisibility(visible ? View.VISIBLE : View.GONE);
}

最後,我們想在點擊 Button 時存儲信息,於是,在視圖模型中創建一個 onButtonClick() 命令,它負責處理與模型的交互。在布局中,我們通過對該方法引用將命令綁定到 Button 的 onClick 屬性上。為了使它直接工作,我們需要在方法中引入一個 View 的單個參數,類似於 OnClickListener。如果你不想使用 View 參數,你也可以直接在布局中使用 lambda 表達式。

為方便測試,我們需要在視圖模型中展示邏輯處理,但要盡量避免將邏輯處理直接放入其中。當然,你也可以自定義綁定適配器,這種方法更簡單。
生命周期和狀態
在實現 MVVM 架構的時候要考慮的另外一件事情是,在應用中如何處理生命周期和狀態。首先,我建議你為視圖模型創建一個基類用於處理這類問題。

public abstract class BaseViewModel<V extends MvvmView> extends BaseObservable {
   private V view;

   @CallSuper public void attachView(V view, Bundle sis) {
       this.view = view;
       if(sis != null) { onRestoreInstanceState(sis); }
   }

   @CallSuper public void detachView() {
       this.view = null;
   }

   protected void onRestoreInstanceState(Bundle sis) { }
   protected void onSaveInstanceState(Bundle outState) { }

   protected final V view() { return view; }
}

Activity 和 Fragment 中都有生命周期回調。現在它們都放在視圖模型中來處理。因此,我們需要傳遞生命周期回調。我建議使用兩個回調,它們能滿足大多數需要:標誌著視圖被創建出來的 attachView() 和標誌著視圖被銷毀的 detachView()。在 attachView() 中,傳入視圖接口,用於在必要時向視圖發送命令。attachView() 通常在 Fragment 的 onCreate() 或 onCreateView() 中調用,detachView() 則是在 onDestory() 和 onDestoryView() 中調用。

現在 Activity 和 Fragment 也提供回調,用於在系統銷毀組件或配置發生變化時保存狀態。我們把狀態保存在視圖模型中,還需要將這些回調傳遞給視圖模型。我建議把 savedInstanceState 直接傳遞至 attachView(),以便在這裏自動恢復狀態。另一個 onSaveInstanceState() 方法需要用於保存狀態,這個方法必須在 Activity 和 Fragment 的相關回調中調用。如果有 UI 狀態,可為每個視圖模型創建單獨的狀態類,當這個類實現 Parcelable 時,保存和恢復狀態都很容易,因為你只需要保存或恢復一個對象。
視圖

public abstract class BaseActivity<B extends ViewDataBinding, V extends MvvmViewModel> 
   extends AppCompatActivity implements MvvmView {

   protected B binding;
   @Inject protected V viewModel;

   protected final void setAndBindContentView(@LayoutRes int layoutResId, @Nullable Bundle sis) {
       binding = DataBindingUtil.setContentView(this, layoutResId);
       binding.setVariable(BR.vm, viewModel);
       viewModel.attachView((MvvmView) this, sis);
   }

   @Override @CallSuper protected void onSaveInstanceState(Bundle outState) {
       super.onSaveInstanceState(outState);
       if(viewModel != null) { viewModel.onSaveInstanceState(outState); }
   }

   @Override @CallSuper protected void onDestroy() {
       super.onDestroy();
       if(viewModel != null) { viewModel.detachView(); }
       binding = null;
       viewModel = null;
   }
}

現在,讓我們討論下視圖的細節。上面例子是創建 activity 基類。View 模型可通過註入用於基類,以便初始化架構配置。然後你只需要在 activity 的 onCreate() 或 fragment 的 onCreateView() 中調用這個方法即可。

上面代碼使用了 setAndBindContentView() 方法處理,和通常的 setContentView() 調用不同,它可以在 onCreate() 中調用。此方法能設置內容視圖並創建綁定,在綁定上設置視圖模型變量,並將視圖附加到視圖模型上,同時還提供保存的示例狀態。

如你所見,onSaveInstanceState() 和 detachView() 回調也可以在基類中實現。 onSaveInstanceState() 將回調轉發到視圖模型中,onDestroy() 則在視圖模型上調用 detachView() 接口。

通過這樣設置基類後,你就可以使用 MVVM 架構編寫 APP 了。
其他考慮項
了解 MVVM 架構 Android 應用的基礎後,還需對應用程序架構做進一步完善。

依賴註入
使用依賴註入可以非常容易地將組件註入到視圖模型中,並將組件很好的聯合在一起,如使用 Dagger 2 依賴註入框架。

依賴註入可以進一步解耦代碼,讓代碼更簡單也更容易測試。同時,也大大增強了代碼的可維護性。更重要的是,依賴接口能真正實現解耦。

業務邏輯
註意:視圖模型只包含呈現邏輯,所以不要把業務邏輯放在視圖模型中。創建模型類的存儲接口並選擇的存儲方式將其實現:

public interface ModelRepo {
   Single<List<Model>> findAll();
   Single<Model> findById(int id);

   void save(Model model);
   void delete(Model model);
}

對於網絡,則使用 Retrofit 創建網絡相關的代碼來實現定義的接口。

public interface ModelRepo {
   @GET("model")
   Single<List<Model>> findAll();

   @GET("model/{id}")
   Single<Model> findById(@Path("id") int id);

   @PUT("model")
   Completable create(@Body Model model);
}

對於像查找、創建這樣的基本操作,可以將存儲庫註入到視圖模型中以獲取和操作數據。對於其它更復雜的情況,比如校驗,則需要創建獨立的組件來實現這些行為,並將其註入到視圖模型中。
導航
Android 中另一個重要內容是導航,因為你需要視圖提供組件,它可能是啟動 Activity 的 Context,也可能是替換 Fragment 的 FragmentManager。同時,使用視圖接口來調用導航命令只會讓架構變得更復雜。

因此,我們需要一個獨立的組件來處理應用中的導航。Navigator 接口定義了一些公共方法用於啟動 Activity,處理 Fragment 並將它們註入視圖模型中。你可以直接在視圖模型中進行導航,而不需要 Context 或者 FragmentManager,因為這些都是由導航器的實現來處理的。

public interface Navigator {
   String EXTRA_ARGS = "_args";

   void finishActivity();
   void startActivity(Intent intent);
   void startActivity(String action);
   void startActivity(String action, Uri uri);
   void startActivity(Class<? extends Activity> activityClass);
   void startActivity(Class<? extends Activity> activityClass, Bundle args);

   void replaceFragment(int containerId, Fragment fragment, Bundle args);
   void replaceFragmentAndAddToBackStack(int containerId, @NonNull Fragment fragment, 
                                         Bundle args, String backstackTag);

   ...
}

視圖持有者可以在視圖模型中使用導航器進行導航,十分方便。比如,點擊回收視圖的某張卡片可以啟動新的 Activity。

單元測試
最後,我們了解一下視圖模型和單元測試。正如前面提到的,MVVM 架構能簡化測試呈現邏輯。我更一般使用 Mockito,它讓我可以模擬視圖接口和其它註入視圖模型和組件。當然,你也可以使用 PowerMock 來進行要求更高的測試,它使用字節碼控制,可以模擬靜態方法。

public class MyViewModelUnitTest {
   @Mock ModelRepo modelRepo;
   @Mock Navigator navigator;
   @Mock MvvmView myView;
   MyViewModel myViewModel;

   @Before public void setup() {
       MockitoAnnotations.initMocks(this);
       myViewModel = new MyViewModel(modelRepo, navigator);
       myViewModel.attachView(myView, null);
   }

   @Test public void buttonClick_submitsForm() {
       final Model model = new Model();
       doReturn(model).when(modelRepo).create();

       myViewModel.onButtonClick(null);

       verify(modelRepo).save(model);
       verify(navigator).finishActivity();
   }
}

在 setup() 方法中初始化 mock,創建視圖模型,同時註入 mock 對象並將視圖接口附加到視圖模型。寫測試用例的時候,若有必要,先通過 Mockito 的 doReturn().when() 語法指定 mock 對象的行為。 然後在視圖模型中調用測試方法。最後使用斷言和 verify() 方法檢查返回值是否正確,檢查 mock 的方法是否按預期進行調用。

總結

? 關於按照 ModelViewViewModel 模式使用數據綁定庫組織 app 架構,總結如下:
? 視圖模型是視圖和模型之間的中間介。
? 視圖通過數據綁定自動更新視圖模型的屬性。
? 視圖事件可調用視圖模型中的命令。
? 視圖模型也可在視圖上調用命令。
? 在 Android 中,視圖模型可以處理基本的生命周期回調和狀態保存及恢復。
? 依賴註入有助於測試和獲得更整潔的代碼。
? 不要在視圖模型中放置業務邏輯,它們只包含展示邏輯。另外,要使用存儲庫進行數據訪問。
? 在 Android App 中導航請使用導航器組件。

MVVM 架構與數據綁定庫