Android App架构设计的目的是通过设计使程序模块化,做到模块内部的高聚合和模块之间的低耦合。这样做的好处是使得程序在开发的过程中,开发人员只需要专注于一点,提高程序开发的效率,并且更容易进行后续的测试以及定位问题。但设计不能违背目的,对于不同量级的工程,具体架构的实现方式必然是不同的,切忌犯为了设计而设计,为了架构而架构的毛病。
Android的架构设计从早期的MVC模式渐渐演变出了MVP模式,分离了Model业务逻辑层和View视图层,到后来又演变出了MVVM模式,由MVP进化而来,其中VM代表ViewModel,替代了MVP模式中的Preseter中间层。
MVC->MVP->MVVM这几个软件设计模式是一步步演化发展的,MVP模式中View视图和Model业务逻辑之间通过Presenter来中转,虽然这样隔离了Model和View,方便了测试,但是需要编写的接口和代码量很多,代码不够优雅简洁,所以MVVM弥补了这个缺陷。在MVVM使用了DataBinding即数据绑定来代替Presenter,实现View和Model之间的通信。
MVVM架构模式主要由View,Model,ViewModel三种主要的成分组成:
Google大佬给开发者提供了DataBinding库来使数据绑定具有更高的灵活性和兼容性,最低支持Android 2.1(API level 7+)。使用数据绑定库在配置上有一点要求,不支持Eclipse,Android Studio版本要求1.3以上的版本,Android Gradile构建工具版本需要1.5.0-alpha1以上,目前最新是3.4,建议上Gradle官网下载后再去配置,官方网址:https://gradle.org/install。
配置完开发工具后,还需要在Android Sdk Manager中下载最新的支持库Support repository。下载完后在当前app module项目下的build.gradle中android节点添加如下代码开启数据绑定,例如:
android {
....
dataBinding {
enabled = true
}
}
注意:如果你的app依赖一个使用数据绑定的外部模块,那么你的app也必须在build.gradle中开启数据绑定。
当使用了DataBinding数据绑定后,编写XML布局文件的方式就与以前完全不一样了。XML布局文件中的根节点是layout,里面包含data节点和view根元素节点,其中data节点主要是用来声明在xml布局中需要使用的变量,view根元素节点就是以前xml布局文件中的根节点了,一般是ViewGroup类型,如LinearLayout,RelativeLayout等。
首先先创建一个JavaBeans 对象User:
public class User {
private final String firstName;
private final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
}
然后编写一个新的xml布局文件activity_main.xml:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.example.User"/>
data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"/>
LinearLayout>
layout>
在上面的代码中,data节点中包含了一个名为user的变量,它引用了User对象,接下来可以在元素节点属性中使用这个变量了,例如在TextView中的text属性参数使用了@{user.firstName}来获取user对象的firstName的值。这样在TextView初始化的时候就会获取user.firstName的值来设置它的text属性了,不用我们去设置setText了,这就是数据绑定了。
注意:在User对象中firstName和lastName属性虽然是private私有类型的,但是DataBinding语法中定义@{user.firstName} 和@{user.getFirstName()} 这两种情况是等价的,也就是使用这两个语法中的其中一个就能正常工作,首选是使用第二个语法,也就是user.getFirstName(),符合JavaBean的规范。
在开始编写activity前,首先来了解下DataBinding的特性。当开启了DataBinding后,每当我们新创建一个layout布局文件时,构建工具都会自动生成一个与这个layout名字相关的Binding类。构建工具会把你的layout文件名转换为驼峰式大小写形式,后面再添加Binding后缀,例如在上面创建的activity_main.xml,系统就会生成一个名为ActivityMainBinding的类。这些生成Binding类就是DataBinding数据绑定的库的核心所在了,这些Binding类包含了在layout文件中声明的变量,例如上面的user变量,也就是在Binding类中也会有setUser和getUser方法,并且这些类会自动解析view元素属性中的binding表达式@{ expressions },然后自动把值设置给这个属性。
可以在build-generated-source-apt-debug-com文件夹中看到这些生成的Binding源码:
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding= DataBindingUtil.setContentView(this,R.layout.activity_main);
User user = new User("Test", "User");
binding.setUser(user);
}
运行后,layout中的textView的text被设置为Test和User了,不用我们去setText方法了,降低耦合,减少了代码的输入流,实现数据绑定。
官方还有一种方式,例如下面代码,但是运行没效果,求评论指点:
ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
如果在ListView或者RecyclerView中使用adapter,那么就要使用data binding items了,例如下面的代码:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
<data>
"user" type="com.example.User"/>
data>
现在可以使用import元素了:
<data>
<import type="com.example.User"/>
<variable
name="user"
type="User"/>//这样就不用输入全路径名了
</data>
使用import元素的另一个用途是导进工具类,然后可以在xml布局文件中直接调用静态方法或者静态属性:
//编写一个工具类
public class StrUtil {
public static String addWarming(String str){
return str+"didi";
}
}
//在布局文件中引进
<data>
<import type="utils.StrUtil"/>
<variable name="user" type="com.example.User"/>
</data>
//然后在view的属性中使用@{ }就可以直接调用了
<TextView
android:textSize="14sp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@{StrUtil.addWarming(user.getName())}"
tools:text="Jason"/>
同时也可以导进android中的提供的内置类:
<data>
<import type="android.view.View"/>
data>
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
import元素标记除了type属性,还有alias属性。alias属性是为了解决导入的类同名冲突的情况,使用这个属性就可以给类设置别名,例如下面的代码创建了一个自定义View,和系统的View同名冲突:
<data>
<import type="android.view.View"/>
<import type="utils.View" alias="ViewUtil"/>
这样在layout文件中View引用android.view.View,而ViewUtil引用utils.View自定义类。
同时Import也可以导进数据类型,所以variable属性和@{ }表达式中使用:
<data>
<import type="com.example.User"/>
<import type="java.util.List"/>
<variable name="user" type="User"/>
<variable name="userList" type="List<User>"/>//<是<转义,>是>转义
</data>
<data>
<import type="android.graphics.drawable.Drawable"/>
<variable name="user" type="com.example.User"/>
<variable name="image" type="Drawable"/>
<variable name="note" type="String"/>
</data>
在构建工具自动生成的binding class中为自动会每个variables变量创建一个setter和getter方法,每个变量都会自动初始化,引用类型初始化为null,基本数据类型如int初始化为0,boolean初始化为false等。
注意构建工具会自动为我们开发者生成一个context变量方便我们在binding表达式中使用,不用自己导进Context类,这个context的值就是view根元素的getContext( )方法中获取的Context上下文。我们可以显式声明一个Context变量来重写这个context,但一般情况下不用的。
//会在默认路径中生成一个ContactItem的class类
"ContactItem">
...
//加上.前戳,会在默认包路径下生成class类
".ContactItem">
...
//或者使用路径全限定名明确指定生成的路径
"com.example.ContactItem">
...
//在name.xml和contact.xml中必须声明user变量
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable name="user" type="com.example.User"/>
data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/name"
bind:user="@{user}"/>
<include layout="@layout/contact"
bind:user="@{user}"/>
LinearLayout>
layout>
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
注意:当在@{ } 表达式使用双引用后,在外部可以使用单引用代替双引号,例如’@{“image_” + id}’。
android:text="@{user.displayName ?? user.lastName}"
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
//两者是等价的
<data>
<import type="android.util.SparseArray"/>
<import type="java.util.Map"/>
<import type="java.util.List"/>
<variable name="list" type="List<String>"/>
<variable name="sparse" type="SparseArray<String>"/>
<variable name="map" type="Map<String, String>"/>
<variable name="index" type="int"/>
<variable name="key" type="String"/>
</data>
…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"
android:text='@{map["firstName"]}'
android:text="@{map[`firstName`}"
android:text="@{map['firstName']}"
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
一些资源需要显式资源的引用:
数据类型 | 原生引用 | @{}表达式中引用 |
---|---|---|
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
Animator | @animator | @animator |
StateListAnimator | @animator | @stateListAnimator |
color int | @color | @color |
ColorStateList | @color | @colorStateList |