Model-View-Presenter(MVP),即模型-視圖-表示層,架構被普遍應用於 Android 應用程序,經過引入表示層將視圖與表示邏輯和模型分離。Model-View-ViewModel(MVVM),即模型-視圖-視圖模型,與 MVP 很是類似,視圖模型充當加強的表示層,使用數據綁定器保持視圖模型和視圖同步。經過將視圖綁定到視圖模型屬性上,數據綁定程序能夠處理視圖更新而無需手動更改數據來設置視圖(例如,不用再設置控件 TextView 的setTest() 或者 setVisibility() 屬性)。與 MVP 中的表示層同樣,視圖模型能夠很容易地進行單元測試。本文介紹了數據綁定庫和 MVVM 架構模式,以及它們在 Android 上協同工做方式。 數據綁定 什麼是數據綁定?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 注入庫也沒有使狀況改善。而數據綁定庫就能很好地解決這個問題。bash
在編譯時建立一個綁定類,它爲全部視圖提供一個 ID 字段,所以再也不須要調用 findViewById() 方法。實際上,這種方式比調用 findViewById() 方法快數倍,由於數據綁定庫建立代碼僅須要遍歷視圖結構一次。網絡
綁定類中也實現了視圖文件的綁定邏輯,所以全部 setter 會在綁定類中被調用,你無須爲之操心。總之,它能讓你的代碼變得更簡潔。架構
如何設置數據綁定?app
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>
複製代碼
接下來,在 標籤中包裝下佈局中的頂層元素,以便爲此佈局建立綁定類。綁定類具備和佈局 xml 文件相同的名稱,只是在結尾添加 Binding,例如, Activity_main.xml 的綁定類名字是 ActivityMainBinding。 如上所示,命名空間的聲明也移到佈局標記中。而後,在佈局標記內聲明將須要綁定的數據做爲變量,並設置好名稱和類型。示例中,惟一的變量是視圖模型,但後續變量會增長。你能夠選擇導入類,以便能使用 View.VISIBLE 或靜態方法等常量。 如何綁定數據?ide
<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 繼承 —— 這樣數據變化時會自動反映到佈局上。
視圖是用戶界面,即佈局。在 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 中導航請使用導航器組件。