簡介
MVVM:MVP的升級版,ViewModel(vm)替換Presenter(p), ViewModel配合xml實現view和model的綁定
DataBinding:Google提出的數據綁定框架,可以輕松實現mvvm
MVVM的目的
實現應用之間數據與視圖的分離、視圖與業務邏輯的分離、數據與業務邏輯的分離,從而達到低耦合、可重用性、易測試性等好處。相對于mvp而言解耦更徹底,更易于進行單元測試。
使用
配置
app的build文件加上:
android {
...
dataBinding{
enabled =true;
}
...
}
數據綁定
ViewModel:
ViewModel需繼承BaseObservable
,實現的ViewModel為自己定義的統一的接口
ViewModel中可以更新view的狀態以及顯示內容,可以綁定點擊事件,顯示圖片等一系列與數據相關的ui操作
注意點:
1.顯示圖片需要使用自定義屬性@BindingAdapter
,方法必須以static修飾,@BindingAdapter({"imageUrl"})
中imageUrl
作為自定義屬性在xml中使用
2.notifyPropertyChanged
可以刷新具體某一屬性,此方法必須配合@Bindable
使用,加上這個注解后,DataBinding框架會在BR
這個生成類中,為特定屬性生成一個唯一的標識符。@Bindable
最好注解在getter方法上而非注解在屬性上
3.ObservableInt
此類的ObservableField
數據類型不需要注解即可綁定view,同樣的String對應的為ObservableField<String>
,但為確保性能此種數據類型盡量少用
/**
* View model for each item in the repositories RecyclerView
*/
public class ItemRepoViewModel extends BaseObservable implements ViewModel {
private Repository repository;
private Context context;
public String firstName;
public ObservableInt tvKindVisibility; //ObservableInt 不需要注解(get方法)即可綁定view的數據類型
public String imageUrl="";
public ItemRepoViewModel(Context context, Repository repository) {
this.repository = repository;
this.context = context;
}
public String getName() {
return repository.name;
}
public String getDescription() {
return repository.description;
}
public String getStars() {
return context.getString(R.string.text_stars, repository.stars);
}
public String getWatchers() {
return context.getString(R.string.text_watchers, repository.watchers);
}
public String getForks() {
return context.getString(R.string.text_forks, repository.forks);
}
@Bindable
public String getFirstName() {
return context.getString(R.string.text_forks, repository.forks);
}
/**
* 點擊事件
* @param view
*/
public void onItemClick(View view) {
context.startActivity(RepositoryActivity.newIntent(context, repository));
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
/**
* 使用ImageLoader顯示圖片 方法必須為static修飾
* @param imageView
* @param url
*/
@BindingAdapter({"imageUrl"})
public static void imageLoader(ImageView imageView, String url) {
Glide.with(imageView.getContext()).load(url)
.signature(GloableData.getSignatureString())
.into(imageView);
}
// Allows recycling ItemRepoViewModels within the recyclerview adapter
public void setRepository(Repository repository) {
this.repository = repository;
notifyChange(); //主動刷新所有數據 更新ui
notifyPropertyChanged(BR.firstName); //主動刷新單個數據 更新ui 此屬性需要@Bindable
}
@Override
public void destroy() {
//In this case destroy doesn't need to do anything because there is not async calls
}
}
xml文件:
需要使用<layout></layout>
作為根節點,在<layout>
節點中我們可以通過<data>
節點來引入我們要使用的數據源,可以使用諸如@{viewModel.onItemClick}
的方式使用<data>
引入的ViewModel,可以直接使用ViewModel中定義的屬性和方法,并且屬性的變化會自動反饋給view完成ui的更新
注意點:
1.<layout></layout>
節點下是沒有“layout_width”和“layout_height”的
2..<data>
下引用的數據包名必須寫全
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="uk.ivanc.archimvvm.viewmodel.ItemRepoViewModel" />
</data>
<android.support.v7.widget.CardView
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/vertical_margin_half"
android:layout_marginLeft="@dimen/vertical_margin"
android:layout_marginRight="@dimen/vertical_margin"
android:layout_marginTop="@dimen/vertical_margin_half"
card_view:cardCornerRadius="2dp">
<LinearLayout
android:id="@+id/layout_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackground"
android:onClick="@{viewModel.onItemClick}"
android:orientation="vertical">
<TextView
android:id="@+id/text_repo_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingTop="12dp"
android:text="@{viewModel.name}"
android:textSize="20sp"
tools:text="Repository Name" />
<TextView
android:id="@+id/text_repo_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="12dp"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingTop="10dp"
android:text="@{viewModel.description}"
android:textColor="@color/secondary_text"
android:textSize="14sp"
tools:text="This is where the repository description will go" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:orientation="horizontal">
<TextView
android:id="@+id/text_watchers"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="@{viewModel.watchers}"
android:textColor="@color/secondary_text"
tools:text="10 \nWatchers" />
<TextView
android:id="@+id/text_stars"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="@{viewModel.stars}"
android:textColor="@color/secondary_text"
tools:text="230 \nStars" />
<TextView
android:id="@+id/text_forks"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="@{viewModel.forks}"
android:textColor="@color/secondary_text"
tools:text="0 \nForks" />
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_centerVertical="true"
android:layout_marginLeft="4dp"
android:layout_toRightOf="@id/layout_left"
android:visibility="@{viewModel.imgvTrendVisibility}"
app:imageUrl="@{viewModel.imageUrl}" />
</LinearLayout>
</LinearLayout>
</android.support.v7.widget.CardView>
</layout>
DataBinding與ViewModel的綁定:
通過DataBindingUtil
的setContentView
進行binding初始化操作,setViewModel(此方法名與xml中data的定義相關)完成與ViewModel的綁定。binding可以替代butterknife直接獲取控件并且使用,如下binding.ptrList
,其中控件名ptrList
由xml定義的id自動生成。
activity:
//MainViewModel 類名由xml文件 R.layout.main_activity自動生成
private MainActivityBinding binding;
private MainViewModel mainViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//activity中binding的初始化方式
binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
mainViewModel = new MainViewModel(this, this);
binding.setViewModel(mainViewModel);
setSupportActionBar(binding.toolbar);
setupRecyclerView(binding.reposRecyclerView);
}
fragment和adapter:
binding.getRoot()獲取根布局,即原本的ContentView
if (binding == null) {
//fragment和adapter中binding的初始化方式
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_quote_databinding, container, false);
binding.setViewModel(viewModel);
init();
}else {
if (binding.getRoot().getParent() != null) {
((ViewGroup) binding.getRoot().getParent()).removeView(binding.getRoot());
}
}
return binding.getRoot();
//通過binding可直接獲取xml中控件 不需要findViewById
binding.ptrList.getRefreshableView().setSelector(R.color.trans);
綁定listview:
xml:
<?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="adapter"
type="android.widget.BaseAdapter" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:adapter="@{adapter}" />
</LinearLayout>
</layout>
通過binding直接設置adapter
binding.setAdapter(mAdapter);
更多使用:
空指針
自動生成的DataBinding
代碼會檢查null,避免出現NullPointerException
。
例如在表達式中@{user.phone}
如果user == null
那么會為user.phone
設置默認值null而不會導致程序崩潰(基本類型將賦予默認值如int為0,引用類型都會賦值null)
自定義DataBinding名
<data class="MainBinding">
....
</data>
class對應的就是生成的Data Binding名
導包
布局文件中支持import的使用,原來的代碼是這樣
<data>
<variable name="user" type="com.example.gavin.databindingtest.User" />
</data>
import后
<data>
<import type="com.example.gavin.databindingtest.User"/>
<variable
name="user"
type="User" />
</data>
遇到相同的類名的時候:
<data>
<import type="com.example.gavin.databindingtest.User" alias="User"/>
<import type="com.example.gavin.mc.User" alias="mcUser"/>
<variable name="user" type="User"/>
<variable name="mcUser" type="mcUser"/>
</data>
使用alias
設置別名,這樣user對應的就是com.example.gavin.databindingtest.User,mcUser就對應com.example.gavin.mc.User,然后
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
當需要用到一些包時,在Java中可以自動導包,不過在布局文件中就沒有這么方便了。需要使用import導入這些包,才能使用。如,需要用到View的時候
<data>
<import type="android.view.View"/>
</data>
...
<TextView
...
android:visibility="@{user.isStudent ? View.VISIBLE : View.GONE}"
/>
注意:只要是在Java中需要導入包的類,這邊都需要導入,如:Map、ArrayList等,不過java.lang
包里的類是可以不用導包的
顯示圖片
除了文字的設置,網絡圖片的顯示也是我們常用的。來看看Data Binding是怎么實現圖片的加載的。
首先要提到BindingAdapter注解,這里創建了一個類,里面有顯示圖片的方法。
public class ImageUtil {
/**
* 使用ImageLoader顯示圖片 必須是public static的
* @param imageView
* @param url
*/
@BindingAdapter({"bind:image"})
public static void imageLoader(ImageView imageView, String url) {
ImageLoader.getInstance().displayImage(url, imageView);
}
}
這里只用了bind聲明了一個image自定義屬性,等下在布局中會用到。
這個類中只有一個靜態方法imageLoader,里面有兩參數,一個是需要設置圖片的view,另一個是對應的Url,這里使用了ImageLoader庫加載圖片。
<?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="imageUrl"
type="String"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:image = "@{imageUrl}"/>
</LinearLayout>
</layout>
最后在MainActivity中綁定下數據就可以了
binding.setImageUrl(
"http://115.159.198.162:3000/posts/57355a92d9ca741017a28375/1467250338739.jpg");
表達式
三元運算
在User中添加boolean類型的isStudent屬性,用來判斷是否為學生。
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{user.isStudent? "Student": "Other"}'
android:textSize="30sp"/>
注意:需要用到雙引號的時候,外層的雙引號改成單引號
??
除了常用的操作法,另外還提供了一個 null 的合并運算符號 ??
,這是一個三目運算符的簡便寫法。
contact.lastName ?? contact.name
相當于
contact.lastName != null ? contact.lastName : contact.name
ObseravbleField
google為我們提供了一些Obserable類:ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, ObservableParcelable
public static class User {
public final ObservableField<String> firstName =
new ObservableField<>();
public final ObservableField<String> lastName =
new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
ObseravbleCollection
此種類型數據和ObseravbleField一樣不需要注解,即不要@Bindable的get和set方法
注意:此類數據在使用的過程中注意初始化,否則會經常出現空指針異常
ObservableArrayMap
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
在xml中使用:
<data>
<import type="android.databinding.ObservableMap"/>
<variable name="user" type="ObservableMap<String, Object>"/>
</data>
…
<TextView
android:text='@{user["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text='@{String.valueOf(1 + (Integer)user["age"])}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
ObservableArrayList
ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);
xml使用:
<data>
<import type="android.databinding.ObservableList"/>
<import type="com.example.my.app.Fields"/>
<variable name="user" type="ObservableList<Object>"/>
</data>
…
<TextView
android:text='@{user[Fields.LAST_NAME]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
單元測試
1.MVVM的實現過程中應盡量將需要測試的邏輯轉移到ViewModel中,進行單元測試時主要測試ViewModel
2.實現過程中某些邏輯與view結合緊密,此時需要靈活使用接口,通過回調的形式來實現。對于復雜頁面而言可能會導致接口中的方法過多,需斟酌
遇到的問題
xml中定義出錯,編輯器不會給出提示,導致binding找不到又很難定位出錯的位置,使用時需謹慎
總結
MVVM的引入對于口袋貴金屬項目而言是為了更好的進行單元測試,此外結合DataBinding的MVVM還有取代ButterKnife,ViewHolder等優勢
對于單元測試,這里需要遵循三個規范(詳細可參考我的自選模塊的實現):
1.需要測試的邏輯盡量在ViewModel中實現,盡量脫離view
2.需要測試的邏輯需要抽離出相應的方法,并且方法應遵循單一原則
3.輸入輸出需要public暴露以方便斷言(具體參考項目中已有的測試用例)
參考
demo
https://github.com/ivacf/archi
博客
http://www.lxweimin.com/p/ba4982be30f8
https://news.realm.io/cn/news/data-binding-android-boyar-mount/