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

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

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


1.基本使用json

2.绑定ImageView
api

3.绑定ListView网络

4.点击事件处理app

5.数据更新处理框架

好了,那就开始吧!
ide

1.基本使用

建立好一个Android Project以后,在gradle文件中添加以下几行代码,表示开启databinding:布局

android {
    ...
	...
	...
    dataBinding{
        enabled true
    }
}

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

要使用数据绑定,咱们得首先建立一个实体类,好比User实体类,以下:性能

/**
 * Created by 王松 on 2016/7/31.
 */
public class UserEntity {
    private String username;
    private String nickname;
    private int age;

    public UserEntity() {
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        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(int age, String nickname, String username) {
        this.age = age;
        this.nickname = nickname;
        this.username = username;
    }
}

而后咱们来看看布局文件该怎么写,首先布局文件再也不是以传统的某一个容器做为根节点,而是使用<layout></layout>做为根节点,在<layout>节点中咱们能够经过<data>节点来引入咱们要使用的数据源,以下:

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    >

    <data>

        <variable
            name="user"
            type="org.lenve.databinding1.UserEntity"/>
    </data>

    <LinearLayout
        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">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.username}"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.nickname}"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{String.valueOf(user.age)}"/>
    </LinearLayout>
</layout>

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

    <data>

        <import type="org.lenve.databinding1.UserEntity"/>
        <variable
            name="user"
            type="UserEntity"/>
    </data>

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

    <data>

        <import type="org.lenve.databinding1.UserEntity" alias="Lenve"/>
        <variable
            name="user"
            type="Lenve"/>
    </data>


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

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

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

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

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

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

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        UserEntity user = new UserEntity();
        user.setAge(34);
        user.setUsername("zhangsan");
        user.setNickname("张三");
        activityMainBinding.setUser(user);
    }


运行,显示效果以下:

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

1.基本的三目运算

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.username??user.nickname}"/>

两个??表示若是username属性为null则显示nickname属性,不然显示username属性。

2.字符拼接

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{`username is :`+user.username}"/>

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

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

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@{user.age &lt; 30 ? 0xFF0000FF:0xFFFF0000}"
            android:text="@{String.valueOf(user.age)}"/>
我在这里给TextView设置背景的时候,作了一个简单的判断,若是用户的年龄小于30,背景就显示为蓝色,不然背景就显示为红色,DataBinding里支持小于号可是不支持大于号,索性,大于小于号我都用转义字符来表示。


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


2.绑定ImageView

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

@BindingAdapter

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

/**
 * Created by 王松 on 2016/7/31.
 */
public class User {
    private String username;
    private String userface;

    public User() {
    }

    public User(String userface, String username) {
        this.userface = userface;
        this.username = username;
    }

    @BindingAdapter("bind:userface")
    public static void getInternetImage(ImageView iv, String userface) {
        Picasso.with(iv.getContext()).load(userface).into(iv);
    }

    public String getUserface() {
        return userface;
    }

    public void setUserface(String userface) {
        this.userface = userface;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}

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

<?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="user"
            type="org.lenve.databinding2.User"/>
    </data>

    <LinearLayout
        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">

        <ImageView
            android:id="@+id/iv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:userface="@{user.userface}"></ImageView>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.username}"/>
    </LinearLayout>
</layout>

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        dataBinding.setUser(new User("http://img2.cache.netease.com/auto/2016/7/28/201607282215432cd8a.jpg", "张三"));
    }

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



3.绑定ListView

好了,看完了简单使用以后,不知道你有没有喜欢上DataBinding,若是尚未,那就再来看看使用DataBinding来给ListView绑定数据吧,这个你必定会喜欢上的。由于使用这中方式来绑定太简单了。

先来看看咱们要作的效果吧:

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

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    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">

    <ListView
        android:id="@+id/lv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></ListView>
</RelativeLayout>

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

<?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="food"
            type="org.lenve.databinding3.Food"/>
    </data>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="96dp"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/iv"
            android:layout_width="96dp"
            android:layout_height="96dp"
            android:padding="6dp"
            app:img="@{food.img}"/>

        <TextView
            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}"/>

        <TextView
            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"/>
    </RelativeLayout>
</layout>

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

/**
 * Created by 王松 on 2016/7/31.
 */
public class Food {
    private String description;
    private String img;
    private String keywords;
    private String summary;

    public Food() {
    }

    public Food(String description, String img, String keywords, String summary) {
        this.description = description;
        this.img = img;
        this.keywords = keywords;
        this.summary = summary;
    }

    @BindingAdapter("bind:img")
    public static void loadInternetImage(ImageView iv, String img) {
        Picasso.with(iv.getContext()).load(img).into(iv);
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getImg() {
        return img;
    }

    public void setImg(String img) {
        this.img = img;
    }

    public String getKeywords() {
        return keywords;
    }

    public void setKeywords(String keywords) {
        this.keywords = keywords;
    }

    public String getSummary() {
        return summary;
    }

    public void setSummary(String summary) {
        this.summary = summary;
    }
}

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

/**
 * Created by 王松 on 2016/7/31.
 */
public class MyBaseAdapter<T> extends BaseAdapter {
    private Context context;
    private LayoutInflater inflater;
    private int layoutId;
    private int variableId;
    private List<T> list;

    public MyBaseAdapter(Context context, int layoutId, List<T> list, int resId) {
        this.context = context;
        this.layoutId = layoutId;
        this.list = list;
        this.variableId = resId;
        inflater = LayoutInflater.from(context);
    }

    @Override

    public int getCount() {
        return list.size();
    }

    @Override
    public Object getItem(int position) {
        return list.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, 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));
        return dataBinding.getRoot();
    }
}

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

public class MainActivity extends AppCompatActivity {

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            MyBaseAdapter<Food> adapter = new MyBaseAdapter<>(MainActivity.this, R.layout.listview_item, foods, org.lenve.databinding3.BR.food);
            lv.setAdapter(adapter);
        }
    };
    private List<Food> foods;
    private ListView lv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        lv = ((ListView) findViewById(R.id.lv));
        initData();
    }

    private void initData() {
        OkHttpClient client = new OkHttpClient.Builder().build();
        Request request = new Request.Builder().url("http://www.tngou.net/api/food/list?id=1").build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                if (response.isSuccessful()) {
                    parseJson(response.body().string());
                }
            }
        });
    }

    private void parseJson(String jsonStr) {
        foods = new ArrayList<>();
        try {
            JSONObject jo = new JSONObject(jsonStr);
            JSONArray tngou = jo.getJSONArray("tngou");
            for (int i = 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(new Food(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的根节点添加以下代码:

<?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"
    >
	....
	....
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="96dp"
        android:onClick="@{food.onItemClick}"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/iv"
            android:layout_width="96dp"
            android:layout_height="96dp"
            android:padding="6dp"
            app:img="@{food.img}"/>
		....
		....
		....
    </RelativeLayout>
</layout>

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

    public void onItemClick(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类变为下面的样子:

public class Food extends BaseObservable {
    private String description;
    private String img;
    private String keywords;
    private String summary;

    public Food() {
    }

    public Food(String description, String img, String keywords, String summary) {
        this.description = description;
        this.img = img;
        this.keywords = keywords;
        this.summary = summary;
    }

    @BindingAdapter("bind:img")
    public static void loadInternetImage(ImageView iv, String img) {
        Picasso.with(iv.getContext()).load(img).into(iv);
    }

    public void onItemClick(View view) {
//        Toast.makeText(view.getContext(), getDescription(), Toast.LENGTH_SHORT).show();
        setDescription("111");
    }

    public void clickKeywords(View view) {
        Toast.makeText(view.getContext(), getKeywords(), Toast.LENGTH_SHORT).show();
    }


    @Bindable
    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
        notifyPropertyChanged(org.lenve.databinding3.BR.description);
    }

    public String getImg() {
        return img;
    }

    public void setImg(String img) {
        this.img = img;
    }

    public String getKeywords() {
        return keywords;
    }

    public void setKeywords(String keywords) {
        this.keywords = keywords;
    }

    public String getSummary() {
        return summary;
    }

    public void setSummary(String summary) {
        this.summary = summary;
    }
}

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

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

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

private final ObservableField<String> description = new ObservableField<>();

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

description.get()

写入方式以下:

this.description.set(description);

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

/**
 * Created by 王松 on 2016/7/31.
 */
public class Food extends BaseObservable {
    private final ObservableField<String> description = new ObservableField<>();
    private final ObservableField<String> img = new ObservableField<>();
    private final ObservableField<String> keywords = new ObservableField<>();
    private final ObservableField<String> summary = new ObservableField<>();
    public Food() {
    }

    public Food(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")
    public static void loadInternetImage(ImageView iv, String img) {
        Picasso.with(iv.getContext()).load(img).into(iv);
    }

    public void onItemClick(View view) {
//        Toast.makeText(view.getContext(), getDescription(), Toast.LENGTH_SHORT).show();
        setDescription("111");
    }

    public void clickKeywords(View view) {
        Toast.makeText(view.getContext(), getKeywords(), Toast.LENGTH_SHORT).show();
    }


    @Bindable
    public String getDescription() {
        return description.get();
    }

    public void setDescription(String description) {
        this.description.set(description);
        notifyPropertyChanged(org.lenve.databinding3.BR.description);
    }

    public String getImg() {
        return img.get();
    }

    public void setImg(String img) {
        this.img.set(img);
    }

    public String getKeywords() {
        return keywords.get();
    }

    public void setKeywords(String keywords) {
        this.keywords.set(keywords);
    }

    public String getSummary() {
        return summary.get();
    }

    public void setSummary(String summary) {
        this.summary.set(summary);
    }
}

这种方式实现的功能和第一个实体类实现的功能如出一辙。

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

DataBinding中给咱们提供了一些现成的集合,用来存储数据,好比ObservableArrayList,ObservableArrayMap,由于这些用的少,我这里就不作介绍了。


本文共涉及到三个Demo,因为CSDN对上传文件大小的限制,我分三次上传,下载地址以下:


1.http://download.csdn.net/detail/u012702547/9591142

2.http://download.csdn.net/detail/u012702547/9591150

3.http://download.csdn.net/detail/u012702547/9591160


以上。

相关文章
相关标签/搜索