Android App架构设计
Android App架构设计的目的是通过设计使程序模块化,做到模块内部的高聚合和模块之间的低耦合。这样做的好处是使得程序在开发的过程中,开发人员只需要专注于一点,提高程序开发的效率,并且更容易进行后续的测试以及定位问题。但设计不能违背目的,对于不同量级的工程,具体架构的实现方式必然是不同的,切忌犯为了设计而设计,为了架构而架构的毛病。
Android的架构设计从早期的MVC模式渐渐演变出了MVP模式,分离了Model业务逻辑层和View视图层,到后来又演变出了MVVM模式,由MVP进化而来,其中VM代表ViewModel,替代了MVP模式中的Preseter中间层。
下面是这3个架构模式的具体框架:
MVC:
MVP:
MVVM:
MVC->MVP->MVVM这几个软件设计模式是一步步演化发展的,MVP模式中View视图和Model业务逻辑之间通过Presenter来中转,虽然这样隔离了Model和View,方便了测试,但是需要编写的接口和代码量很多,代码不够优雅简洁,所以MVVM弥补了这个缺陷。在MVVM使用了DataBinding即数据绑定来代替Presenter,实现View和Model之间的通信。
MVVM架构模式主要由View,Model,ViewModel三种主要的成分组成:
- Model:数据对象,负责存储,检索和操纵数据。同时提供本应用外部对应用程序数据的操作的接口,也可以在数据变化时发出变更通知。Model不依赖于View的实现,只要外部程序调用Model的接口就能够实现对数据的增删查改。
- View:视图对象,UI层负责绘制UI元素,提供与用户进行交互的操作功能,包括UI展现代码以及一些相关的界面逻辑代码。
- ViewModel:数据绑定层,这里的Model指的是View的Model,跟MVVM中的Model层不是同一个对象。所谓View的Model就是一个包含View的数据属性和操作View数据的方法的对象。这种模式的关键技术就是数据绑定(DataBinding),View的变化会直接影响ViewModel,ViewModel的变化或者内容会直接体现在View上。这种架构模式实际上是框架替开发者封装了一些对象和操作,开发者只需要较少的代码就能实现比较复杂的交互。
-
Data Binding库配置
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中开启数据绑定。
Layout布局中获取数据变量
当使用了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:
<?xml version="1.0" encoding="utf-8"?> <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文件中绑定数据变量
在开始编写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);
Layout布局文件中新增属性和元素标记
- Import元素标记
import关键字在java文件中很常见,一般是用来导进外部class文件的,在xml布局文件中的<data>元素标记里面可以使用<import>元素标记来引用外部的class,导进了外部的class文件后,就可以在xml布局文件中使用了,注意需要使用全限定名:
例如之前是使用type来引用外部class,来声明变量的类型: -
<data> <variable name="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>
- Variables
在layout的<data>元素标记内可以包含N个Variables元素,Variables就是声明一个在layout文件中的binding表达式@{ } 中使用的变量: -
<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,但一般情况下不用的。 - 自定义binding class名
构建工具默认生成Binding class名字是根据layout文件名来生成的,一般是以大写字母开头,替换_下划线,并在下划线之后的第一个字母转换为大写,即驼峰式大小写规则,最后加Binding后缀.class。可以在build-generated-source-apt-debug-com-example.xxxx.xxxx-databinding文件夹中看到生成的这些类。
可以在<data>标记中使用class属性来重命名关联当前布局的Binding类,或者重定向这个Binding类的路径,例如: -
//会在默认路径中生成一个ContactItem的class类 <data class="ContactItem"> ... </data> //加上.前戳,会在默认包路径下生成class类 <data class=".ContactItem"> ... </data> //或者使用路径全限定名明确指定生成的路径 <data class="com.example.ContactItem"> ... </data>
- Includes
variables声明的变量可以直接传递进去在布局文件中引用的重用布局Include,这样include引用的布局文件中的声明的变量就可以使用外部layout文件中的命名空间和变量值,前提是include布局中必须拥有和外部layout同名的variable属性: -
//在name.xml和contact.xml中必须声明user变量 <?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> <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>
- Layout中@{ }使用的表达式
在@{ }绑定语句中可以使用类似在Java 表达式中使用一些语法,例如:
计算表达式 + - / * %
字符串连接符 +
逻辑运算符 && ||
二进制运算符 & | ^
一元运算符 + - ! ~
位移运算符 >> >>> <<
比较运算符 == > < >= <=
判断类型符 instanceof
分组 Grouping ()
字面量符号 Literals - character, String, numeric, null
强制类型转换 Cast
方法调用 Method calls
访问属性 Field access
使用数组 Array access []
三元运算符 Ternary operator ?:
例如: -
android:text="@{String.valueOf(index + 1)}" android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}" android:transitionName='@{"image_" + id}'
注意:当在@{ } 表达式使用双引用后,在外部可以使用单引用代替双引号,例如’@{“image_” + id}’。
- 空合并操作符 ??
空合并操作符?? 是新增的运算符,类似三元运算符? : ; ,如果参数是null,就使用左边的表达式,如果不是null,就使用右边的表达式: -
android:text="@{user.displayName ?? user.lastName}" android:text="@{user.displayName != null ? user.displayName : user.lastName}" //两者是等价的
- 避免空指针
自动生成的Binding类代码会自动检查变量是否为空以避免发生空指针异常。例如,在表达式@{user.name}中,如果user变量我们没有初始化,那么user就是Null,user.name的值就会被赋予默认值null。基本数据类型同样会被赋予默认值。 - layout布局文件中使用集合
DataBinding框架支持Java中的集合类型,例如数组,List,SparseList和Map,可以使用[ ]操作符来使用它们。 -
<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']}"
- 访问资源
在layout布局文件元素属性值中可以使用正常的访问资源的表达式语法: -
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