安卓Jetpack从两眼一抹黑到两眼一黑
安卓Jetpack从两眼一抹黑到两眼一黑
写的太烂,慢慢改只能
依赖
1 | dependencies { |
其他参见 https://developer.android.com/topic/libraries/architecture/adding-components?hl=zh-cn#lifecycle
LifeCycle
监听Activity/Fragment的生命周期
以前我们经常在Activity的onDestroy里去释放一些资源。如果业务逻辑一多,那么对应的onDestroy里的代码也会很多,难以维护。而有了Lifecycle,我们可以使用Lifecycle去解耦这些逻辑代码,使Activity跟Fragment更加的干净。 下面是一个简单的监听Activity声明周期的例子:
有很多教程使用的是注解如
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
,已废弃。
1 | class SampleActivity : AppCompatActivity() { |
Lifecycle
是一个类,用于存储有关组件(如 Activity 或 Fragment)的生命周期状态的信息,并允许其他对象观察此状态。
Lifecycle
使用两种主要枚举跟踪其关联组件的生命周期状态:
LifecycleOwner
LifecycleOwner
是单一方法接口,表示类具有 Lifecycle
。它具有一种方法(即 getLifecycle()
),该方法必须由类实现。如果尝试管理整个应用进程的生命周期,参阅 ProcessLifecycleOwner
。
LifecycleOwner
是一个单一方法接口,表示该类具有生命周期,它只有一个方法getLifecycle()
。它主要用于独立的Activity/Fragment
感知场景,Lifecycle.Event
将跟随UI调度。 而ProcessLifecycleOwner
是LifecycleOwner
接口的扩展类,它可以将LifecycleOwner
视为所有活动的组合,用于整个应用的生命周期感知。所以Lifecycle.Event.ON_CREATE
将调度一次并且Lifecycle.Event.ON_DESTROY
永远不会被调度。
此接口从各个类(如 Fragment
和 AppCompatActivity
)抽象化 Lifecycle
的所有权,并允许编写与这些类搭配使用的组件。任何自定义应用类均可实现 LifecycleOwner
接口。
实现 DefaultLifecycleObserver
的组件可与实现 LifecycleOwner
的组件完美配合,因为所有者可以提供生命周期,而观察者可以注册以观察生命周期。
对于此,google codelab给出了入下java代码:
1 | import androidx.annotation.NonNull; |
1 | import android.Manifest; |
ViewModel
ViewModel 是Android 架构组件中负责管理UI相关数据与逻辑的,它的功能定义与MVP架构中的Persenter十分相似,配合其他组件使用增加许多方便开发的功能。
如果系统销毁或重新创建界面控制器,则存储在其中的任何瞬态界面相关数据都会丢失。例如,应用的某个 Activity 中可能包含用户列表。因配置更改而重新创建 activity 后,新 activity 必须重新提取用户列表。对于简单的数据,activity 可以使用 onSaveInstanceState()
方法从 onCreate()
中的捆绑包恢复其数据,但此方法仅适合可以序列化再反序列化的少量数据,而不适合数量可能较大的数据,如用户列表或位图。
另一个问题是,界面控制器经常需要进行可能需要一些时间才能返回的异步调用。界面控制器需要管理这些调用,并确保系统在其销毁后清理这些调用以避免潜在的内存泄漏。此项管理需要大量的维护工作,并且在为配置更改重新创建对象的情况下,会造成资源的浪费,因为对象可能需要重新发出已经发出过的调用。
诸如 activity 和 fragment 之类的界面控制器主要用于显示界面数据、对用户操作做出响应或处理操作系统通信(如权限请求)。如果要求界面控制器也负责从数据库或网络加载数据,那么会使类越发膨胀。为界面控制器分配过多的责任可能会导致单个类尝试自己处理应用的所有工作,而不是将工作委托给其他类。以这种方式为界面控制器分配过多的责任也会大大增加测试的难度。
从界面控制器逻辑中分离出视图数据所有权的做法更易行且更高效。
使用
1 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1' |
定义一个类继承ViewModel,ViewModel主要负责为界面准备数据,所以我们一般会在ViewModel里使用LiveData去承载数据。
1 | class MyViewModel : ViewModel() { |
Activity内的访问:
1 | class MyActivity : AppCompatActivity() { |
注意:
ViewModel
绝不能引用视图、Lifecycle
或可能存储对 Activity 上下文的引用的任何类。因为ViewModel生命周期更长。
ViewModel 的生命周期
在 Fragment 之间共享数据
Activity 中的两个或更多 Fragment 需要相互通信是一种很常见的现象,可以使用 ViewModel
对象解决这一常见的难点。这两个 fragment 可以使用其 activity 范围共享 ViewModel
来处理此类通信,如以下示例代码所示:
1 | //被共用的ViewModel |
请注意,这两个 Fragment 都会检索包含它们的 Activity。这样,当这两个 Fragment 各自获取 ViewModelProvider
时,它们会收到相同的 SharedViewModel
实例(其范围限定为该 Activity)。
此方法具有以下优势:
- Activity 不需要执行任何操作,也不需要对此通信有任何了解。
- 除了
SharedViewModel
约定之外,Fragment 不需要相互了解。如果其中一个 Fragment 消失,另一个 Fragment 将继续照常工作。 - 每个 fragment 都有自己的生命周期,而不受另一个 fragment 的生命周期的影响。如果一个 fragment 替换另一个 fragment,界面将继续工作而没有任何问题。
AndroidViewModel中慎重进行R.string.xxx
还没碰见过,以后有机会试试
先上一段代码:
1 | public class MyViewModel extends AndroidViewModel { |
这种用法的问题在于,ViewModel在配置更新的时候,并不会销毁重建因此构造函数不会重走。
因此如果此时需要动态替换R.string.labelString
,那么这种情况下是不正确的。
因此在ViewModel里动态加载string,是有坑的,需要慎重。
ViewModel+协程:https://developer.android.com/topic/libraries/architecture/coroutines?hl=zh-cn
LiveData
核心:观察者模式。
LiveData
是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循其他应用组件(如 activity、fragment 或 service)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。
如果观察者(由 Observer
类表示)的生命周期处于 STARTED
或 RESUMED
状态,则 LiveData 会认为该观察者处于活跃状态。LiveData 只会将更新通知给活跃的观察者。为观察 LiveData
对象而注册的非活跃观察者不会收到更改通知。
您可以注册与实现 LifecycleOwner
接口的对象配对的观察者。有了这种关系,当相应的 Lifecycle
对象的状态变为 DESTROYED
时,便可移除此观察者。这对于 activity 和 fragment 特别有用,因为它们可以放心地观察 LiveData
对象,而不必担心泄露(当 activity 和 fragment 的生命周期被销毁时,系统会立即退订它们)。
使用
创建 LiveData 对象
LiveData 是一种可用于任何数据的封装容器,其中包括可实现 Collections
的对象,如 List
。LiveData
对象通常存储在 ViewModel
对象中,并可通过 getter 方法进行访问,如以下示例中所示:
1 | class NameViewModel : ViewModel() { |
请确保用于更新界面的
LiveData
对象存储在ViewModel
对象中,而不是将其存储在 activity 或 fragment 中,原因如下:
- 避免 Activity 和 Fragment 过于庞大。现在,这些界面控制器负责显示数据,但不负责存储数据状态。
- 将
LiveData
实例与特定的 Activity 或 Fragment 实例分离开,并使LiveData
对象在配置更改后继续存在。
观察 LiveData 对象
在大多数情况下,应用组件的 onCreate()
方法是开始观察 LiveData
对象的正确着手点,原因如下:
- 确保系统不会从 Activity 或 Fragment 的
onResume()
方法进行冗余调用。 - 确保 activity 或 fragment 变为活跃状态后具有可以立即显示的数据。一旦应用组件处于
STARTED
状态,就会从它正在观察的LiveData
对象接收最新值。只有在设置了要观察的LiveData
对象时,才会发生这种情况。
通常,LiveData 仅在数据发生更改时才发送更新,并且仅发送给活跃观察者。此行为的一种例外情况是,观察者从非活跃状态更改为活跃状态时也会收到更新。此外,如果观察者第二次从非活跃状态更改为活跃状态,则只有在自上次变为活跃状态以来值发生了更改时,它才会收到更新。
以下示例代码说明了如何开始观察 LiveData
对象:
1 | class NameActivity : AppCompatActivity() { |
在传递 nameObserver
参数的情况下调用 [observe()
](https://developer.android.com/reference/androidx/lifecycle/LiveData?hl=zh-cn#observe(android.arch.lifecycle.LifecycleOwner, android.arch.lifecycle.Observer)) 后,系统会立即调用 onChanged()
。如果 LiveData
对象尚未设置值,系统不会调用 onChanged()
。
必须调用
setValue(T)
方法以从主线程更新LiveData
对象。如果在工作器线程中执行代码,您可以改用postValue(T)
方法来更新LiveData
对象。官网如此写。…
不过我的Android Studio似乎更推荐我使用
viewModel.value = yourValue
这种直接属性访问的形式,有点怪。可能是跟kt有关吧
Transformations
您可能希望在将 LiveData
对象分派给观察者之前对存储在其中的值进行更改,或者您可能需要根据另一个实例的值返回不同的 LiveData
实例。Lifecycle
软件包会提供 Transformations
类,该类包括可应对这些情况的辅助程序方法。
这个类提供了三个静态方法:map、switchMap和distinctUntilChanged,这些方法将在下面解释。
map
将LiveData的值转换为另一个值。下面是一个简单的例子。
1 | val userLiveData: LiveData<User> = UserLiveData() |
switchMap
将一个LiveData转换为另一个LiveData。与 map()
类似,对存储在 LiveData
对象中的值应用函数,并将结果解封和分派到下游。传递给 switchMap()
的函数必须返回 LiveData
对象
1 | private fun getUser(id: String): LiveData<User> { |
distinctUntilChanged
官网并没有给出这个方法的示例
对LiveData进行过滤,除非数值发生了变化,否则不会被检索出来。很多时候,我们可能会收到一个不包含任何相关变化的通知。如果我们监听的是所有球员的名字,我们不想在分数不发生变化时更新用户界面。这就是distinctUntilChanged方法的用处。
1 | val players: LiveData<List<Player>> = ... |
一些拓展
livedata-ktx extensions for Transformations
上述所有的Transformations类函数也可以作为LiveData的扩展函数,使用下面的依赖。
1 androidx.lifecycle:lifecycle-livedata-ktx:<version>有了它,例如,你可以把上面的例子改写成下面这样。
1
2
3
4
5 val players: LiveData<List<Player>> = ...
val playerNames: LiveData<List<String>> = players.map {
it.map { player -> player.name }
}.distinctUntilChanged()Behind the scenes of the Transformations class
我们刚刚涵盖了3个简单的转换,你实际上可以自己写。所有这些都是使用MediatorLiveData类编写的。MediatorLiveData类是我在处理LiveData时使用最多的类(尽管我在有意义的时候使用map / switchMap / distinctUntilChanged)。
为了给你一个例子,说明你什么时候应该创建你自己的MediatorLiveData类,看看这段代码。
1
2
3
4
5
6
7
8
9
10
11 val players: LiveData<List<Player>> = ...
val dbGame: LiveData<GameEntity> = ...
val game: LiveData<Game> =
Transformations.map(dbGame) { game ->
val players = this.players.value // Getting current players here may be unsafe
Game(players = game.playerIds.mapNotNull { playerId ->
players?.find { it.id == playerId }
})
}通过只映射dbGame的变化,我在Player更新时取了玩家的当前值(
this.player.value
)。所以,当Player被更新时,我并没有更新Game。为了解决这个问题,我应该使用MediatorLiveData来合并Player和Game,如果他们中的任何一个被更新。这将看起来像这样。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 val players: LiveData<List<Player>> = ...
val dbGame: LiveData<GameEntity> = ...
val game: LiveData<Game> = MediatorLiveData<Game>()
.apply {
fun update() {
val players = players.value ?: return
val game = dbGame.value ?: return
value = Game(players = game.playerIds
.mapNotNull { playerId ->
players?.find { it.id == playerId }
}
)
}
addSource(players) { update() }
addSource(dbGame) { update() }
update()
}有了这个解决方案,每当球员或dbGame更新时,我都会得到Game更新。
MediatorLiveData
MediatorLiveData可以转换、过滤和合并其他LiveData实例。每当我创建MediatorLiveData时,我倾向于遵循同样的模式,它看起来像这样。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 val a = MutableLiveData<Int>(40)
val b = MutableLiveData<Int>(2)
val sum: LiveData<Int> = MediatorLiveData<Int>().apply {
fun update() {
// OPTION 3
val aVal = a.value ?: return
val bVal = b.value ?: return
// OPTION 4
value = aVal + bVal
}
// OPTION 1
addSource(a) { update() }
addSource(b) { update() }
// OPTION 2
update()
}在这个例子中,我正在观察两个LiveData源(a和b)。我在调解器创建时调用了更新函数,只有在两个源都是非空的情况下才会发出一个值。这种模式非常通用,但让我们一个一个地走完每一步。
方案1
在从这个LiveData发出任何东西之前,你想监控哪些源的变化。这可以只是一个单一的源(或更多),但没有固定的上限。(即让你对单个LiveData进行条件映射或合并多个LiveDatas)
方案2
如果你想在创建MediatorLiveData时设置一个初始值,在这里调用内部更新函数。为了简单起见,我通常调用我的更新函数,但只是设置MediatorLiveData的值/postValue也可以。在某些情况下,我不想发出一个初始值,因为我希望在a或b还没有设置的情况下发出空值。那么我就跳过在这里调用更新或设置初始值。
方案3
因为只要a或b发出更新,就会调用update,我们必须期望a和b为空。有时你实际上想更新你的MediatorLiveData,即使一个或多个来源目前是空的,但这是一个很好的方法,在从MediatorLiveData发出新值之前,确保局部变量aVal和bVal不是空的。你甚至可以在这里应用更多的验证/过滤,以减少你所创建的最终MediatorLiveData的排放。
方案4
由于MediatorLiveData是一个LiveData实例,我们可以设置值(像上面的例子)或调用postValue(如果由于某种原因,你在发射值时不在主线程上)。这也是你决定如何转换源数据值的地方。上面的例子只是将aVal和bVal相加,但你当然可以在这里应用你想要的任何转换。
结论
在所有的LiveData转换中使用map、switchMap和distinctUntilChanged。除非有必要,否则应避免编写自己的转换,并尝试结合操作来创建更复杂的转换。
使用distinctUntilChanged来避免发出相同的数据,否则将导致不必要的UI更新。
如果你发现自己在地图/switchMap内或观察块内使用.value属性获得另一个LiveData的当前值,你应该考虑创建一个MediatorLiveData来正确合并来源。
DataBinding
布局通常是使用调用界面框架方法的代码在 Activity 中定义的。例如,以下代码调用 findViewById()
来查找 TextView
并将其绑定到 viewModel
变量的 userName
属性:
1 | findViewById<TextView>(R.id.sample_text).apply { |
以下示例展示了如何在布局文件中使用数据绑定库将文本直接分配到组件。这样就无需调用上述任何代码。请注意赋值表达式中 @{}
语法的使用:
1 | <TextView android:text="@{viewmodel.userName}" /> |
借助布局文件中的绑定组件,您可以移除 Activity 中的许多界面框架调用,使其维护起来更简单、方便。还可以提高应用性能,并且有助于防止内存泄漏以及避免发生 Null 指针异常。
在许多情况下,视图绑定可简化实现,提高性能,提供与数据绑定相同的好处。如果您使用数据绑定的主要目的是取代
findViewById()
调用,请考虑改用视图绑定。
使用入门
在build.gradle
中开启功能
1 | android { |
数据类:
1 | data class User(val firstName: String, val lastName: String) |
布局和绑定表达式
数据绑定布局文件以根标记 layout
开头,后跟 data
元素和 view
根元素。此视图元素是非绑定布局文件中的根。以下代码展示了示例布局文件:
1 |
|
data
中的 user
变量描述了可在此布局中使用的属性。
1 | <variable name="user" type="com.example.User" /> |
布局中的表达式使用“@{}
”语法写入特性属性中。在这里,TextView
文本被设置为 user
变量的 firstName
属性:
1 | <TextView android:layout_width="wrap_content" |
注意:布局表达式应保持精简,因为它们无法进行单元测试,并且拥有的 IDE 支持也有限。为了简化布局表达式,可以使用自定义绑定适配器。(或见下文)
绑定数据
系统会为每个布局文件生成一个绑定类。默认情况下,类名称基于布局文件的名称,它会转换为 Pascal 大小写形式并在末尾添加 Binding 后缀。以上布局文件名为 activity_main.xml
,因此生成的对应类为 ActivityMainBinding
。此类包含从布局属性(例如,user
变量)到布局视图的所有绑定,并且知道如何为绑定表达式指定值。建议的绑定创建方法是在扩充布局时创建,如以下示例所示:
1 | override fun onCreate(savedInstanceState: Bundle?) { |
如果要在 Fragment
、ListView
或 RecyclerView
适配器中使用数据绑定项,您可能更愿意使用绑定类或 DataBindingUtil
类的 [inflate()
](https://developer.android.com/reference/androidx/databinding/DataBindingUtil?hl=zh-cn#inflate(android.view.LayoutInflater, int, android.view.ViewGroup, boolean, android.databinding.DataBindingComponent)) 方法,如以下代码示例所示:
1 | //更喜欢这种,理由同上 |
常见的布局表达式语言
可以在表达式语言中使用以下运算符和关键字:
- 算术运算符
+ - / * %
- 字符串连接运算符
+
- 逻辑运算符
&& ||
- 二元运算符
& | ^
- 一元运算符
+ - ! ~
- 移位运算符
>> >>> <<
- 比较运算符
== > < >= <=
(请注意,<
需要转义为<
) instanceof
- 分组运算符
()
- 字面量运算符 - 字符、字符串、数字、
null
- 类型转换
- 方法调用
- 字段访问
- 数组访问
[]
- 三元运算符
?:
示例:
1 | android:text="@{String.valueOf(index + 1)}" |
Null 合并运算符
如果左边运算数不是 null
,则 Null 合并运算符 (??
) 选择左边运算数,如果左边运算数为 null
,则选择右边运算数。
1 | android:text="@{user.displayName ?? user.lastName}" |
视图引用
表达式可以通过以下语法按 ID 引用布局中的其他视图:
1 | android:text="@{exampleText.text}" |
绑定类将 ID 转换为驼峰式大小写。
在以下示例中,TextView
视图引用同一布局中的 EditText
视图:
1 | <EditText |
集合
为方便起见,可使用 []
运算符访问常见集合,例如数组、列表、稀疏列表和映射。
1 | <data> |
注意:要使 XML 不含语法错误,必须转义
<
字符。例如:不要写成List<String>
形式,而是必须写成List<String>
。
还可以使用 object.key
表示法在映射中引用值。例如,以上示例中的 @{map[key]}
可替换为 @{map.key}
。
资源
表达式可以使用以下语法引用应用资源:
1 | android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}" |
事件处理
方法引用
事件可以直接绑定到处理脚本方法,类似于为 Activity 中的方法指定 android:onClick
的方式。与 View
onClick
特性相比,一个主要优点是表达式在编译时进行处理,因此,如果该方法不存在或其签名不正确,则会收到编译时错误。
方法引用和监听器绑定之间的主要区别在于实际监听器实现是在绑定数据时创建的,而不是在事件触发时创建的。如果您希望在事件发生时对表达式求值,则应使用监听器绑定。
要将事件分配给其处理脚本,请使用常规绑定表达式,并以要调用的方法名称作为值。例如,请考虑以下布局数据对象示例:
1 | class MyHandlers { |
绑定表达式可将视图的点击监听器分配给 onClickFriend()
方法,如下所示:
1 |
|
注意:表达式中的方法签名必须与监听器对象中的方法签名完全一致。
监听器绑定
监听器绑定是在事件发生时运行的绑定表达式。它们类似于方法引用,但允许您运行任意数据绑定表达式。此功能适用于 Gradle 2.0 版及更高版本的 Android Gradle 插件。
在方法引用中,方法的参数必须与事件监听器的参数匹配。在监听器绑定中,只有您的返回值必须与监听器的预期返回值相匹配(预期返回值无效除外)。例如,请参考以下具有 onSaveClick()
方法的 presenter 类:
1 | class Presenter { |
然后,您可以将点击事件绑定到 onSaveClick()
方法,如下所示:
1 |
|
在表达式中使用回调时,数据绑定会自动为事件创建并注册必要的监听器。当视图触发事件时,数据绑定会对给定表达式求值。与常规绑定表达式一样,在对这些监听器表达式求值时,仍会获得数据绑定的 Null 值和线程安全。
在上面的示例中,我们尚未定义传递给 onClick(View)
的 view
参数。监听器绑定提供两个监听器参数选项:您可以忽略方法的所有参数,也可以命名所有参数。如果您想命名参数,则可以在表达式中使用这些参数。例如,上面的表达式可以写成如下形式:
1 | android:onClick="@{(view) -> presenter.onSaveClick(task)}" |
或者,如果您想在表达式中使用参数,则采用如下形式:
1 | class Presenter { |
您可以在 lambda 表达式中使用多个参数:
1 | class Presenter { |
如果您监听的事件返回类型不是 void
的值,则表达式也必须返回相同类型的值。例如,如果要监听长按事件,表达式应返回一个布尔值。
1 | class Presenter { |
如果由于 null
对象而无法对表达式求值,则数据绑定将返回该类型的默认值。例如,引用类型返回 null
,int
返回 0
,boolean
返回 false
,等等。
如果您需要将表达式与谓词(例如,三元运算符)结合使用,则可以使用 void
作为符号。
1 | android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}" # void相当于doNothing |
避免使用复杂的监听器
监听器表达式功能非常强大,可以使您的代码非常易于阅读。另一方面,包含复杂表达式的监听器会使您的布局难以阅读和维护。这些表达式应该像将可用数据从界面传递到回调方法一样简单。您应该在从监听器表达式调用的回调方法中实现任何业务逻辑。
导入、变量和包含
导入
通过导入功能,您可以轻松地在布局文件中引用类,就像在托管代码中一样。您可以在 data
元素使用多个 import
元素,也可以不使用。以下代码示例将 View
类导入到布局文件中:
1 | <data> |
导入 View
类可让您通过绑定表达式引用该类。以下示例展示了如何引用 View
类的 VISIBLE
和 GONE
常量:
1 | <TextView |
类型别名
当类名有冲突时,其中一个类可使用别名重命名。以下示例将 com.example.real.estate
软件包中的 View
类重命名为 Vista
:
1 | <import type="android.view.View"/> |
您可以在布局文件中使用 Vista
引用 com.example.real.estate.View
,使用 View
引用 android.view.View
。
导入其他类
导入的类型可用作变量和表达式中的类型引用。以下示例显示了用作变量类型的 User
和 List
:
1 | <data> |
注意:Android Studio 尚不处理导入,因此导入变量的自动填充功能可能无法在您的 IDE 中使用。您的应用仍可以编译,并且您可以通过在变量定义中使用完全限定名称来解决这个 IDE 问题。
您还可以使用导入的类型来对表达式的一部分进行类型转换。以下示例将 connection
属性强制转换为类型 User
:
1 | <TextView |
在表达式中引用静态字段和方法时,也可以使用导入的类型。以下代码会导入 MyStringUtils
类,并引用其 capitalize
方法:
1 | <data> |
就像在托管代码中一样,系统会自动导入 java.lang.*
。
变量
您可以在 data
元素中使用多个 variable
元素。每个 variable
元素都描述了一个可以在布局上设置、并将在布局文件中的绑定表达式中使用的属性。以下示例声明了 user
、image
和 note
变量:
1 | <data> |
变量类型在编译时进行检查,因此,如果变量实现 Observable
或者是可观察集合,则应反映在类型中。如果该变量是不实现 Observable
接口的基类或接口,则变量是“不可观察的”。
如果不同配置(例如横向或纵向)有不同的布局文件,则变量会合并在一起。这些布局文件之间不得存在有冲突的变量定义。
在生成的绑定类中,每个描述的变量都有一个对应的 setter 和 getter。在调用 setter 之前,这些变量一直采用默认的托管代码值,例如引用类型采用 null
,int
采用 0
,boolean
采用 false
,等等。
可以在
onCreate
时通过binding
对象为这些变量赋值当然,更推荐使用ViewModel进行管理
系统会根据需要生成名为 context
的特殊变量,用于绑定表达式。context
的值是根视图的 getContext()
方法中的 Context
对象。context
变量会被具有该名称的显式变量声明替换。
包含
通过使用应用命名空间和特性中的变量名称,变量可以从包含的布局传递到被包含布局的绑定。以下示例展示了来自 name.xml
和 contact.xml
布局文件的被包含 user
变量:
1 |
|
数据绑定不支持 include 作为 merge 元素的直接子元素。例如,以下布局不受支持:
1 |
|
可观察的数据对象
可观察字段
当然,可能LiveData更常用
在创建实现 Observable
接口的类时要完成一些操作,但如果您的类只有少数几个属性,这样操作的意义不大。在这种情况下,您可以使用通用 Observable
类和以下特定于基元的类,将字段设为可观察字段:
ObservableBoolean
ObservableByte
ObservableChar
ObservableShort
ObservableInt
ObservableLong
ObservableFloat
ObservableDouble
ObservableParcelable
可观察字段是具有单个字段的自包含可观察对象。原语版本避免在访问操作期间封箱和开箱。如需使用此机制,请采用 Java 编程语言创建 public final
属性,或在 Kotlin 中创建只读属性,如以下示例所示:
1 | class User { |
如需访问字段值,请使用 set()
和 get()
访问器方法,或使用 Kotlin 属性语法:
1 | user.firstName = "Google" |
可观察集合
某些应用使用动态结构来保存数据。可观察集合允许使用键访问这些结构。当key为引用类型(如 String
)时,ObservableArrayMap
类非常有用,如以下示例所示:
1 | ObservableArrayMap<String, Any>().apply { |
在布局中,可使用key-value找到Map,如下所示:
1 | <data> |
当键为整数时,ObservableArrayList
类非常有用,如下所示:
1 | ObservableArrayList<Any>().apply { |
在布局中,可通过索引访问列表,如以下示例所示:
1 | <data> |
可观察对象
实现 Observable
接口的类允许注册监听器,以便它们接收有关可观察对象的属性更改的通知。
Observable
接口具有添加和移除监听器的机制,但何时发送通知必须由您决定。为便于开发,数据绑定库提供了用于实现监听器注册机制的 BaseObservable
类。实现 BaseObservable
的数据类负责在属性更改时发出通知。具体操作过程是向 getter 分配 Bindable
注释,然后在 setter 中调用 notifyPropertyChanged()
方法,如以下示例所示:
1 | class User : BaseObservable() { |
数据绑定在模块包中生成一个名为 BR
的类,该类包含用于数据绑定的资源的 ID。在编译期间,Bindable
注释会在 BR
类文件中生成一个条目。如果数据类的基类无法更改,Observable
接口可以使用 PropertyChangeRegistry
对象实现,以便有效地注册和通知监听器。
使用 LiveData 将数据变化通知给界面
您可以使用 LiveData
对象作为数据绑定来源,自动将数据变化通知给界面。
与实现
Observable
的对象(例如可观察字段)不同,LiveData
对象了解订阅数据更改的观察器的生命周期。了解这一点有许多好处,具体说明请参阅使用 LiveData 的优势。在 Android Studio 版本 3.1 及更高版本中,您可以在数据绑定代码中将可观察字段替换为LiveData
对象。
要将 LiveData
对象与绑定类一起使用,您需要指定生命周期所有者来定义 LiveData
对象的范围。以下示例在绑定类实例化后将 Activity 指定为生命周期所有者:
1 | class ViewModelActivity : AppCompatActivity() { |
您可以根据使用 ViewModel 管理界面相关数据中所述,使用 ViewModel
组件来将数据绑定到布局。在 ViewModel
组件中,您可以使用 LiveData
对象转换数据或合并多个数据源。以下示例展示了如何在 ViewModel
中转换数据:
1 | class ScheduleViewModel : ViewModel() { |
使用 ViewModel 管理界面相关数据
数据绑定库可与 ViewModel
组件无缝协作,这类组件会公开布局观察到并对其变化做出响应的数据。通过将 ViewModel
组件与数据绑定库结合使用,您可以将界面逻辑从布局移出,并移入到这些组件中,以便于测试。数据绑定库确保在需要时将视图与数据源绑定或解绑。大部分的其余工作是为了确保您公开的是正确的数据。有关此架构组件的更多信息,请参阅 ViewModel 概览。
要将 ViewModel
组件与数据绑定库一起使用,必须实例化从 ViewModel
类继承而来的组件,获取绑定类的实例,并将您的 ViewModel
组件分配给绑定类中的属性。以下示例展示了如何将组件与库结合使用:
1 | class ViewModelActivity : AppCompatActivity() { |
在您的布局中,使用绑定表达式将 ViewModel
组件的属性和方法分配给对应的视图,如以下示例所示:
1 | <CheckBox |
使用 Observable ViewModel 更好地控制绑定适配器
LiveData直接跳过
您可以使用实现 Observable
的 ViewModel
组件,向其他应用组件发出数据变化通知,这与使用 LiveData
对象的方式类似。
在某些情况下,您可能更愿意使用实现 Observable
接口的 ViewModel
组件,而不是使用 LiveData
对象,即使这样会失去对 LiveData
的生命周期管理功能也不影响。使用实现 Observable
的 ViewModel
组件可让您更好地控制应用中的绑定适配器。例如,这种模式可让您更好地控制数据更改时发出的通知,您还可以指定自定义方法来设置双向数据绑定中的属性值。
如需实现可观察的 ViewModel
组件,您必须创建一个从 ViewModel
类继承而来并实现 Observable
接口的类。您可以使用 addOnPropertyChangedCallback()
和 removeOnPropertyChangedCallback()
方法提供观察器订阅或取消订阅通知时的自定义逻辑。您还可以在 notifyPropertyChanged()
方法中提供属性更改时运行的自定义逻辑。以下代码示例展示了如何实现一个可观察的 ViewModel
:
1 | /** |
自定义绑定逻辑
这个很重要,建议细看
一些属性需要自定义绑定逻辑。例如,android:paddingLeft
特性没有关联的 setter,而是提供了 setPadding(left, top, right, bottom)
方法。使用 BindingAdapter
注释的静态绑定适配器方法支持自定义特性 setter 的调用方式。
Android 框架类的特性已经创建了 BindingAdapter
注释。例如,以下示例展示了 paddingLeft
属性的绑定适配器:
1 |
|
参数类型非常重要。第一个参数用于确定与特性关联的视图类型,第二个参数用于确定在给定特性的绑定表达式中接受的类型。
绑定适配器对其他类型的自定义很有用。例如,可以通过工作器线程调用自定义加载程序来加载图片。
出现冲突时,您定义的绑定适配器会替换由 Android 框架提供的默认适配器。
您还可以使用接收多个属性的适配器,如以下示例所示:
1 |
|
您可以在布局中使用适配器,如以下示例所示。请注意,@drawable/venueError
引用应用中的资源。使用 @{}
将资源括起来可使其成为有效的绑定表达式。
1 | <ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" /> |
注意:数据绑定库在匹配时会忽略自定义命名空间。
如果 ImageView
对象同时使用了 imageUrl
和 error
,并且 imageUrl
是字符串,error
是 Drawable
,就会调用适配器。如果您希望在设置了任意属性时调用适配器,则可以将适配器的可选 requireAll
标志设置为 false
,如以下示例所示:
1 |
|
注意:出现冲突时,绑定适配器会替换默认的数据绑定适配器。
绑定适配器方法可以选择性在处理程序中使用旧值。同时获取旧值和新值的方法应该先为属性声明所有旧值,然后再声明新值,如以下示例所示:
1 |
|
事件处理脚本只能与具有一种抽象方法的接口或抽象类一起使用,如以下示例所示:
1 |
|
按如下方式在布局中使用此事件处理脚本:
1 | <View android:onLayoutChange="@{() -> handler.layoutChanged()}"/> |
当监听器有多个方法时,必须将它拆分为多个监听器。例如,View.OnAttachStateChangeListener
有两个方法:onViewAttachedToWindow(View)
和 onViewDetachedFromWindow(View)
。该库提供了两个接口,用于区分它们的属性和处理脚本:
1 | // Translation from provided interfaces in Java: |
因为更改一个监听器也会影响另一个监听器,所以需要适用于其中一个属性或同时适用于这两个属性的适配器。您可以在注释中将 requireAll
设置为 false
,以指定并非必须为每个属性都分配绑定表达式,如以下示例所示:
1 |
|
以上示例比一般情况稍微复杂一些,因为 View
类使用 addOnAttachStateChangeListener()
和 removeOnAttachStateChangeListener()
方法,而非 OnAttachStateChangeListener
的 setter 方法。android.databinding.adapters.ListenerUtil
类有助于跟踪以前的监听器,以便在绑定适配器中将它们移除。
通过用 @TargetApi(VERSION_CODES.HONEYCOMB_MR1)
注释接口 OnViewDetachedFromWindow
和 OnViewAttachedToWindow
,数据绑定代码生成器知道只应在运行 Android 3.1(API 级别 12)及更高级别(addOnAttachStateChangeListener()
方法支持的相同版本)时生成监听器。
双向绑定
https://developer.android.com/topic/libraries/data-binding/two-way?hl=zh-cn
DataBinding-Codelab
项目下载与分析
- 下载项目,解压运行
- 这是一个类似于关注用户的demo,点击Like按钮会更新进度条,超过阈值后更新图标。
- 主要代码是
PlainOldActivity
,SimpleViewModel.kt
,plain_activity.xml
PlainOldActivity
类中的 UI 实现存在许多问题:
多次调用
findViewById ()
。这不仅速度慢,而且不安全,因为在编译时没有检查它。如果传递给findViewById ()
的 ID 是错误的,应用程序将在运行时崩溃。在
onCreate()
中设置初始值在 XML 布局声明的 Button 元素中使用
android: onClick
属性,这也是不安全的: 如果onLike()
方法没有在 Activity 中实现(或者被重命名) ,应用程序将在运行时崩溃。代码庞杂。Activities 和fragments 往往增长得非常快,我们应当从中移出尽可能多的代码。此外,Activities 和 fragments 中的代码很难测试和维护。
tip:你可能发现有一行废弃代码:
1 >private val viewModel by lazy { ViewModelProviders.of(this).get(SimpleViewModel::class.java) }替换成
1
2
3 private val viewModel by lazy{
ViewModelProvider(this).get(SimpleViewModel::class.java)
}不过你可能会觉得这样也是可以的
1 >private val viewModel = SimpleViewModel()实则不然,因为ViewModel生命周期长于Activity,这种方式创建viewModel实例很容易导致ViewModel本不应被结束但由于Activity的结束而被迫结束,引发一系列问题
代码迁移
布局代码迁移
打开plain-activity.xml
,我们发现整个布局其实就是一个约束布局。我们要进行迁移,其实就是把根布局换成<layout>
,以及一些其他改动,我们可以将光标移到约束布局,使用快捷键Alt+Enter
,或者右键选择第一行Show Context Actions,然后选 Convert to data binding layout。
代码会变成这个样子:
1 | <layout xmlns:android="http://schemas.android.com/apk/res/android" |
其中ConstraintLayout
内还是原先的布局。增加的<data>
用来存放布局变量,这些变量会在布局表达式中用到,布局中的表达式使用“@{}
”语法写入特性属性中,上面教程提到过。
注意最好避免在视图中嵌套复杂的逻辑。复杂的表达式将使布局更难阅读和维护,上面也提到过。
使用布局表达式
将姓名两个变量添加到<data>
,如下:
1 | <data> |
在约束布局中,给id为plain_name
的TextView
写布局表达式,plain_lastName
的TextView
同理:
1 | <TextView |
改后的结果可以参照plain_activity_solution_2.xml
Activity代码迁移
在PlainOldActivity
,修改onCreate
方法,将setContentView(R.layout.plain_activity)
替换为:
1 | val binding : PlainActivityBinding = |
当然,我更喜欢:
1 | val binding: PlainActivityBinding = PlainActivityBinding.inflate(layoutInflater) |
绑定类由库自动生成。我们可以使用bind
对姓名两个变量进行配置:
1 | binding.name = "Ada" |
然后我们就可以去掉updateName()
方法。
所有代码在 PlainOldActivitySolution2
。
用户事件处理
首先使用ViewModel
替换掉xml的<data>
里的两个变量。换成如下:
1 | <data> |
并且对布局表达式进行修正:
1 | <TextView |
然后我们更新点击Like这个button的事件处理,将android:onClick="onLike"
替换成android:onClick="@{() -> viewmodel.onLike()}"
。下面这种好处是编译时进行检查,能及时发现错误,上面也提到过。
然后将Activity内的姓名变量的初始化改为viewModel的初始化,即:
删掉:
1 | binding.name = "Ada" |
在原位置增加:
1 | binding.viewmodel = viewModel |
可以查看 plain_activity_solution_3.xml
以及 PlainOldActivitySolution3
。
数据监听
如果你运行上一步的代码,你会发现好像没反应(但其实是进行了点击的),我们来解决这个问题。
我们使用LiveData对原VIewModel内的变量进行替换:
1 | private val _name = MutableLiveData("Ada") |
以及替换剩余逻辑为:
1 | // popularity is exposed as LiveData using a Transformation instead of a @Bindable property. |
然后在Activity中设置LifeCycle的Owner并删除原有的updat的方法:
1 | override fun onCreate(savedInstanceState: Bundle?) { |
然后删掉其余的所有私有方法。
可以查看 PlainOldActivitySolution4
。
自定义属性by Binding Adapter
我们希望进度条有如下功能:
- 0人Like的时候不可见
- 5人Like时满进度
- 满进度时更改自身颜色
在utils包中找到 BindingAdapters.kt
,找到hideIfZero(...)
这个函数,我们使用它去实现第一个需求
1 |
|
逻辑还算简单,不过最好了解一下View。另外注解是必不可少的,这标识了此函数为一个BindingAdapter
。
然后在xml里配置:
1 | <ProgressBar |
然后可以运行,如果运行时有如下error/warning属于正常情况:
1 | Application namespace for attribute app:popularityIcon will be ignored. |
当然如果觉得太扎眼,可以直接删除所有@BindingAdapter("app:hideIfZero")
中的app:
。
多参数Binding Adapter
在BindingAdapters
中,你能够找到:
1 | /** |
如果缺少属性,Binding Adapter将不会被调用,这是在编译期实现的。
requireAll
参数为true时,表示所有元素都必须在xml中进行定义。false时可以不定义某(几)个元素,the missing attributes will be null
, false
if booleans, or 0 if primitives。
在xml中添加:
1 | <ProgressBar |
其余的留作了练习,可以在solution中找答案。
Others
绑定适配器
绑定适配器负责发出相应的框架调用来设置值。例如,设置属性值就像调用 setText()
方法一样。再比如,设置事件监听器就像调用 setOnClickListener()
方法。
数据绑定库允许您通过使用适配器指定为设置值而调用的方法、提供您自己的绑定逻辑,以及指定返回对象的类型。
设置特性值
只要绑定值发生更改,生成的绑定类就必须使用绑定表达式在视图上调用 setter 方法。您可以允许数据绑定库自动确定方法、显式声明方法或提供选择方法的自定义逻辑。
自动选择方法
对于名为 example
的特性,库自动尝试查找接受兼容类型作为参数的方法 setExample(arg)
。系统不会考虑特性的命名空间,搜索方法时仅使用特性名称和类型。
以 android:text="@{user.name}"
表达式为例,库会查找接受 user.getName()
所返回类型的 setText(arg)
方法。如果 user.getName()
的返回类型为 String
,则库会查找接受 String
参数的 setText()
方法。如果表达式返回的是 int
,则库会搜索接受 int
参数的 setText()
方法。表达式必须返回正确的类型,您可以根据需要强制转换返回值的类型。
即使不存在具有给定名称的特性,数据绑定也会起作用。然后,您可以使用数据绑定为任何 setter 创建特性。例如,支持类 DrawerLayout
没有任何特性,但有很多 setter。以下布局会自动将 setScrimColor(int)
和 setDrawerListener(DrawerListener)
方法分别用作 app:scrimColor
和 app:drawerListener
特性的 setter:
1 | <android.support.v4.widget.DrawerLayout |
指定自定义方法名称
一些属性具有名称不符的 setter 方法。在这些情况下,某个特性可能会使用 BindingMethods
注解与 setter 相关联。注解与类一起使用,可以包含多个 BindingMethod
注解,每个注解对应一个重命名的方法。绑定方法是可添加到应用中任何类的注解。在以下示例中,android:tint
属性与 setImageTintList(ColorStateList)
方法相关联,而不与 setTint()
方法相关联:
1 |
大多数情况下,您无需在 Android 框架类中重命名 setter。特性已使用命名惯例实现,可自动查找匹配的方法。
提供自定义逻辑
一些属性需要自定义绑定逻辑。例如,android:paddingLeft
特性没有关联的 setter,而是提供了 setPadding(left, top, right, bottom)
方法。使用 BindingAdapter
注释的静态绑定适配器方法支持自定义特性 setter 的调用方式。
Android 框架类的特性已经创建了 BindingAdapter
注释。例如,以下示例展示了 paddingLeft
属性的绑定适配器:
1 |
|
参数类型非常重要。第一个参数用于确定与特性关联的视图类型,第二个参数用于确定在给定特性的绑定表达式中接受的类型。
绑定适配器对其他类型的自定义很有用。例如,可以通过工作器线程调用自定义加载程序来加载图片。
出现冲突时,您定义的绑定适配器会替换由 Android 框架提供的默认适配器。
您还可以使用接收多个属性的适配器,如以下示例所示:
1 |
|
您可以在布局中使用适配器,如以下示例所示。请注意,@drawable/venueError
引用应用中的资源。使用 @{}
将资源括起来可使其成为有效的绑定表达式。
1 | <ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" /> |
注意:数据绑定库在匹配时会忽略自定义命名空间。
如果 ImageView
对象同时使用了 imageUrl
和 error
,并且 imageUrl
是字符串,error
是 Drawable
,就会调用适配器。如果您希望在设置了任意属性时调用适配器,则可以将适配器的可选 requireAll
标志设置为 false
,如以下示例所示:
1 |
|
注意:出现冲突时,绑定适配器会替换默认的数据绑定适配器。
绑定适配器方法可以选择性在处理程序中使用旧值。同时获取旧值和新值的方法应该先为属性声明所有旧值,然后再声明新值,如以下示例所示:
1 |
|
事件处理脚本只能与具有一种抽象方法的接口或抽象类一起使用,如以下示例所示:
1 |
|
按如下方式在布局中使用此事件处理脚本:
1 | <View android:onLayoutChange="@{() -> handler.layoutChanged()}"/> |
当监听器有多个方法时,必须将它拆分为多个监听器。例如,View.OnAttachStateChangeListener
有两个方法:onViewAttachedToWindow(View)
和 onViewDetachedFromWindow(View)
。该库提供了两个接口,用于区分它们的属性和处理脚本:
1 | // Translation from provided interfaces in Java: |
因为更改一个监听器也会影响另一个监听器,所以需要适用于其中一个属性或同时适用于这两个属性的适配器。您可以在注释中将 requireAll
设置为 false
,以指定并非必须为每个属性都分配绑定表达式,如以下示例所示:
1 |
|
以上示例比一般情况稍微复杂一些,因为 View
类使用 addOnAttachStateChangeListener()
和 removeOnAttachStateChangeListener()
方法,而非 OnAttachStateChangeListener
的 setter 方法。android.databinding.adapters.ListenerUtil
类有助于跟踪以前的监听器,以便在绑定适配器中将它们移除。
通过用 @TargetApi(VERSION_CODES.HONEYCOMB_MR1)
注释接口 OnViewDetachedFromWindow
和 OnViewAttachedToWindow
,数据绑定代码生成器知道只应在运行 Android 3.1(API 级别 12)及更高级别(addOnAttachStateChangeListener()
方法支持的相同版本)时生成监听器。
视图绑定
如果仅仅是想替代 findViewById
,视图绑定会更佳。
https://developer.android.com/topic/libraries/view-binding?hl=zh-cn
视图绑定和数据绑定均会生成可用于直接引用视图的绑定类。但是,视图绑定旨在处理更简单的用例,与数据绑定相比,具有以下优势:
- 更快的编译速度:视图绑定不需要处理注释,因此编译时间更短。
- 易于使用:视图绑定不需要特别标记的 XML 布局文件,因此在应用中采用速度更快。在模块中启用视图绑定后,它会自动应用于该模块的所有布局。
反过来,与数据绑定相比,视图绑定也具有以下限制:
- 视图绑定不支持布局变量或布局表达式,因此不能用于直接在 XML 布局文件中声明动态界面内容。
- 视图绑定不支持双向数据绑定。
考虑到这些因素,在某些情况下,最好在项目中同时使用视图绑定和数据绑定。您可以在需要高级功能的布局中使用数据绑定,而在不需要高级功能的布局中使用视图绑定。