转载:
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 plain copy
1. android {
2. ...
3. ...
4. ...
5. dataBinding{
6. true
7. }
8. }
就是这么简单,一个简单的databinding配置之后,就可以开始使用数据绑定了。
要使用数据绑定,我们得首先创建一个实体类,比如User实体类,如下:
[java] view plain copy
1. /**
2. * Created by 王松 on 2016/7/31.
3. */
4. public class UserEntity {
5. private String username;
6. private String nickname;
7. private int age;
8.
9. public UserEntity() {
10. }
11.
12. public int getAge() {
13. return age;
14. }
15.
16. public void setAge(int age) {
17. this.age = age;
18. }
19.
20. public String getNickname() {
21. return nickname;
22. }
23.
24. public void setNickname(String nickname) {
25. this.nickname = nickname;
26. }
27.
28. public String getUsername() {
29. return username;
30. }
31.
32. public void setUsername(String username) {
33. this.username = username;
34. }
35.
36. public UserEntity(int age, String nickname, String username) {
37. this.age = age;
38. this.nickname = nickname;
39. this.username = username;
40. }
41. }
然后我们来看看布局文件该怎么写,首先布局文件不再是以传统的某一个容器作为根节点,而是使用<layout></layout>作为根节点,在<layout>节点中我们可以通过<data>节点来引入我们要使用的数据源,如下:
[java] view plain copy
1. <?xml version="1.0" encoding="utf-8"?>
2. <layout
3. "http://schemas.android.com/apk/res/android"
4. >
5.
6. <data>
7.
8. <variable
9. "user"
10. "org.lenve.databinding1.UserEntity"/>
11. </data>
12.
13. <LinearLayout
14. "http://schemas.android.com/tools"
15. "match_parent"
16. "match_parent"
17. "vertical"
18. "org.lenve.databinding1.MainActivity">
19.
20. <TextView
21. "wrap_content"
22. "wrap_content"
23. "@{user.username}"/>
24.
25. <TextView
26. "wrap_content"
27. "wrap_content"
28. "@{user.nickname}"/>
29.
30. <TextView
31. "wrap_content"
32. "wrap_content"
33. "@{String.valueOf(user.age)}"/>
34. </LinearLayout>
35. </layout>
在data中定义的variable节点,name属性表示变量的名称,type表示这个变量的类型,实例就是我们实体类的位置,当然,这里你也可以换一种写法,如下:
[java] view plain copy
1. <data>
2.
3. import type="org.lenve.databinding1.UserEntity"/>
4. <variable
5. "user"
6. "UserEntity"/>
7. </data>
先使用import节点将UserEntity导入,然后直接使用即可。但是如果这样的话又会有另外一个问题,假如我有两个类都是UserEntity,这两个UserEntity分属于不同的包中,又该如何?看下面:
[java] view plain copy
1. <data>
2.
3. import type="org.lenve.databinding1.UserEntity" alias="Lenve"/>
4. <variable
5. "user"
6. "Lenve"/>
7. </data>
在import节点中还有一个属性叫做alias,这个属性表示我可以给该类取一个别名,我给UserEntity这个实体类取一个别名叫做Lenve,这样我就可以在variable节点中直接写Lenve了。
看完data节点我们再来看看布局文件,TextView的text属性被我直接设置为了@{user.username},这样,该TextView一会直接将UserEntity实体类的username属性的值显示出来,对于显示age的TextView,我用了String.valueOf来显示,因为大家知道TextView并不能直接显示int型数据,所以需要一个简单的转换,事实上,我们还可以在{}里边进行一些简单的运算,这些我一会再说。
最后,我们来看看Activity中该怎么写,setContentView方法不能够再像以前那样来写了,换成下面的方式:
[java] view plain copy
1. DataBindingUtil.setContentView(this, R.layout.activity_main)
该方法有一个返回值,这个返回值就是系统根据我们的activity_main.xml布局生成的一个ViewModel类,所以完整写法如下:
[java] view plain copy
1. ActivityMainBinding activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
有了ViewModel,再把数据绑定上去就可以了,如下:
[java] view plain copy
1. @Override
2. protected void onCreate(Bundle savedInstanceState) {
3. super.onCreate(savedInstanceState);
4. this, R.layout.activity_main);
5. new UserEntity();
6. 34);
7. "zhangsan");
8. "张三");
9. activityMainBinding.setUser(user);
10. }
运行,显示效果如下:
OK,那我们刚才还说到可以在@{}进行简单的计算,都有哪些计算呢?我们来看看:
1.基本的三目运算
[java] view plain copy
1. <TextView
2. "wrap_content"
3. "wrap_content"
4. "@{user.username??user.nickname}"/>
两个??表示如果username属性为null则显示nickname属性,否则显示username属性。
2.字符拼接
[java] view plain copy
1. <TextView
2. "wrap_content"
3. "wrap_content"
4. "@{`username is :`+user.username}"/>
大家注意,这里的字符拼接不是用单引号哦,用的是ESC按键下面那个按键按出来的。目前DataBinding中的字符拼接还不支持中文。
3.根据数据来决定显示样式
[java] view plain copy
1. <TextView
2. "wrap_content"
3. "wrap_content"
4. "@{user.age < 30 ? 0xFF0000FF:0xFFFF0000}"
5. "@{String.valueOf(user.age)}"/>
我在这里给TextView设置背景的时候,做了一个简单的判断,如果用户的年龄小于30,背景就显示为蓝色,否则背景就显示为红色,DataBinding里支持小于号但是不支持大于号,索性,大于小于号我都用转义字符来表示。
另外,DataBinding对于基本的四则运算、逻辑与、逻辑或、取反位移等都是支持的,我这里不再举例。
2.绑定ImageView
OK,上文只是一个简单的绑定文本,下面我们来看看怎么样绑定图片,这里我们还得介绍DataBinding的另一项新功能,就是关于DataBinding自定义属性的问题,事实上,在我们使用DataBinding的时候,可以给一个控件自定义一个属性,比如我们下面即将说的这个绑定ImageView的案例。假设我现在想要通过Picasso显示一张网络图片,正常情况下这个显示很简单,可是如果我要通过DataBinding来实现,该怎么做呢?我们可以使用
[java] view plain copy
1. @BindingAdapter
注解来创建一个自定义属性,同时还要有一个配套的注解的方法。当我们在布局文件中使用这个自定义属性的时候,会触发这个被我们注解的方法,这样说大家可能还有一点模糊,我们来看看新的实体类:
[java] view plain copy
1. /**
2. * Created by 王松 on 2016/7/31.
3. */
4. public class User {
5. private String username;
6. private String userface;
7.
8. public User() {
9. }
10.
11. public User(String userface, String username) {
12. this.userface = userface;
13. this.username = username;
14. }
15.
16. @BindingAdapter("bind:userface")
17. public static void getInternetImage(ImageView iv, String userface) {
18. Picasso.with(iv.getContext()).load(userface).into(iv);
19. }
20.
21. public String getUserface() {
22. return userface;
23. }
24.
25. public void setUserface(String userface) {
26. this.userface = userface;
27. }
28.
29. public String getUsername() {
30. return username;
31. }
32.
33. public void setUsername(String username) {
34. this.username = username;
35. }
36. }
新类里边只有两个属性,分别是用户名和用户图像,用户图像中存储的实际上是一个网络图片地址,这里除了基本的get/set方法之外还多了一个叫做getInternetImage的网络方法,这个方法有一个注解@BindAdapter("bind:userface"),该注解表示当用户在ImageView中使用自定义属性userface的时候,会触发这个方法,我在这个方法中来为这个ImageView加载一张图片,这里有一点需要注意,就是该方法必须为静态方法。OK,我们再来看看这次的布局文件:
[java] view plain copy
1. <?xml version="1.0" encoding="utf-8"?>
2. <layout
3. "http://schemas.android.com/apk/res/android"
4. "http://schemas.android.com/apk/res-auto"
5. >
6.
7. <data>
8.
9. <variable
10. "user"
11. "org.lenve.databinding2.User"/>
12. </data>
13.
14. <LinearLayout
15. "http://schemas.android.com/tools"
16. "match_parent"
17. "match_parent"
18. "vertical"
19. "org.lenve.databinding2.MainActivity">
20.
21. <ImageView
22. "@+id/iv"
23. "wrap_content"
24. "wrap_content"
25. "@{user.userface}"></ImageView>
26.
27. <TextView
28. "wrap_content"
29. "wrap_content"
30. "@{user.username}"/>
31. </LinearLayout>
32. </layout>
大家注意我在ImageView控件中使用userface属性的时候,使用的前缀不是android而是app哦。再来看看Activity中的代码:
[java] view plain copy
1. @Override
2. protected void onCreate(Bundle savedInstanceState) {
3. super.onCreate(savedInstanceState);
4. this, R.layout.activity_main);
5. new User("http://img2.cache.netease.com/auto/2016/7/28/201607282215432cd8a.jpg", "张三"));
6. }
就是这么简单,加上网络权限就可以运行了,运行效果如下:
3.绑定ListView
好了,看完了简单使用之后,不知道你有没有喜欢上DataBinding,如果还没有,那就再来看看使用DataBinding来给ListView绑定数据吧,这个你一定会喜欢上的。因为使用这中方式来绑定太简单了。
先来看看我们要做的效果吧:
就是一个ListView,左边显示图片,右边显示文本,这样一个效果。OK,那就一步一步来吧,先是主布局:
[java] view plain copy
1. <?xml version="1.0" encoding="utf-8"?>
2. <RelativeLayout
3. "http://schemas.android.com/apk/res/android"
4. "http://schemas.android.com/tools"
5. "match_parent"
6. "match_parent"
7. "org.lenve.databinding3.MainActivity">
8.
9. <ListView
10. "@+id/lv"
11. "match_parent"
12. "match_parent"></ListView>
13. </RelativeLayout>
主布局很简单,就是一个ListView,再来看看ListView的item布局:
[java]
view plain
copy
1. <?xml version="1.0" encoding="utf-8"?>
2. <layout
3. "http://schemas.android.com/apk/res/android"
4. "http://schemas.android.com/apk/res-auto"
5. >
6.
7. <data>
8.
9. <variable
10. "food"
11. "org.lenve.databinding3.Food"/>
12. </data>
13.
14. <RelativeLayout
15. "match_parent"
16. "96dp"
17. "vertical">
18.
19. <ImageView
20. "@+id/iv"
21. "96dp"
22. "96dp"
23. "6dp"
24. "@{food.img}"/>
25.
26. <TextView
27. "@+id/description"
28. "match_parent"
29. "wrap_content"
30. "8dp"
31. "@id/iv"
32. "end"
33. "3"
34. "@{food.description}"/>
35.
36. <TextView
37. "wrap_content"
38. "wrap_content"
39. "8dp"
40. "@id/iv"
41. "true"
42. "2dp"
43. "@{food.keywords}"
44. "bold"/>
45. </RelativeLayout>
46. </layout>
图片加载、文本加载前两节都已经说过了,这里的东西就没有什么难度了,我们再来看看实体类Food:
[java] view plain copy
1. /**
2. * Created by 王松 on 2016/7/31.
3. */
4. public class Food {
5. private String description;
6. private String img;
7. private String keywords;
8. private String summary;
9.
10. public Food() {
11. }
12.
13. public Food(String description, String img, String keywords, String summary) {
14. this.description = description;
15. this.img = img;
16. this.keywords = keywords;
17. this.summary = summary;
18. }
19.
20. @BindingAdapter("bind:img")
21. public static void loadInternetImage(ImageView iv, String img) {
22. Picasso.with(iv.getContext()).load(img).into(iv);
23. }
24.
25. public String getDescription() {
26. return description;
27. }
28.
29. public void setDescription(String description) {
30. this.description = description;
31. }
32.
33. public String getImg() {
34. return img;
35. }
36.
37. public void setImg(String img) {
38. this.img = img;
39. }
40.
41. public String getKeywords() {
42. return keywords;
43. }
44.
45. public void setKeywords(String keywords) {
46. this.keywords = keywords;
47. }
48.
49. public String getSummary() {
50. return summary;
51. }
52.
53. public void setSummary(String summary) {
54. this.summary = summary;
55. }
56. }
这个实体类中有一个加载图片的方法,加载方式我们上文都已经介绍过了,不多说。好了,再来看看我们的终极Adapter类:
[java] view plain copy
1. /**
2. * Created by 王松 on 2016/7/31.
3. */
4. public class MyBaseAdapter<T> extends BaseAdapter {
5. private Context context;
6. private LayoutInflater inflater;
7. private int layoutId;
8. private int variableId;
9. private List<T> list;
10.
11. public MyBaseAdapter(Context context, int layoutId, List<T> list, int resId) {
12. this.context = context;
13. this.layoutId = layoutId;
14. this.list = list;
15. this.variableId = resId;
16. inflater = LayoutInflater.from(context);
17. }
18.
19. @Override
20.
21. public int getCount() {
22. return list.size();
23. }
24.
25. @Override
26. public Object getItem(int position) {
27. return list.get(position);
28. }
29.
30. @Override
31. public long getItemId(int position) {
32. return position;
33. }
34.
35. @Override
36. public View getView(int position, View convertView, ViewGroup parent) {
37. ViewDataBinding dataBinding;
38. if (convertView == null) {
39. false);
40. else{
41. dataBinding = DataBindingUtil.getBinding(convertView);
42. }
43. dataBinding.setVariable(variableId, list.get(position));
44. return dataBinding.getRoot();
45. }
46. }
这个大概算是Adapter的终极写法了,如果你按这种方式来写Adapter,那么如果没有非常奇葩的需求,你这个App中可能就只有这一个给ListView使用的Adapter了,为什么这么说呢?因为这个Adapter中没有一个变量和我们的ListView沾边,解释一下几个变量吧:layoutId这个表示item布局的资源id,variableId是系统自动生成的,根据我们的实体类,直接从外部传入即可。另外注意布局加载方式为DataBindingUtil类中的inflate方法。OK,最后再来看看Activity:
[java] view plain copy
1. public class MainActivity extends AppCompatActivity {
2.
3. private Handler mHandler = new Handler(){
4. @Override
5. public void handleMessage(Message msg) {
6. new MyBaseAdapter<>(MainActivity.this, R.layout.listview_item, foods, org.lenve.databinding3.BR.food);
7. lv.setAdapter(adapter);
8. }
9. };
10. private List<Food> foods;
11. private ListView lv;
12.
13. @Override
14. protected void onCreate(Bundle savedInstanceState) {
15. super.onCreate(savedInstanceState);
16. setContentView(R.layout.activity_main);
17. lv = ((ListView) findViewById(R.id.lv));
18. initData();
19. }
20.
21. private void initData() {
22. new OkHttpClient.Builder().build();
23. new Request.Builder().url("http://www.tngou.net/api/food/list?id=1").build();
24. new Callback() {
25. @Override
26. public void onFailure(Call call, IOException e) {
27.
28. }
29.
30. @Override
31. public void onResponse(Call call, Response response) throws IOException {
32. if (response.isSuccessful()) {
33. parseJson(response.body().string());
34. }
35. }
36. });
37. }
38.
39. private void parseJson(String jsonStr) {
40. new ArrayList<>();
41. try {
42. new JSONObject(jsonStr);
43. "tngou");
44. for (int i = 0; i < tngou.length(); i++) {
45. JSONObject item = tngou.getJSONObject(i);
46. "description");
47. "http://tnfs.tngou.net/image"+item.getString("img");
48. "【关键词】 "+item.getString("keywords");
49. "summary");
50. new Food(description, img, keywords, summary));
51. }
52. 0);
53. catch (JSONException e) {
54. e.printStackTrace();
55. }
56. }
57. }
OkHttp下载数据和Json解析自不用多说,在构造MyAdapter的时候传入的最后一个参数,是BR中的,这个BR和我们项目中的R文件类似,都是系统自动生成的。
至此,我们使用DataBinding的方式来给ListView加载数据就算完成了。so easy~~~
4.点击事件处理
如果你使用DataBinding,我们的点击事件也会有新的处理方式,首先以ListView为例来说说如何绑定点击事件,在listview_item布局文件中每一个item的根节点添加如下代码:
[java] view plain copy
1. <?xml version="1.0" encoding="utf-8"?>
2. <layout
3. "http://schemas.android.com/apk/res/android"
4. "http://schemas.android.com/apk/res-auto"
5. >
6. ....
7. ....
8. <RelativeLayout
9. "match_parent"
10. "96dp"
11. "@{food.onItemClick}"
12. "vertical">
13.
14. <ImageView
15. "@+id/iv"
16. "96dp"
17. "96dp"
18. "6dp"
19. "@{food.img}"/>
20. ....
21. ....
22. ....
23. </RelativeLayout>
24. </layout>
OK,我给RelativeLayout容器添了onClick属性,属性的值为food.onItemClick,那么这个onItemClick到底是什么呢?其实就是在实体类Food中定义的一个方法,如下:
[java] view plain copy
1. public void onItemClick(View view) {
2. Toast.makeText(view.getContext(), getDescription(), Toast.LENGTH_SHORT).show();
3. }
点击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 plain copy
1. public class Food extends BaseObservable {
2. private String description;
3. private String img;
4. private String keywords;
5. private String summary;
6.
7. public Food() {
8. }
9.
10. public Food(String description, String img, String keywords, String summary) {
11. this.description = description;
12. this.img = img;
13. this.keywords = keywords;
14. this.summary = summary;
15. }
16.
17. @BindingAdapter("bind:img")
18. public static void loadInternetImage(ImageView iv, String img) {
19. Picasso.with(iv.getContext()).load(img).into(iv);
20. }
21.
22. public void onItemClick(View view) {
23. // Toast.makeText(view.getContext(), getDescription(), Toast.LENGTH_SHORT).show();
24. "111");
25. }
26.
27. public void clickKeywords(View view) {
28. Toast.makeText(view.getContext(), getKeywords(), Toast.LENGTH_SHORT).show();
29. }
30.
31.
32. @Bindable
33. public String getDescription() {
34. return description;
35. }
36.
37. public void setDescription(String description) {
38. this.description = description;
39. notifyPropertyChanged(org.lenve.databinding3.BR.description);
40. }
41.
42. public String getImg() {
43. return img;
44. }
45.
46. public void setImg(String img) {
47. this.img = img;
48. }
49.
50. public String getKeywords() {
51. return keywords;
52. }
53.
54. public void setKeywords(String keywords) {
55. this.keywords = keywords;
56. }
57.
58. public String getSummary() {
59. return summary;
60. }
61.
62. public void setSummary(String summary) {
63. this.summary = summary;
64. }
65. }
OK,这是第一种解决方案,也是比较简单常用的一种。
2.使用DataBinding提供的ObservableFields来创建实体类
这种方式使用起来略微麻烦,除了继承BaseObservable之外,创建属性的方式也变成下面这种:
[java] view plain copy
1. private final ObservableField<String> description = new ObservableField<>();
属性的读写方式也变了,读取方式如下:
[java] view plain copy
1. description.get()
写入方式如下:
[java] view plain copy
1. this.description.set(description);
OK,依据上面几个规则,我新定义的实体类如下:
[java] view plain copy
1. /**
2. * Created by 王松 on 2016/7/31.
3. */
4. public class Food extends BaseObservable {
5. private final ObservableField<String> description = new ObservableField<>();
6. private final ObservableField<String> img = new ObservableField<>();
7. private final ObservableField<String> keywords = new ObservableField<>();
8. private final ObservableField<String> summary = new ObservableField<>();
9. public Food() {
10. }
11.
12. public Food(String description, String img, String keywords, String summary) {
13. this.description.set(description);
14. this.keywords.set(keywords);
15. this.img.set(img);
16. this.summary.set(summary);
17. }
18.
19. @BindingAdapter("bind:img")
20. public static void loadInternetImage(ImageView iv, String img) {
21. Picasso.with(iv.getContext()).load(img).into(iv);
22. }
23.
24. public void onItemClick(View view) {
25. // Toast.makeText(view.getContext(), getDescription(), Toast.LENGTH_SHORT).show();
26. "111");
27. }
28.
29. public void clickKeywords(View view) {
30. Toast.makeText(view.getContext(), getKeywords(), Toast.LENGTH_SHORT).show();
31. }
32.
33.
34. @Bindable
35. public String getDescription() {
36. return description.get();
37. }
38.
39. public void setDescription(String description) {
40. this.description.set(description);
41. notifyPropertyChanged(org.lenve.databinding3.BR.description);
42. }
43.
44. public String getImg() {
45. return img.get();
46. }
47.
48. public void setImg(String img) {
49. this.img.set(img);
50. }
51.
52. public String getKeywords() {
53. return keywords.get();
54. }
55.
56. public void setKeywords(String keywords) {
57. this.keywords.set(keywords);
58. }
59.
60. public String getSummary() {
61. return summary.get();
62. }
63.
64. public void setSummary(String summary) {
65. this.summary.set(summary);
66. }
67. }
这种方式实现的功能和第一个实体类实现的功能一模一样。
3.使用DataBinding中提供的集合来存储数据即可
DataBinding中给我们提供了一些现成的集合,用来存储数据,比如ObservableArrayList,ObservableArrayMap,因为这些用的少,我这里就不做介绍了。
本文共涉及到三个Demo,由于CSDN对上传文件大小的限制,我分三次上传,下载地址如下:
以上。