什麼是MVVM 我們一步步來,從MVC開始。
MVC我們都知道,模型——視圖——控制器。為了使得程序的各個部分分離降低耦合性,我們對代碼的結構進行了劃分。

他們的通信方式也如上圖所示,即View層觸發操作通知到業務層完成邏輯處理,業務層完成業務邏輯之後通知Model層更新數據,數據更新完之後通知View層展現。在實際運用中人們發現View和Model之間的依賴還是太強,希望他們可以絕對獨立的存在,慢慢的就演化出了MVP。

Presenter替換掉了Controller,不僅僅處理邏輯部分。而且還控制著View的刷新,監聽Model層的數據變化。這樣隔離掉View和Model的關係後使得View層變的非常的薄,沒有任何的邏輯部分又不用主動監聽數據,被稱之為“被動視圖”。

至於MVVM基本上和MVP一模一樣,感覺只是名字替換了一下。他的關鍵技術就是今天的主題(Data Binding)。View的變化可以自動的反應在ViewModel,ViewModel的數據變化也會自動反應到View上。這樣開發者就不用處理接收事件和View更新的工作,框架已經幫你做好了。
Data Binding Library今年的Google IO大會上,Android團隊發布了一個數據綁定框架(Data Binding Library)。以後可以直接在layout佈局xml文件中綁定數據了,無需再findViewById然後手工設置數據了。其語法和使用方式和JSP中的EL表達式非常類似。
下面就來介紹怎麼使用Data Binding Library。 配置環境目前,最新版的Android Studio已經內置了該框架的支持,配置起來也很簡單,只需要編輯app目錄下的build.gradle文件,添加下面的內容就好了 - android {
- ....
- dataBinding {
- enabled = true
- }
- }
複製代碼
Data Binding Layout文件Data Binding layout文件有點不同的是:起始根標籤是layout,接下來一個data 元素以及一個view 的根元素。這個view 元素就是你沒有使用Data Binding的layout文件的根元素。舉例說明如下: - <?xml version="1.0" encoding="utf-8"?><layout xmlns:android="http://schemas.android.com/apk/res/android">
- <data>
- <variable name="user" type="com.example.User"/>
- </data>
- <LinearLayout
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
- <TextView android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@{user.firstName}"/>
- <TextView android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@{user.lastName}"/>
- </LinearLayout></layout>
複製代碼
上面定義了一個com.example.User類型的變量user,然後接 著android:text="@{user.firstName}"把變量user的firstName屬性的值和TextView的text屬性綁定起來。
Data Object我們來看下上面用到的com.example.User對象。 - public class User {
- public final String firstName;
- public final String lastName;
- public User(String firstName, String lastName) {
- this.firstName = firstName;
- this.lastName = lastName;
- }
- }
複製代碼
他有兩個public的屬性firstName,lastName,這和上面layout文件裡面的@{user.firstName}和 @ {user.lastName}對應
或者下面這種形式的對像也是支持的。 - public class User {
- private final String firstName;
- private final String lastName;
- public User(String firstName, String lastName) {
- this.firstName = firstName;
- this.lastName = lastName;
- }
- // getXXX形式
- public String getFirstName() {
- return this.firstName;
- }
- // 或者属性名和方法名相同
- public String lastName() {
- return this.lastName;
- }
- }
複製代碼
綁定數據添加完<data>標籤後,Android Studio就會根據xml的文件名自動生成一個繼承ViewDataBinding的類。例如: activity_main.xml就會生成ActivityMainBinding,然後我們在Activity裡面添加如下代碼: - @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
- User user = new User("Test", "User");
- binding.setUser(user);
- }
複製代碼
綁定事件就像你可以在xml文件裡面使用屬性android:onClick綁定Activity裡面的一個方法一樣,Data Binding Library擴展了更多的事件可以用來綁定方法,比如View.OnLongClickListener有個方法onLongClick(),你就可以使用android:onLongClick屬性來綁定一個方法,需要注意的是綁定的方法的簽名必須和該屬性原本對應的方法的簽名完全一樣,否則編譯階段會報錯。
下面舉例來說明具體怎麼使用,先看用來綁定事件的類: - public class MyHandlers {
- public void onClickButton(View view) { ... }
- public void afterFirstNameChanged(Editable s) { ... }
- }
複製代碼
然後就是layout文件: - <?xml version="1.0" encoding="utf-8"?><layout xmlns:android="http://schemas.android.com/apk/res/android">
- <data>
- <variable name="handlers" type="com.example.Handlers"/>
- <variable name="user" type="com.example.User"/>
- </data>
- <LinearLayout
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
- <EditText android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@{user.firstName}"
- android:afterTextChanged="@{handlers.afterFirstNameChanged}"/>
- <Button android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:onClick="@{handlers.onClickButton}"/>
- </LinearLayout></layout>
複製代碼
表達式語言(Expression Language)你可以直接在layout文件裡面使用常見的表達式:
更新界面有些時候,代碼會修改我們綁定的對象的某些屬性,那麼怎麼通知界面刷新呢?下面就給出兩種方案。
方案一讓你的綁定數據類繼承BaseObservable,然後通過調用notifyPropertyChanged方法來通知界面屬性改變,如下:
- private static class User extends BaseObservable {
- private String firstName;
- private String lastName;
- @Bindable
- public String getFirstName() {
- return this.firstName;
- }
- @Bindable
- public String getLastName() {
- return this.lastName;
- }
- public void setFirstName(String firstName){
- this.firstName = firstName;
- notifyPropertyChanged(BR.firstName);
- }
- public void setLastName(String lastName) { this.lastName = lastName;
- notifyPropertyChanged(BR.lastName);
- }
- }
複製代碼
在需要通知的屬性的get方法上加上@Bindable,這樣編譯階段會生成BR.[property name],然後使用這個調用方法notifyPropertyChanged就可以通知界面刷新了。如果你的數據綁定類不能繼承BaseObservable,那你就只能自己實現Observable接口,可以參考BaseObservable的實現。
方案二Data Binding Library提供了很便利的類ObservableField,還有ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, 和ObservableParcelable,基本上涵蓋了各種我們需要的類型。用法很簡單,如下:
- private static class User {
- public final ObservableField<String> firstName = new ObservableField<>();
- public final ObservableField<String> lastName = new ObservableField<>();
- public final ObservableInt age = new ObservableInt();
- }
複製代碼
然後使用下面的代碼來訪問:
- user.firstName.set("Google");int age = user.age.get();
複製代碼
調用set方法時,Data Binding Library就會自動的幫我們通知界面刷新了。
綁定AdapterView在一個實際的項目中,相信AdapterView是使用得很多的,使用官方提供給的API來進行AdapterView的綁定需要寫很多代碼,使用起來不方便,但是由於Data Binding Library提供豐富的擴展功能,所以出現了很多第三方的庫來擴展它,下面就來介紹一個比較好用的庫binding-collection-adapter,Github地址:https://github.com/evant/binding-collection-adapter
使用的時候在你的build.gradle文件裡面添加
- compile 'me.tatarka:bindingcollectionadapter:0.16'
複製代碼
如果你要是用RecyclerView,還需要添加
- compile 'me.tatarka:bindingcollectionadapter-recyclerview:0.16'
複製代碼
下面就是ViewModel的寫法: - public class ViewModel {
- public final ObservableList<String> items = new ObservableArrayList<>();
- public final ItemView itemView = ItemView.of(BR.item, R.layout.item);
- }
複製代碼
這裡用到了ObservableList,他會在items變化的時候自動刷新界面
然後下面是layout文件:
- <!-- layout.xml --><layout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
- <data>
- <variable name="viewModel" type="com.example.ViewModel"/>
- <import type="me.tatarka.bindingcollectionadapter.LayoutManagers" />
- </data>
- <ListView
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- app:items="@{viewModel.items}"
- app:itemView="@{viewModel.itemView}"/>
- <android.support.v7.widget.RecyclerView
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- app:layoutManager="@{LayoutManagers.linear()}"
- app:items="@{viewModel.items}"
- app:itemView="@{viewModel.itemView}"/>
- <android.support.v4.view.ViewPager
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- app:items="@{viewModel.items}"
- app:itemView="@{viewModel.itemView}"/>
- <Spinner
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- app:items="@{viewModel.items}"
- app:itemView="@{viewModel.itemView}"
- app:dropDownItemView="@{viewModel.dropDownItemView}"/></layout>
複製代碼
然後是item layout:
- <!-- item.xml --><layout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
- <data>
- <variable name="item" type="String"/>
- </data>
- <TextView
- android:id="@+id/text"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@{item}"/></layout>
複製代碼
如果有多種樣式的佈局,那麼就需要把ItemView換成ItemViewSelector,如下: - public final ItemViewSelector<String> itemView = new BaseItemViewSelector<String>() {
-
- @Override
- public void select(ItemView itemView, int position, String item) {
- itemView.set(BR.item, position == 0 ? R.layout.item_header : R.layout.item);
- }
- // This is only needed if you are using a BindingListViewAdapter
- @Override
- public int viewTypeCount() {
- return 2;
- }
- };
複製代碼
自定義綁定正常情況下,Data Binding Library會根據屬性名去找對應的set方法,但是我們有時候需要自定義一些屬性,Data Binding Library也提供了很便利的方法讓我們來實現。
比如我們想在layout文件裡面設置ListView的emptyView,以前這個是無法做到的,只能在代碼裡面通過調用setEmptyView來做;
但是現在藉助Data Binding Library,我們可以很容易的實現這個功能了。先看layout文件:
- <?xml version="1.0" encoding="utf-8"?><layout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
- <data>
- <variable
- name="viewModel"
- type="com.example.databinding.viewmodel.ViewAlbumsViewModel"/>
- </data>
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:paddingLeft="10dp"
- android:paddingRight="10dp"
- android:orientation="vertical">
- <ListView
- android:layout_width="fill_parent"
- android:layout_height="0px"
- android:layout_weight="1.0"
- app:items="@{viewModel.albums}"
- app:itemView="@{viewModel.itemView}"
- app:emptyView="@{@id/empty_view}"
- android:onItemClick="@{viewModel.viewAlbum}"
- android:id="@+id/albumListView"/>
- <TextView
- android:id="@+id/empty_view"
- android:layout_width="fill_parent"
- android:layout_height="0px"
- android:layout_weight="1.0"
- android:gravity="center"
- android:text="@string/albums_list_empty" />
- <Button
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:text="@string/create"
- android:onClick="@{viewModel.createAlbum}"/>
- </LinearLayout></layout>
複製代碼
這個代碼就用來指定emptyView,下面來看下實現的代碼:
- @BindingAdapter("emptyView")
- public static <T> void setEmptyView(AdapterView adapterView, int viewId) {
- View rootView = adapterView.getRootView();
- View emptyView = rootView.findViewById(viewId);
- if (emptyView != null) {
- adapterView.setEmptyView(emptyView);
- }
- }
複製代碼
下面我們來分析上面的代碼,@{@id/empty_view}表示引用了@id/empty_view這個id,所以它的值就是int, 再看上面的setEmptyView方法,第一個參數AdapterView adapterView表示使用emptyView這個屬性的控件, 而第二個參數int viewId則是emptyView屬性傳進來的值,上面的layout可以看出來它就是R.id.empty_view, 然後通過id找到控件,然後調用原始的setEmptyView來設置。
文章來源 |