简介
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.jianshu.com/p/ba4982be30f8
https://news.realm.io/cn/news/data-binding-android-boyar-mount/
网友评论