美文网首页
玩转Android之MVVM开发模式实战,炫酷的DataBind

玩转Android之MVVM开发模式实战,炫酷的DataBind

作者: 天天大保建 | 来源:发表于2017-10-19 14:04 被阅读0次

    C# 很早就有了MVVM的开发模式,Android手机中的MVVM一直到去年Google的I\O大会上才推出,姗姗来迟。MVVM这中开发模式的优点自不必多说,可以实现视图和逻辑代码的解耦,而且,按照Google的说法,使用了MVVM的开发模式,还可以提高布局文件的解析速度,个人觉得这一点非常重要。我们在安卓开发中经常需要写很多个findViewById,让人心烦,很多人不想写这个于是用了一些注解框架,可是注解框架无论性能多好,效率总是要低于findViewById的,因此,Android中的MVVM也即databinding可以帮助我们彻底解决这个问题。OK,废话不多说,我们来看看具体要怎么在Android开发中使用MVVM。

    在低版本的AndroidStudio中使用DataBinding稍微有点麻烦,这里不做介绍。我这里以AndroidStuido2.1为例来介绍DataBinding。本文主要包含以下几方面内容:

    1.基本使用

    2.绑定ImageView

    3.绑定ListView

    4.点击事件处理

    5.数据更新处理

    好了,那就开始吧!

    1.基本使用

    创建好一个Android Project之后,在gradle文件中添加如下几行代码,表示开启databinding:

    [java]view plaincopy

    print?

    android {

    ...

    ...

    ...

    dataBinding{

    enabled true

           }

    }

    就是这么简单,一个简单的databinding配置之后,就可以开始使用数据绑定了。

    要使用数据绑定,我们得首先创建一个实体类,比如User实体类,如下:

    [java]view plaincopy

    print?

    public class UserEntity {

    private String username;

    private String nickname;

    private int age;

    public User Entity() {

    }

    public int getAge() {

    return age;

    }

    public void setAge(intage) {

    this.age = age;

    }

    public String getNickname() {

    return nickname;

    }

    public void setNickname(String nickname) {

    this.nickname = nickname;

    }

    public String getUsername() {

    return username;

    }

    public void setUsername(String username) {

    this.username = username;

    }

    public UserEntity(intage, String nickname, String username) {

    this.age = age;

    this.nickname = nickname;

    this.username = username;

    }

    }

    然后我们来看看布局文件该怎么写,首先布局文件不再是以传统的某一个容器作为根节点,而是使用作为根节点,在节点中我们可以通过节点来引入我们要使用的数据源,如下:

    [java]view plaincopy

    print?

    xmlns:android="http://schemas.android.com/apk/res/android"

    >

     name="user"

     type="org.lenve.databinding1.UserEntity"/>

    xmlns:tools="http://schemas.android.com/tools"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:orientation="vertical"

    tools:context="org.lenve.databinding1.MainActivity">

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:text="@{user.username}"/>

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:text="@{user.nickname}"/>

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:text="@{String.valueOf(user.age)}"/>

    在data中定义的variable节点,name属性表示变量的名称,type表示这个变量的类型,实例就是我们实体类的位置,当然,这里你也可以换一种写法,如下:

    [java]view plaincopy

    print?

    name="user"

    type="UserEntity"/>

    先使用import节点将UserEntity导入,然后直接使用即可。但是如果这样的话又会有另外一个问题,假如我有两个类都是UserEntity,这两个UserEntity分属于不同的包中,又该如何?看下面:

    [java]view plaincopy

    print?

    name="user"

    type="Lenve"/>

    在import节点中还有一个属性叫做alias,这个属性表示我可以给该类取一个别名,我给UserEntity这个实体类取一个别名叫做Lenve,这样我就可以在variable节点中直接写Lenve了。

    看完data节点我们再来看看布局文件,TextView的text属性被我直接设置为了@{user.username},这样,该TextView一会直接将UserEntity实体类的username属性的值显示出来,对于显示age的TextView,我用了String.valueOf来显示,因为大家知道TextView并不能直接显示int型数据,所以需要一个简单的转换,事实上,我们还可以在{}里边进行一些简单的运算,这些我一会再说。

    最后,我们来看看Activity中该怎么写,setContentView方法不能够再像以前那样来写了,换成下面的方式:

    [java]view plaincopy

    print?

    DataBindingUtil.setContentView(this, R.layout.activity_main)

    该方法有一个返回值,这个返回值就是系统根据我们的activity_main.xml布局生成的一个ViewModel类,所以完整写法如下:

    [java]view plaincopy

    print?

    ActivityMainBinding activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);

    有了ViewModel,再把数据绑定上去就可以了,如下:

    [java]view plaincopy

    print?

    @Override

    protectedvoidonCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    ActivityMainBinding activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);

    UserEntity user =newUserEntity();

    user.setAge(34);

    user.setUsername("zhangsan");

    user.setNickname("张三");

    activityMainBinding.setUser(user);

    }

    运行,显示效果如下:

    OK,那我们刚才还说到可以在@{}进行简单的计算,都有哪些计算呢?我们来看看:

    1.基本的三目运算

    [java]view plaincopy

    print?

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:text="@{user.username??user.nickname}"/>

    两个??表示如果username属性为null则显示nickname属性,否则显示username属性。

    2.字符拼接

    [java]view plaincopy

    print?

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:text="@{`username is :`+user.username}"/>

    大家注意,这里的字符拼接不是用单引号哦,用的是ESC按键下面那个按键按出来的。目前DataBinding中的字符拼接还不支持中文。

    3.根据数据来决定显示样式

    [java]view plaincopy

    print?

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:background="@{user.age < 30 ? 0xFF0000FF:0xFFFF0000}"

    android:text="@{String.valueOf(user.age)}"/>

    我在这里给TextView设置背景的时候,做了一个简单的判断,如果用户的年龄小于30,背景就显示为蓝色,否则背景就显示为红色,DataBinding里支持小于号但是不支持大于号,索性,大于小于号我都用转义字符来表示。

    另外,DataBinding对于基本的四则运算、逻辑与、逻辑或、取反位移等都是支持的,我这里不再举例。

    2.绑定ImageView

    OK,上文只是一个简单的绑定文本,下面我们来看看怎么样绑定图片,这里我们还得介绍DataBinding的另一项新功能,就是关于DataBinding自定义属性的问题,事实上,在我们使用DataBinding的时候,可以给一个控件自定义一个属性,比如我们下面即将说的这个绑定ImageView的案例。假设我现在想要通过Picasso显示一张网络图片,正常情况下这个显示很简单,可是如果我要通过DataBinding来实现,该怎么做呢?我们可以使用

    [java]view plaincopy

    print?

    @BindingAdapter

    注解来创建一个自定义属性,同时还要有一个配套的注解的方法。当我们在布局文件中使用这个自定义属性的时候,会触发这个被我们注解的方法,这样说大家可能还有一点模糊,我们来看看新的实体类:

    [java]view plaincopy

    print?

    /**

    * Created by 王松 on 2016/7/31.

    */

    publicclassUser {

    privateString username;

    privateString userface;

    publicUser() {

    }

    publicUser(String userface, String username) {

    this.userface = userface;

    this.username = username;

    }

    @BindingAdapter("bind:userface")

    publicstaticvoidgetInternetImage(ImageView iv, String userface) {

    Picasso.with(iv.getContext()).load(userface).into(iv);

    }

    publicString getUserface() {

    returnuserface;

    }

    publicvoidsetUserface(String userface) {

    this.userface = userface;

    }

    publicString getUsername() {

    returnusername;

    }

    publicvoidsetUsername(String username) {

    this.username = username;

    }

    }

    新类里边只有两个属性,分别是用户名和用户图像,用户图像中存储的实际上是一个网络图片地址,这里除了基本的get/set方法之外还多了一个叫做getInternetImage的网络方法,这个方法有一个注解@BindAdapter("bind:userface"),该注解表示当用户在ImageView中使用自定义属性userface的时候,会触发这个方法,我在这个方法中来为这个ImageView加载一张图片,这里有一点需要注意,就是该方法必须为静态方法。OK,我们再来看看这次的布局文件:

    [java]view plaincopy

    print?

    xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:app="http://schemas.android.com/apk/res-auto"

    >

    name="user"

    type="org.lenve.databinding2.User"/>

    xmlns:tools="http://schemas.android.com/tools"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:orientation="vertical"

    tools:context="org.lenve.databinding2.MainActivity">

    android:id="@+id/iv"

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    app:userface="@{user.userface}">

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:text="@{user.username}"/>

    大家注意我在ImageView控件中使用userface属性的时候,使用的前缀不是android而是app哦。再来看看Activity中的代码:

    [java]view plaincopy

    print?

    @Override

    protectedvoidonCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    ActivityMainBinding dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);

    dataBinding.setUser(newUser("http://img2.cache.netease.com/auto/2016/7/28/201607282215432cd8a.jpg","张三"));

    }

    就是这么简单,加上网络权限就可以运行了,运行效果如下:

    3.绑定ListView

    好了,看完了简单使用之后,不知道你有没有喜欢上DataBinding,如果还没有,那就再来看看使用DataBinding来给ListView绑定数据吧,这个你一定会喜欢上的。因为使用这中方式来绑定太简单了。

    先来看看我们要做的效果吧:

    就是一个ListView,左边显示图片,右边显示文本,这样一个效果。OK,那就一步一步来吧,先是主布局:

    [java]view plaincopy

    print?

    xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:tools="http://schemas.android.com/tools"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    tools:context="org.lenve.databinding3.MainActivity">

    android:id="@+id/lv"

    android:layout_width="match_parent"

    android:layout_height="match_parent">

    主布局很简单,就是一个ListView,再来看看ListView的item布局:

    [java]view plaincopy

    print?

    xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:app="http://schemas.android.com/apk/res-auto"

    >

    name="food"

    type="org.lenve.databinding3.Food"/>

    android:layout_width="match_parent"

    android:layout_height="96dp"

    android:orientation="vertical">

    android:id="@+id/iv"

    android:layout_width="96dp"

    android:layout_height="96dp"

    android:padding="6dp"

    app:img="@{food.img}"/>

    android:id="@+id/description"

    android:layout_width="match_parent"

    android:layout_height="wrap_content"

    android:layout_marginLeft="8dp"

    android:layout_toRightOf="@id/iv"

    android:ellipsize="end"

    android:maxLines="3"

    android:text="@{food.description}"/>

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:layout_marginLeft="8dp"

    android:layout_toRightOf="@id/iv"

    android:layout_alignParentBottom="true"

    android:layout_marginBottom="2dp"

    android:text="@{food.keywords}"

    android:textStyle="bold"/>

    图片加载、文本加载前两节都已经说过了,这里的东西就没有什么难度了,我们再来看看实体类Food:

    [java]view plaincopy

    print?

    /**

    * Created by 王松 on 2016/7/31.

    */

    publicclassFood {

    privateString description;

    privateString img;

    privateString keywords;

    privateString summary;

    publicFood() {

    }

    publicFood(String description, String img, String keywords, String summary) {

    this.description = description;

    this.img = img;

    this.keywords = keywords;

    this.summary = summary;

    }

    @BindingAdapter("bind:img")

    publicstaticvoidloadInternetImage(ImageView iv, String img) {

    Picasso.with(iv.getContext()).load(img).into(iv);

    }

    publicString getDescription() {

    returndescription;

    }

    publicvoidsetDescription(String description) {

    this.description = description;

    }

    publicString getImg() {

    returnimg;

    }

    publicvoidsetImg(String img) {

    this.img = img;

    }

    publicString getKeywords() {

    returnkeywords;

    }

    publicvoidsetKeywords(String keywords) {

    this.keywords = keywords;

    }

    publicString getSummary() {

    returnsummary;

    }

    publicvoidsetSummary(String summary) {

    this.summary = summary;

    }

    }

    这个实体类中有一个加载图片的方法,加载方式我们上文都已经介绍过了,不多说。好了,再来看看我们的终极Adapter类:

    [java]view plaincopy

    print?

    /**

    * Created by 王松 on 2016/7/31.

    */

    publicclassMyBaseAdapterextendsBaseAdapter {

    privateContext context;

    privateLayoutInflater inflater;

    privateintlayoutId;

    privateintvariableId;

    privateList list;

    publicMyBaseAdapter(Context context,intlayoutId, List list,intresId) {

    this.context = context;

    this.layoutId = layoutId;

    this.list = list;

    this.variableId = resId;

    inflater = LayoutInflater.from(context);

    }

    @Override

    publicintgetCount() {

    returnlist.size();

    }

    @Override

    publicObject getItem(intposition) {

    returnlist.get(position);

    }

    @Override

    publiclonggetItemId(intposition) {

    returnposition;

    }

    @Override

    publicView getView(intposition, View convertView, ViewGroup parent) {

    ViewDataBinding dataBinding;

    if(convertView ==null) {

    dataBinding = DataBindingUtil.inflate(inflater, layoutId, parent,false);

    }else{

    dataBinding = DataBindingUtil.getBinding(convertView);

    }

    dataBinding.setVariable(variableId, list.get(position));

    returndataBinding.getRoot();

    }

    }

    这个大概算是Adapter的终极写法了,如果你按这种方式来写Adapter,那么如果没有非常奇葩的需求,你这个App中可能就只有这一个给ListView使用的Adapter了,为什么这么说呢?因为这个Adapter中没有一个变量和我们的ListView沾边,解释一下几个变量吧:layoutId这个表示item布局的资源id,variableId是系统自动生成的,根据我们的实体类,直接从外部传入即可。另外注意布局加载方式为DataBindingUtil类中的inflate方法。OK,最后再来看看Activity:

    [java]view plaincopy

    print?

    publicclassMainActivityextendsAppCompatActivity {

    privateHandler mHandler =newHandler(){

    @Override

    publicvoidhandleMessage(Message msg) {

    MyBaseAdapter adapter =newMyBaseAdapter<>(MainActivity.this, R.layout.listview_item, foods, org.lenve.databinding3.BR.food);

    lv.setAdapter(adapter);

    }

    };

    privateList foods;

    privateListView lv;

    @Override

    protectedvoidonCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);

    lv = ((ListView) findViewById(R.id.lv));

    initData();

    }

    privatevoidinitData() {

    OkHttpClient client =newOkHttpClient.Builder().build();

    Request request =newRequest.Builder().url("http://www.tngou.net/api/food/list?id=1").build();

    client.newCall(request).enqueue(newCallback() {

    @Override

    publicvoidonFailure(Call call, IOException e) {

    }

    @Override

    publicvoidonResponse(Call call, Response response)throwsIOException {

    if(response.isSuccessful()) {

    parseJson(response.body().string());

    }

    }

    });

    }

    privatevoidparseJson(String jsonStr) {

    foods =newArrayList<>();

    try{

    JSONObject jo =newJSONObject(jsonStr);

    JSONArray tngou = jo.getJSONArray("tngou");

    for(inti =0; i < tngou.length(); i++) {

    JSONObject item = tngou.getJSONObject(i);

    String description = item.getString("description");

    String img ="http://tnfs.tngou.net/image"+item.getString("img");

    String keywords ="【关键词】 "+item.getString("keywords");

    String summary = item.getString("summary");

    foods.add(newFood(description, img, keywords, summary));

    }

    mHandler.sendEmptyMessage(0);

    }catch(JSONException e) {

    e.printStackTrace();

    }

    }

    }

    OkHttp下载数据和Json解析自不用多说,在构造MyAdapter的时候传入的最后一个参数,是BR中的,这个BR和我们项目中的R文件类似,都是系统自动生成的。

    至此,我们使用DataBinding的方式来给ListView加载数据就算完成了。so easy~~~

    4.点击事件处理

    如果你使用DataBinding,我们的点击事件也会有新的处理方式,首先以ListView为例来说说如何绑定点击事件,在listview_item布局文件中每一个item的根节点添加如下代码:

    [java]view plaincopy

    print?

    xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:app="http://schemas.android.com/apk/res-auto"

    >

    ....

    ....

    android:layout_width="match_parent"

    android:layout_height="96dp"

    android:onClick="@{food.onItemClick}"

    android:orientation="vertical">

    android:id="@+id/iv"

    android:layout_width="96dp"

    android:layout_height="96dp"

    android:padding="6dp"

    app:img="@{food.img}"/>

    ....

    ....

    ....

    OK,我给RelativeLayout容器添了onClick属性,属性的值为food.onItemClick,那么这个onItemClick到底是什么呢?其实就是在实体类Food中定义的一个方法,如下:

    [java]view plaincopy

    print?

    publicvoidonItemClick(View view) {

    Toast.makeText(view.getContext(), getDescription(), Toast.LENGTH_SHORT).show();

    }

    点击item获取当前position的数据,获取方式也是非常简单,直接get方法获取即可,比传统的ListView的点击事件通过position来获取数据方便多了。如果我想为关键字这个TextView添加点击事件也很简单,和上面一样,这里我就不再贴代码了,文末可以下载源码。

    5. 数据更新处理

    单纯的更新Food对象并不能改变ListView的UI显示效果,那该怎么做呢?Google给我们提供了三种解决方案,分别如下:

    1.让实体类继承自BaseObservable

    让实体类继承自BaseObservable,然后给需要改变的字段的get方法添加上@Bindable注解,然后给需要改变的字段的set方法加上notifyPropertyChanged(org.lenve.databinding3.BR.description);一句即可,比如我想点击item的时候把description字段的数据全部改为111,我可以修改Food类变为下面的样子:

    [java]view plaincopy

    print?

    publicclassFoodextendsBaseObservable {

    privateString description;

    privateString img;

    privateString keywords;

    privateString summary;

    publicFood() {

    }

    publicFood(String description, String img, String keywords, String summary) {

    this.description = description;

    this.img = img;

    this.keywords = keywords;

    this.summary = summary;

    }

    @BindingAdapter("bind:img")

    publicstaticvoidloadInternetImage(ImageView iv, String img) {

    Picasso.with(iv.getContext()).load(img).into(iv);

    }

    publicvoidonItemClick(View view) {

    //        Toast.makeText(view.getContext(), getDescription(), Toast.LENGTH_SHORT).show();

    setDescription("111");

    }

    publicvoidclickKeywords(View view) {

    Toast.makeText(view.getContext(), getKeywords(), Toast.LENGTH_SHORT).show();

    }

    @Bindable

    publicString getDescription() {

    returndescription;

    }

    publicvoidsetDescription(String description) {

    this.description = description;

    notifyPropertyChanged(org.lenve.databinding3.BR.description);

    }

    publicString getImg() {

    returnimg;

    }

    publicvoidsetImg(String img) {

    this.img = img;

    }

    publicString getKeywords() {

    returnkeywords;

    }

    publicvoidsetKeywords(String keywords) {

    this.keywords = keywords;

    }

    publicString getSummary() {

    returnsummary;

    }

    publicvoidsetSummary(String summary) {

    this.summary = summary;

    }

    }

    OK,这是第一种解决方案,也是比较简单常用的一种。

    2.使用DataBinding提供的ObservableFields来创建实体类

    这种方式使用起来略微麻烦,除了继承BaseObservable之外,创建属性的方式也变成下面这种:

    [java]view plaincopy

    print?

    privatefinalObservableField description =newObservableField<>();

    属性的读写方式也变了,读取方式如下:

    [java]view plaincopy

    print?

    description.get()

    写入方式如下:

    [java]view plaincopy

    print?

    this.description.set(description);

    OK,依据上面几个规则,我新定义的实体类如下:

    [java]view plaincopy

    print?

    /**

    * Created by 王松 on 2016/7/31.

    */

    publicclassFoodextendsBaseObservable {

    privatefinalObservableField description =newObservableField<>();

    privatefinalObservableField img =newObservableField<>();

    privatefinalObservableField keywords =newObservableField<>();

    privatefinalObservableField summary =newObservableField<>();

    publicFood() {

    }

    publicFood(String description, String img, String keywords, String summary) {

    this.description.set(description);

    this.keywords.set(keywords);

    this.img.set(img);

    this.summary.set(summary);

    }

    @BindingAdapter("bind:img")

    publicstaticvoidloadInternetImage(ImageView iv, String img) {

    Picasso.with(iv.getContext()).load(img).into(iv);

    }

    publicvoidonItemClick(View view) {

    //        Toast.makeText(view.getContext(), getDescription(), Toast.LENGTH_SHORT).show();

    setDescription("111");

    }

    publicvoidclickKeywords(View view) {

    Toast.makeText(view.getContext(), getKeywords(), Toast.LENGTH_SHORT).show();

    }

    @Bindable

    publicString getDescription() {

    returndescription.get();

    }

    publicvoidsetDescription(String description) {

    this.description.set(description);

    notifyPropertyChanged(org.lenve.databinding3.BR.description);

    }

    publicString getImg() {

    returnimg.get();

    }

    publicvoidsetImg(String img) {

    this.img.set(img);

    }

    publicString getKeywords() {

    returnkeywords.get();

    }

    publicvoidsetKeywords(String keywords) {

    this.keywords.set(keywords);

    }

    publicString getSummary() {

    returnsummary.get();

    }

    publicvoidsetSummary(String summary) {

    this.summary.set(summary);

    }

    }

    这种方式实现的功能和第一个实体类实现的功能一模一样。

    3.使用DataBinding中提供的集合来存储数据即可

    DataBinding中给我们提供了一些现成的集合,用来存储数据,比如ObservableArrayList,ObservableArrayMap,因为这些用的少,我这里就不做介绍了。

    相关文章

      网友评论

          本文标题:玩转Android之MVVM开发模式实战,炫酷的DataBind

          本文链接:https://www.haomeiwen.com/subject/szpuuxtx.html