多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
android引入MVVM框架时间还不长,目前还很少有应用到app中的。但它是比较新的技术,使用它来搭建项目能省很多代码,而且能使用代码架构比较清晰。本篇文章是我在学习MVVM时翻译的,篇幅比较长,先翻译前半部分。 这篇文档解析如何使用数据绑定库来写响应式布局并减少用来绑定应用程序和布局之间冗余代码,使用逻辑层和布局分离。 数据绑定库提供了即灵活又全面的兼容性——它的支持库.so可以用在android2.1平台(API level 7+)。 使用MVVM需要Gradle1.5.0-alphal或更高版本的插件。 ### 一、测试版 请注意,数据绑定库是一个测试版。虽然数据绑定是处于测试阶段,开发人员应该注意以下事项: *目前它只是一个测试版,可能不适合你的用例,我们需要你的反馈。 *数据绑定库测试版有重大的改变,包括那些没有源代码与应用程序不兼容,也就是说,可能以后需要进行更改。 *开发人员应该随时发布应用程序构建与数据绑定库测试版,它与Android SDK和谷歌的服务条款适用,建议经常采用新库或工具来彻底测试自己的应用程序。 ### 二、搭建环境 首先你需要在Android SDK manager中下载支持库。 配置您的应用程序,在你的module中的build.gradle文件中添加dataBinding元素。 使用下面的代码片段来配置数据绑定: ~~~ android {     ....     dataBinding {         enabled = true     } } ~~~ 如果你的app使用到的库使用到数据绑定,那你的app也需要在build.gradle文件中进行配置。 另外,确保您正在使用一个兼容的版本的Android工作室。Android Studio 1.3或更新的版本支持数据绑定。 ### 数据绑定的布局文件 ### 编写你的第一个数据绑定布局 数据绑定布局文件略有不同,它以layout作为布局的起点,,后跟一个data标签和一个view元素。这个view元素是普通不使用数据绑定布局的根元素。一个示例文件是这样的: ~~~ <?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中使用的variable表示了一个可能会在这个布局中作用的属性。 ~~~ <variable name="user" type="com.example.User"/> ~~~ 布局属性的设置使用“@ { }”语法,这里TextView的文字属性就设置为user中的firstName属性。 ~~~ <TextView android:layout_width="wrap_content"           android:layout_height="wrap_content"           android:text="@{user.firstName}"/> ~~~ # ### 对象 让我们假设现在有一个User的普通java对象(POJO): ~~~ public class User { public final String firstName; public final String lastName; public User(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } } ~~~ 这种类型的对象数据不会改变。通常在应用程序的数据读取一次,永远不会改变。它还可以改为javabean对象: ~~~ 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; } } ~~~ 从数据绑定的角度来看,这两个类是等价的。用于设置TextView的android:文本的表达式@{user.firstName}将访问第一个类中的firstName字段和后一个类的getFirstName()方法。另外,如果firstName()方法存在,它还将访问firstName()方法。 ### 绑定数据 默认情况下,绑定类的名称是基于布局文件的名称起的,它是将布局文件名开头大写并加上“Binding”而成。上述布局文件名称为main_activity.xml,那它的绑定类名为MainActivityBinding。这个类拥有所有从属性(例如用户变量)到布局的绑定关系并知道如何赋值绑定表达式。最简单的方法创建绑定的方法就是通过反射: ~~~ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity); User user = new User("Test", "User"); binding.setUser(user); } ~~~ 完成了!运行应用程序,你会看到在UI上看到“Test User”。另外,你也可以通过以下代码来获取view: ~~~ MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater()); ~~~ 如果你想在 ListView或者RecyclerView 使用数据绑定,你应该这样使用: ~~~ ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false); //or ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false); ~~~ 绑定事件 事件可以直接绑定到处理程序方法,类似于android:onClick可以分配给一个Activity的方法。事件属性名称是listener方法的名称有一部分。例如,[View.OnLongClickListener](http://developer.android.com/reference/android/view/View.OnLongClickListener.html)有一个onLongClick()方法,所以这个事件的属性名应写为android:onLongClick。 分配一个事件给handler,使用方法名称来作为正常绑定表达式的变量,例如,如果您的数据对象有两个方法: ~~~ public class MyHandlers { public void onClickFriend(View view) { ... } public void onClickEnemy(View view) { ... } } ~~~ 绑定表达式会为View 分配一个click监听事件。 ~~~ <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="handlers" type="com.example.Handlers"/> <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}" android:onClick="@{user.isFriend ? handlers.onClickFriend : handlers.onClickEnemy}"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.lastName}" android:onClick="@{user.isFriend ? handlers.onClickFriend : handlers.onClickEnemy}"/> </LinearLayout> </layout> ~~~ ### 三、布局细节 ### Imports 零个或多个导入元素可以使用内部数据元素。这些允许简单引用类内部布局文件,就像在Java。 ~~~ <data> <import type="android.view.View"/> </data> ~~~ 现在,view可以使用在你的绑定表达式: ~~~ <TextView android:text="@{user.lastName}" android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/> ~~~ 当有类名称冲突,其中一个类可能用alias进行重命名: ~~~ <import type="android.view.View"/> <import type="com.example.real.estate.View" alias="Vista"/> ~~~ 现在,Vista可以使用 com.example.real.estate.view中的引用并且View可用于代表android.view.view。导入类型可以在变量和表达式中作为类型引用: ~~~ <data> <import type="com.example.User"/> <import type="java.util.List"/> <variable name="user" type="User"/> <variable name="userList" type="List<User>"/> </data> ~~~ 注意:Android studio还没处理好import的自动导入,因此你的IDE可能无法自动导入变量,你可以在变量定义使用完全限定名称,这样你的应用程序仍可以正常编译。 ~~~ <TextView android:text="@{((User)(user.connection)).lastName}" android:layout_width="wrap_content" android:layout_height="wrap_content"/> ~~~ 导入类型时也可以使用在引用静态字段和方法中: ~~~ <data> <import type="com.example.MyStringUtils"/> <variable name="user" type="com.example.User"/> </data> … <TextView android:text="@{MyStringUtils.capitalize(user.lastName)}" android:layout_width="wrap_content" android:layout_height="wrap_content"/> ~~~ 就像在java中,java.lang.*会自动导入一样。 ### 变量 数据元素可以使用任意数量的变量元素。每个变量元素描述一个属性,它都必须在layout中通过绑定表达式来设置值。 ~~~ <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> ~~~ 变量在编译时进行类型检查,如果一个变量实现了Observable接口或者它是一个observable集合,那应该反射得到。如果变量是一个没有实现Observable的基类或接口,,那变量将无法被观察到! 当不同的布局文件中的变量配置不相同(如landscape或者protrait),这些变量将被组合起来。在这些布局文件之间定义变量不会有冲突。 生成的绑定类中,对应每个变量都有一个setter和getter方法。变量将设置为默认的Java值直到setter被调用,如引用类型默认为null,int默认值是0,boolean默认为false等。 ### Custom Binding Class Names 默认情况下,生成绑定类名称是基于布局文件的名称的,把布局文件名称开头用大写,删除下划线(_),最后加上”Binding”。这个类将被放置在module的一个databinding包模块下。例如,contact_item.xml将产生一个类名ContactItemBinding的类。如果module包名为com.example.my。那么它将会被放置在com.example.my.app.databinding这个包下。 通过调整数据元素的class属性可以将绑定类重命名或放置在不同的包。例如: ~~~ <data class="ContactItem"> ... </data> ~~~ 这个生成绑定类ContactItem将会存在module包下的databinding包中。如果想让生成的类存放在module中的另一个包下,它加入前缀”.”: ~~~ <data class=".ContactItem"> ... </data> ~~~ 这种情况下,ContactItem将被直接放置在module包下,如果想放在其它包可以使用包名的全称: ~~~ <data class="com.example.ContactItem"> ... </data> ~~~ ### Includes 变量可以通过一个included元素导入到布局文件中,included中要有应用程序名称空间和属性的变量名: ~~~ ?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> ~~~ ### 在这里,name.xml和contact.xml中都必须要有user变量。 数据绑定不支持直接在merge元素里添加include并以此来作为子view, ~~~ <pre name="code" class="html"><span style="font-weight: normal;"><?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> <merge> <include layout="@layout/name" bind:user="@{user}"/> <include layout="@layout/contact" bind:user="@{user}"/> </merge> </layout></span> ~~~ ### 表达式语法 ### 共同特性 表达式语法跟java语法很像,下面的一样的语法: · 数学计算 + - / * % · 字符串连接 + · 逻辑运算符&& || · 位运算& | ^ · 一元运算 + - ! ~ · 位移>> >>> << · 比较== > < >= <= · instanceof · Grouping () · 文字 - character, String, numeric, null · Cast · 方法调用 · 字段访问 · 数组访问 [ ] · 三元运算符?: 例如: ~~~ android:text="@{String.valueOf(index + 1)}" android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}" android:transitionName='@{"image_" + id}' ~~~ ### 不具备的方法 有一些操作只能在Java表达式中使用。 · this · super · new · Explicit generic invocation #### 空联合操作 联合操作符(??)不为空的话选择左边操作,为空的话选择右边操作。 ~~~ android:text="@{user.displayName ?? user.lastName}" ~~~ 它等同于: ~~~ android:text="@{user.displayName != null ? user.displayName : user.lastName}" ~~~ #### 避免指针异常 生成数据绑定代码会自动检查null,避免空指针异常。例如,在表达式@ { user.name },如果用户是null,user.name分配其默认值(null)。如果是@ { user.age }。age是int,那么它将默认值为0。 #### Collections 集合共同点:数组,list,sparse list和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"]}" ~~~ ### 资源文件 可以使用正常的访问资源的表达式语法:  ~~~ android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}" ~~~ 格式字符串和复数可以通过提供参数来定义: ~~~ android:text="@{@string/nameFormat(firstName, lastName)}" android:text="@{@plurals/banana(bananaCount)}" ~~~ 当一个复数多个参数,所有参数都应该通过: ~~~ Have an orange Have %d oranges android:text="@{@plurals/orange(orangeCount, orangeCount)}" ~~~ 一些资源需要显式类型的评估:.  <table class=" "><tbody><tr><td valign="top" style="background:rgb(153,153,153)"><p><span style="color:rgb(255,255,255)">Type</span></p></td><td valign="top" style="background:rgb(153,153,153)"><p><span style="color:rgb(255,255,255)">Normal Reference</span></p></td><td valign="top" style="background:rgb(153,153,153)"><p><span style="color:rgb(255,255,255)">Expression Reference</span></p></td></tr><tr><td valign="top" style="background:rgb(247,247,247)"><p>String[]</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@array</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@stringArray</p></td></tr><tr><td valign="top" style="background:rgb(247,247,247)"><p>int[]</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@array</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@intArray</p></td></tr><tr><td valign="top" style="background:rgb(247,247,247)"><p>TypedArray</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@array</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@typedArray</p></td></tr><tr><td valign="top" style="background:rgb(247,247,247)"><p>Animator</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@animator</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@animator</p></td></tr><tr><td valign="top" style="background:rgb(247,247,247)"><p>StateListAnimator</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@animator</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@stateListAnimator</p></td></tr><tr><td valign="top" style="background:rgb(247,247,247)"><p>color <span style="color:rgb(0,102,0)">int</span></p></td><td valign="top" style="background:rgb(247,247,247)"><p>@color</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@color</p></td></tr><tr><td valign="top" style="background:rgb(247,247,247)"><p>ColorStateList</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@color</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@colorStateList</p></td></tr></tbody></table>