前言
- 简书上data-binding 的文章不少,但真正用来实现MVVM架构的文章不多。有些是官方的guide(https://developer.android.com/topic/libraries/data-binding/index.html) 的翻译版本,且官方的guide的架构主要采用 data-binding + mvp 的形式。 本文讲述一个快速入门的data- binding + mvvm架构。
基本配置
详见官方
gradle ,当然 gradle版本需要在1.5.0-alpha1 or higher
android {
....
dataBinding {
enabled = true
}
}
快速demo入门
-
具体就是用了个gank.io的接口展示了一组妹子图片。不多说,看结果:
- gradle配置
compile fileTree(include: ['*.jar'], dir: 'libs')
compile 'com.android.support:appcompat-v7:24.1.1'
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.github.bumptech.glide:glide:3.7.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
compile 'com.android.support:cardview-v7:24.1.1'
这里本来demo不应该引入其他的库的,但为了方便写demo,故添加了retrofit来请求数据,glide来加载图片。
- 数据请求封装Model 省略getter和setter
/**
* @Description: error: false,
* results: [
* {
* _id: "57bc5238421aa9125fa3ed70",
* createdAt: "2016-08-23T21:40:08.159Z",
* desc: "8.24",
* publishedAt: "2016-08-24T11:38:48.733Z",
* source: "chrome",
* type: "福利",
* url: "http://ww3.sinaimg.cn/large/610dc034jw1f740f701gqj20u011hgo9.jpg",
* used: true,
* who: "daimajia"
* },
* ]
*/
public class MeiZiModel implements Serializable{
private String error;
private List<Result> results;
public static class Result{
private String _id;
private String desc;
private String publishedAt;
private String createdAt;
private String source;
private String url;
private String used;
private String who;
}
}
- 使用retrofit 请求数据如下:
public interface MeiZiService {
@GET("api/data/福利/{page}/{number}")
Call<MeiZiModel> getMeiZi(@Path("page") int page, @Path("number") int number);
}
简单解释下,@GET当然是指get的方式请求,后面是具体的路径。{page}/{number}这个是我随便填的参数名,测试了下gank的接口,确实可以通过修改这两个参数请求不同页面的数据。
@path 就是path路径上的变量。
- 回调获取返回结果:
public class ServiceGenerator {
public static final String API_BASE_URL = "http://gank.io";
private static Retrofit retrofit =
new Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
public static void getMeiZi(int page, int number, final MeiZiCallBack callBack) {
MeiZiService service = retrofit.create(MeiZiService.class);
Call<MeiZiModel> meiziCall = service.getMeiZi(page, number);
meiziCall.enqueue(new Callback<MeiZiModel>() {
@Override
public void onResponse(Call<MeiZiModel> call, Response<MeiZiModel> response) {
if (callBack != null) {
callBack.onSuccess(response.body().getResults());
}
}
@Override
public void onFailure(Call<MeiZiModel> call, Throwable t) {
if (callBack != null) {
callBack.onFail(t.getMessage());
}
}
});
}
}
注意整个url 为http://gank.io/ api/data/福利/{page}/{number} 后面的两个为需要传入的参数,然后把retrofit请求结果回调即可。
6 MainActivity
public class MainActivity extends AppCompatActivity {
private ListView mListView;
private MeiziAdapter mMeiziAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mListView = (ListView) findViewById(R.id.list);
mMeiziAdapter = new MeiziAdapter(this);
ServiceGenerator.getMeiZi(11, 2, new MeiZiCallBack() {
@Override
public void onSuccess(List<MeiZiModel.Result> result) {
mMeiziAdapter.setDatas(result);
mListView.setAdapter(mMeiziAdapter);
}
@Override
public void onFail(String error) {
//TODO 没处理失败的情况。
}
});
}
}
很简单,将数据塞给adpter。
ViewModel 和View
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View"/>
<variable
name="viewModel"
type="com.example.xxx.mvvmdemo.ViewModel.ItemViewModel">
</variable>
</data>
<android.support.v7.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.example.xxx.mvvmdemo.View.CustomImage
android:layout_width="wrap_content"
android:layout_height="wrap_content"
bind:load = "@{viewModel.imageUrl}"
android:onClick="@{viewModel::onItemClick}"
/>
<TextView
android:layout_width="60dp"
android:layout_height="wrap_content"
android:visibility="@{viewModel.isVisibility ? View.VISIBLE : View.GONE }"
android:text="@{viewModel.text}"
/>
</android.support.v7.widget.CardView>
</layout>
这里需要注意的几点:
(1) 先看整个头部,添加了 layout 和 data 部分。 data 部分import 导入需要使用的包,如textView的 visibility需要 View.VISIBLE 常数,存在于 android.view.View类。
(2) 引入自定义的viewModel 注意,这里变量name 为viewModel ,type具体的类。
(3) 针对简单的TextView 的text 直接使用@{viewModel.text},viewModel.text 这个默认回去访问text的getter方法getText(),但找不到时,会去找text的公共属性text。
(4) data- binding 支持三元运算符 @{viewModel.isVisibility ? View.VISIBLE : View.GONE } 还支持逻辑运算等等,详见
https://developer.android.com/topic/libraries/data-binding/index.html#expression_language
(5) 特殊情形,如这里是imageView,我想让其自动加载图片。这里是使用三方库Glide加载,怎么办呢?使用BindingMethod 。自定义 bind:load = "@{viewModel.imageUrl}"
load方法时,我们通过url参数,直接在网上下载该图片。具体见CustomImageAdapter类通过注解 @BindingAdapter("load") 来指明需要动态加载的图片url。这里还有点特殊,imageView没有使用Glide加载图片的接口,故扩展了load方法,详见CustomImage。
public class ItemViewModel {
private final ObservableBoolean isVisibility = new ObservableBoolean(false);
private final ObservableField<String> mImageUrl = new ObservableField<>();
private final ObservableField<String> text = new ObservableField<>();
public Context mContext;
public String mUrl;
public ItemViewModel(Context mContext) {
this.mContext = mContext;
}
public void setData(MeiZiModel.Result result,boolean isShowText){
if(result == null ){
return;
}
mImageUrl.set(result.getUrl());
mUrl = result.getUrl();
text.set(result.getDesc());
isVisibility.set(isShowText);
}
public ObservableBoolean getIsVisibility() {
return isVisibility;
}
public ObservableField<String> getImageUrl() {
return mImageUrl;
}
public ObservableField<String> getText() {
return text;
}
}
public class MeiziAdapter extends BaseAdapter {
private Context mContext;
private List<MeiZiModel.Result> mDatas;
public void setDatas(List<MeiZiModel.Result> mDatas) {
this.mDatas = mDatas;
}
public MeiziAdapter(Context mContext) {
this.mContext = mContext;
}
@Override
public int getCount() {
return mDatas == null ? 0 : mDatas.size();
}
@Override
public Object getItem(int i) {
return mDatas == null || mDatas.size() == 0 ? null : mDatas.get(i);
}
@Override
public long getItemId(int i) {
return i;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ItemViewModel viewModel;
if (convertView == null) {
ItemViewBinding binding = DataBindingUtil.inflate((LayoutInflater) mContext.getApplicationContext().getSystemService
(Context.LAYOUT_INFLATER_SERVICE), R.layout.item_view, null, false);
viewModel = new ItemViewModel(mContext);
binding.setViewModel(viewModel);
convertView = binding.getRoot();
convertView.setTag(viewModel);
} else {
viewModel = (ItemViewModel) convertView.getTag();
}
viewModel.setData(mDatas.get(position),position % 2 == 0);
return convertView;
}
}
这里需要注意ItemViewBinding 类 是data-binding给我们生成的,采用我们布局的xml下划线改为驼峰命名,最后加上Binding作为区分。 其实这里跟holder类似, 不过这里的优点是可以重用ViewModel的逻辑。
public class CustomImage extends ImageView {
public CustomImage(Context context) {
super(context);
}
public CustomImage(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomImage(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void load(String url){
Glide.with(this.getContext().getApplicationContext()).load(url).fitCenter().into(this);
}
}
public class CustomImageAdapter {
@BindingAdapter("load")
public static void load(CustomImage imageView,String url){
imageView.load(url);
}
}
more
整个工程结构讲完了,这里mvvm的形式,为什么比一般的mvc更好呢?
答案是ViewModel和View的弱耦合,这里就可以多次重用。比如重用xml或重用ViewModel或者整个重用。最关键的是这样减轻了Activity的工作量,可以模块化去拆分每个页面的逻辑,做到更多的复用。
ps: data binding 报错
Cannot find the setter for attribute '' with parameter type int on class.