MVVM + data-binding 快速入门

前言

  1. 简书上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入门

  1. 具体就是用了个gank.io的接口展示了一组妹子图片。不多说,看结果:


    ezgif.com-gif-maker.gif
  2. 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来加载图片。

  1. 数据请求封装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;
    }
}
  1. 使用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路径上的变量。

  1. 回调获取返回结果:
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.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容