1、数据绑定 - 以声明方式将可观察数据绑定到界面元素
数据绑定库是一种支持库,借助该库,您可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。
一般调用 findViewById() 来查找 TextView 并将其绑定到 viewModel 变量的 userName 属性:
findViewById<TextView>(R.id.sample_text).apply {
text = viewModel.userName
}
如何在布局文件中使用数据绑定库将文本直接分配呢。这样就无需调用上述代码。请注意 @{} 语法的使用:
<TextView
android:text="@{viewmodel.userName}" />
借助布局文件中的绑定组件,您可以移除 Activity 中的许多界面框架调用,使其维护起来更简单、方便。还可以提高应用性能,并且有助于防止内存泄漏以及避免发生 Null 指针异常。
使用数据绑定
请在应用模块的 build.gradle 文件中添加 dataBinding 元素,如以下示例所示
android {
...
dataBinding {
enabled = true
}
}
Android 数据绑定库示例
注意:在许多情况下,视图绑定可简化实现,提高性能,提供与数据绑定相同的好处。如果您使用数据绑定的主要目的是取代 findViewById() 调用,请考虑改用视图绑定。
使用视图绑定- 视图绑定在 Android Studio 3.6 Canary 11 及更高版本中可用。
视图绑定功能可按模块启用。要在某个模块中启用视图绑定,请将 viewBinding 元素添加到其 build.gradle 文件中,如下例所示:
android {
...
viewBinding {
enabled = true
}
}
如果您希望在生成绑定类时忽略某个布局文件,请将 tools:viewBindingIgnore=“true” 属性添加到相应布局文件的根视图中:
<LinearLayout
...
tools:viewBindingIgnore="true" >
...
</LinearLayout>
为某个模块启用视图绑定功能后,系统会为该模块中包含的每个 XML 布局文件各生成一个绑定类。 每个绑定类均包含对根视图以及具有 ID 的所有视图的引用。 系统会通过以下方式生成绑定类的名称:将 XML 文件的名称转换为驼峰式大小写,并在末尾添加“Binding”一词。
假设某个布局文件名为 result_profile.xml:
<LinearLayout ... >
<TextView android:id="@+id/name" />
<ImageView android:cropToPadding="true" />
<Button android:id="@+id/button"
android:background="@drawable/rounded_button" />
</LinearLayout>
生成的绑定类将名为 ResultProfileBinding。 此类具有两个字段:一个是名为 name 的 TextView,另一个是名为 button 的 Button。 由于布局中的 ImageView 没有 ID,因此绑定类中不存在对它的引用。
每个绑定类还包含一个 getRoot() 方法,用于为相应布局文件的根视图提供直接引用。 在此示例中,ResultProfileBinding 类中的 getRoot() 方法会返回 LinearLayout 根视图。
要获取生成的绑定类的实例,您可以调用其静态 inflate() 方法。 通常情况下,您还会调用 setContentView(),从而将该绑定类的根视图作为参数进行传递,以使它成为屏幕上的活动视图。 在此示例中,您可以在 Activity 中调用 ResultProfileBinding.inflate():
private lateinit var binding: ResultProfileBinding
@Override
fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
binding = ResultProfileBinding.inflate(layoutInflater)
setContentView(binding.root)
}
现在,绑定类的实例可用于引用任何视图:
binding.name.text = viewModel.name
binding.button.setOnClickListener { viewModel.userClicked() }
与使用 findViewById 相比,视图绑定具有一些很显著的优点:
- Null 安全:由于视图绑定会创建对视图的直接引用,因此不存在因视图 ID 无效而引发 Null 指针异常的风险。 此外,如果视图仅出现在布局的某些配置中,则绑定类中包含其引用的字段会使用 @Nullable 标记。
- 类型安全:每个绑定类中的字段均具有与它们在 XML 文件中引用的视图相匹配的类型。这意味着不存在发生类转换异常的风险。 这些差异意味着布局和代码之间的不兼容性可能会导致编译版本在编译时(而非运行时)失败。
视图绑定和数据绑定的区别
视图绑定和数据绑定库均会生成可用于直接引用视图的绑定类。不过,这两者之间存在明显差异:
- 数据绑定库仅处理使用 <layout> 代码创建的数据绑定布局。
- 视图绑定不支持布局变量或布局表达式,因此它不能用于在 XML 中将布局与数据绑定。
2、Lifecycle - 管理您的 Activity 和 Fragment 生命周期
一般是在 Activity 和 Fragment 的生命周期方法中实现依赖组件的操作,这种模式会导致代码条理性很差而且会扩散错误。 通过使用生命周期感知型组件,您可以将依赖组件的代码从生命周期方法移入组件本身中。
假设我们有一个在屏幕上显示设备位置的 Activity。常见的实现可能如下所示:
internal class MyLocationListener(
private val context: Context,
private val callback: (Location) -> Unit
) {
fun start() {
// connect to system location service
}
fun stop() {
// disconnect from system location service
}
}
class MyActivity : AppCompatActivity() {
private lateinit var myLocationListener: MyLocationListener
override fun onCreate(...) {
myLocationListener = MyLocationListener(this) { location ->
// update UI
}
}
public override fun onStart() {
super.onStart()
myLocationListener.start()
// manage other components that need to respond
// to the activity lifecycle
}
public override fun onStop() {
super.onStop()
myLocationListener.stop()
// manage other components that need to respond
// to the activity lifecycle
}
}
在真实的应用中,最终会有太多管理界面和其他组件的调用,以响应生命周期的当前状态。 管理多个组件会在生命周期方法(如 onStart() 和 onStop())中放置大量的代码,这使得它们难以维护。
此外,无法保证组件会在 Activity 或 Fragment 停止之前启动。在我们需要执行长时间运行的操作(如 onStart() 中的某种配置检查)时尤其如此。这可能会导致出现一种竞争条件,在这种条件下,onStop() 方法会在 onStart() 之前结束,这使得组件留存的时间比所需的时间要长。
androidx.lifecycle 软件包提供的类和接口可帮助您以弹性和隔离的方式解决这些问题。
Lifecycle 是一个类,用于存储有关组件(如 Activity 或 Fragment)的生命周期状态的信息,并允许其他对象观察此状态。
//类可以通过向其方法添加注解来监控组件的生命周期状态。
//然后,您可以通过调用 Lifecycle 类的 addObserver() 方法并传递观察者的实例来添加观察者
class MyObserver : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun connectListener() {
...
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun disconnectListener() {
...
}
}
//myLifecycleOwner 对象实现了 LifecycleOwner 接口
//LifecycleOwner 是单一方法接口,表示类具有 Lifecycle。
myLifecycleOwner.getLifecycle().addObserver(MyObserver())
下面我们重新写一下MyLocationListener示例、
我们可以让 MyLocationListener 类实现 LifecycleObserver, 然后在 onCreate() 方法中使用 Activity 的 Lifecycle 对其进行初始化。
这样,MyLocationListener 类便可以“自给自足”,这意味着,对生命周期状态的变化做出响应的逻辑会在 MyLocationListener(而不是在 Activity)中进行声明。让各个组件存储自己的逻辑,可使 Activity 和 Fragment 逻辑更易于管理。
internal class MyLocationListener(
private val context: Context,
private val lifecycle: Lifecycle,
private val callback: (Location) -> Unit
) {
private var enabled = false
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun start() {
if (enabled) {
// connect
}
}
fun enable() {
enabled = true
//如果 Lifecycle 现在未处于良好的状态,则应避免调用某些回调。
//例如,如果回调在 Activity 状态保存后运行 Fragment 事务,就会引发崩溃,因此我们绝不能调用该回调。
//所以,Lifecycle 类允许其他对象查询当前状态。
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
// connect if not connected
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stop() {
// disconnect if connected
}
}
class MyActivity : AppCompatActivity() {
private lateinit var myLocationListener: MyLocationListener
override fun onCreate(...) {
myLocationListener = MyLocationListener(this, lifecycle) { location ->
// update UI
}
Util.checkUserStatus { result ->
if (result) {
myLocationListener.enable()
}
}
}
}
LocationListener 类可以完全感知生命周期。如果我们需要从另一个 Activity 或 Fragment 使用 LocationListener,只需对其进行初始化。所有设置和拆解操作都由类本身管理,我们建议您使用生命周期感知型组件,可以轻松集成这些组件,而无需在客户端进行手动生命周期管理。
生命周期感知型组件的最佳做法
1.使界面控制器(Activity 和 Fragment)保持精简。它们不应直接获取数据,而应使用 ViewModel 获取,并观察 LiveData 对象更新UI视图。
2.设法编写数据驱动型界面,界面控制器的责任是随着数据更改而更新视图,或者将用户操作通知给 ViewModel。
3.将数据逻辑放在 ViewModel 类中。 ViewModel 应充当界面控制器与应用其余部分之间的连接器。不过要注意,ViewModel 不负责获取数据(例如,从网络获取)。ViewModel 应调用相应的组件来获取数据,然后将结果提供给界面控制器。
4.使用 Data Binding 在视图与界面控制器之间维持干净的接口,您可以使视图更具声明性,并尽量减少需要在 Activity 和 Fragment 中编写的更新代码。
5.如果界面很复杂,不妨考虑创建 presenter 类来处理界面的修改。
6.避免在 ViewModel 中引用 View 或 Activity 上下文。 如果 ViewModel 存在的时间比 Activity 更长(在配置更改的情况下),Activity 将泄露并且不会由垃圾回收器妥善处置。
7.使用 Kotlin 协程管理长时间运行的任务和其他可以异步运行的操作。
生命周期感知型组件的使用场景
生命周期感知型组件可使您在各种情况下更轻松地管理生命周期。下面列举几个例子:
1.在粗粒度和细粒度位置更新之间切换。在位置应用可见时启用细粒度位置更新,在应用位于后台时切换到粗粒度更新。借助生命周期感知型组件 LiveData,应用可以在用户使用位置发生变化时自动更新界面。
2.停止和开始视频缓冲。可尽快开始视频缓冲,但会推迟播放,直到应用完全启动。此外,应用销毁后,您还可以使用生命周期感知型组件终止缓冲。
3.开始和停止网络连接。可在应用位于前台时启用网络数据的实时更新(流式传输),并在应用进入后台时自动暂停。
4.暂停和恢复动画可绘制资源。可在应用位于后台时暂停动画可绘制资源,并在应用位于前台后恢复可绘制资源。
处理 ON_STOP 事件
如果 Lifecycle 属于 AppCompatActivity 或 Fragment,那么调用 AppCompatActivity 或 Fragment 的 onSaveInstanceState() 时,Lifecycle 的状态会更改为 CREATED 并且会分派 ON_STOP 事件。
通过 onSaveInstanceState() 保存 Fragment 或 AppCompatActivity 的状态后,其界面被视为不可变,直到调用 ON_START。如果在保存状态后尝试修改界面,很可能会导致应用的导航状态不一致,因此应用在保存状态后运行 FragmentTransaction 时,FragmentManager 会抛出异常。
LiveData 本身可防止出现这种极端情况,方法是在其观察者的关联 Lifecycle 还没有至少处于 STARTED 状态时避免调用其观察者。 在后台,它会在决定调用其观察者之前调用 isAtLeast()。
遗憾的是,AppCompatActivity 的 onStop() 方法会在 onSaveInstanceState() 之后调用,这样就会留下一个缺口,即不允许界面状态发生变化,但 Lifecycle 尚未移至 CREATED 状态。
为防止出现这个问题,beta2 及更低版本中的 Lifecycle 类会将状态标记为 CREATED 而不分派事件,这样一来,即使未分派事件(直到系统调用 onStop()),检查当前状态的任何代码也会获得实际值。
遗憾的是,此解决方案有两个主要问题:
在 API 23 及更低级别,Android 系统实际上会保存 Activity 的状态,即使它的一部分被另一个 Activity 覆盖。换句话说,Android 系统会调用 onSaveInstanceState(),但不一定会调用 onStop()。这样可能会产生很长的时间间隔,在此时间间隔内,观察者仍认为生命周期处于活动状态,虽然无法修改其界面状态。
要向 LiveData 类公开类似行为的任何类都必须实现由 Lifecycle 版本 beta 2 及更低版本提供的解决方案。
注意:为了简化此流程并让其与较低版本实现更好的兼容性,自 1.0.0-rc1 版本起,当调用 onSaveInstanceState() 时,会将 Lifecycle 对象标记为 CREATED 并分派 ON_STOP,而不等待调用 onStop() 方法。这不太可能影响您的代码,但您需要注意这一点,因为它与 API 26 及更低级别的 Activity 类中的调用顺序不符。
示例:
- Android 架构组件基本示例
- Sunflower,这是一个演示应用,演示架构组件的最佳做法
3、LiveData - 在底层数据库更改时通知视图
LiveData 是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。
如果观察者(由 Observer 类表示)的生命周期处于 STARTED 或 RESUMED 状态,则 LiveData 会认为该观察者处于活跃状态。LiveData 只会将更新通知给活跃的观察者。为观察 LiveData 对象而注册的非活跃观察者不会收到更改通知
LiveData 的优势
确保界面符合数据状态
LiveData 遵循观察者模式。当生命周期状态发生变化时,LiveData 会通知 Observer 对象,观察者可以在每次发生更改时更新界面,而不是在每次应用数据发生更改时更新界面。
不会发生内存泄露
观察者会绑定到 Lifecycle 对象,并在其关联的生命周期遭到销毁后进行自我清理。
不会因 Activity 停止而导致崩溃
如果观察者的生命周期处于非活跃状态(如返回栈中的 Activity),则它不会接收任何 LiveData 事件。
不再需要手动处理生命周期
界面组件只是观察相关数据,不会停止或恢复观察。LiveData 将自动管理所有这些操作,因为它在观察时可以感知相关的生命周期状态变化。
数据始终保持最新状态
如果生命周期变为非活跃状态,它会在再次变为活跃状态时接收最新的数据。例如,曾经在后台的 Activity 会在返回前台后立即接收最新的数据。
适当的配置更改
如果由于配置更改(如设备旋转)而重新创建了 Activity 或 Fragment,它会立即接收最新的可用数据。
共享资源
您可以使用单一实例模式扩展 LiveData 对象以封装系统服务,以便在应用中共享它们。LiveData 对象连接到系统服务一次,然后需要相应资源的任何观察者只需观察 LiveData 对象
使用LiveData
1.创建 LiveData 实例以存储某种类型的数据。这通常在 ViewModel 类中完成。
2.创建可定义 onChanged() 方法的 Observer 对象,该方法可以控制当 LiveData 对象存储的数据更改时会发生什么。通常情况下,您可以在界面控制器(如 Activity 或 Fragment)中创建 Observer 对象。
3.使用 observe() 方法将 Observer 对象附加到 LiveData 对象。observe() 方法会采用 LifecycleOwner 对象。这样会使 Observer 对象订阅 LiveData 对象,以使其收到有关更改的通知。通常情况下,您可以在界面控制器(如 Activity 或 Fragment)中附加 Observer 对象。
注意:您可以使用 observeForever(Observer) 方法来注册未关联 LifecycleOwner 对象的观察者。在这种情况下,观察者会被视为始终处于活跃状态,因此它始终会收到关于修改的通知。您可以通过调用 removeObserver(Observer) 方法来移除这些观察者。
当您更新存储在 LiveData 对象中的值时,它会触发所有已注册的观察者(只要附加的 LifecycleOwner 处于活跃状态)。 LiveData 允许界面控制器观察者订阅更新。当 LiveData 对象存储的数据发生更改时,界面会自动更新以做出响应。
创建LiveData对象
LiveData 对象通常存储在 ViewModel 对象中,并可通过 getter 方法进行访问
class NameViewModel : ViewModel() {
// Create a LiveData with a String
val currentName: MutableLiveData<String> by lazy {
MutableLiveData<String>()
}
// Rest of the ViewModel...
}
注意:请确保将用于更新界面的 LiveData 对象存储在 ViewModel 对象中,而不是将其存储在 Activity 或 Fragment 中,原因如下:
- 避免 Activity 和 Fragment 过于庞大。现在,这些界面控制器负责显示数据,但不负责存储数据状态。
- 将 LiveData 实例与特定的 Activity 或 Fragment 实例分离开,并使 对象在配置更改后继续存在。
观察 LiveData 对象
一般在 onCreate() 方法是开始观察 LiveData 对象的正确着手点,原因如下:
- 确保系统不会从 Activity 或 Fragment 的 onResume() 方法进行冗余调用。
- 确保 Activity 或 Fragment 变为活跃状态后具有可以立即显示的数据。一旦应用组件处于 STARTED 状态,就会从它正在观察的 LiveData 对象接收最新值。只有在设置了要观察的 LiveData 对象时,才会发生这种情况。
//在传递 nameObserver 参数的情况下调用 observe() 后,系统会立即调用 onChanged(),从而提供 mCurrentName 中存储的最新值。 如果 LiveData 对象尚未在 mCurrentName 中设置值,则不会调用 onChanged()。
class NameActivity : AppCompatActivity() {
private lateinit var model: NameViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Other code to setup the activity...
// Get the ViewModel.
model = ViewModelProviders.of(this).get(NameViewModel::class.java)
// Create the observer which updates the UI.
val nameObserver = Observer<String> { newName ->
// Update the UI, in this case, a TextView.
nameTextView.text = newName
}
// Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
model.currentName.observe(this, nameObserver)
}
}
更新 LiveData 对象
LiveData 没有公开可用的方法来更新存储的数据。MutableLiveData 类将公开 setValue(T) 和 postValue(T) 方法,如果您需要修改存储在 LiveData 对象中的值,则必须使用这些方法。 通常情况下会在 ViewModel 中使用 MutableLiveData,然后 ViewModel 只会向观察者公开不可变的 LiveData 对象。 设置观察者关系后,您可以更新 LiveData 对象的值(如以下示例中所示),这样当用户点按某个按钮时会触发所有观察者:
button.setOnClickListener {
val anotherName = "John Doe"
model.currentName.setValue(anotherName)
}
注意:您必须调用 setValue(T) 方法以从主线程更新 LiveData 对象。如果在 worker 线程中执行代码,则您可以改用 postValue(T) 方法来更新 LiveData 对象。
将LiveData与Room一起使用
当数据库更新时,Room 会生成更新 LiveData 对象所需的所有代码。在需要时,生成的代码会在后台线程上异步运行查询。此模式有助于使界面中显示的数据与存储在数据库中的数据保持同步。
将协程与 LiveData 一起使用
https://developer.android.google.cn/topic/libraries/architecture/coroutines
扩展 LiveData
class StockLiveData(symbol: String) : LiveData<BigDecimal>() {
private val stockManager = StockManager(symbol)
private val listener = { price: BigDecimal ->
value = price
}
//LiveData有活跃观察者时
override fun onActive() {
stockManager.requestPriceUpdates(listener)
}
//LiveData没有任何活跃观察者时
override fun onInactive() {
stockManager.removeUpdates(listener)
}
}
LiveData 可以在多个 Activity、Fragment 和 Service 之间共享它们。您可以将 LiveData 类实现为单一实例
class StockLiveData(symbol: String) : LiveData<BigDecimal>() {
private val stockManager: StockManager = StockManager(symbol)
private val listener = { price: BigDecimal ->
value = price
}
override fun onActive() {
stockManager.requestPriceUpdates(listener)
}
override fun onInactive() {
stockManager.removeUpdates(listener)
}
companion object {
private lateinit var sInstance: StockLiveData
@MainThread
fun get(symbol: String): StockLiveData {
sInstance = if (::sInstance.isInitialized) sInstance else StockLiveData(symbol)
return sInstance
}
}
}
调用:
class MyFragment : Fragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) {
StockLiveData.get(symbol).observe(this, Observer<BigDecimal> { price: BigDecimal? ->
// Update the UI.
})
}
转换LiveData
LiveData 对象分派给观察者之前对存储的值进行更改, 根据另一个实例的值返回不同的 LiveData 实例 Lifecycle 软件包会提供 Transformations 类,该类包括可应对这些情况的辅助程序方法。
val userLiveData: LiveData<User> = UserLiveData()
val userName: LiveData<String> = Transformations.map(userLiveData) {
user -> "${user.name} ${user.lastName}"
}
与 map() 类似,对存储在 LiveData 对象中的值应用函数,并将结果解封和分派到下游。传递给 switchMap() 的函数必须返回 LiveData 对象,如以下示例中所示:
private fun getUser(id: String): LiveData<User> {
...
}
val userId: LiveData<String> = ...
val user = Transformations.switchMap(userId) { id -> getUser(id) }
要实现自定义转换,您可以使用 MediatorLiveData 类
合并多个 LiveData 源
MediatorLiveData 是 LiveData 的子类,允许您合并多个 LiveData 源。只要任何原始的 LiveData 源对象发生更改,就会触发 MediatorLiveData 对象的观察者。
例如,如果界面中有可以从本地数据库或网络更新的 LiveData 对象,则可以向 MediatorLiveData 对象添加以下源:
- 与存储在数据库中的数据关联的 LiveData 对象。
- 与从网络访问的数据关联的 LiveData 对象。 您的 Activity 只需观察 MediatorLiveData 对象即可从这两个源接收更新。
示例
- Sunflower,这是一个演示应用,演示架构组件的最佳做法
- Android 架构组件基本示例
4、navigation - 处理应用内导航所需的一切
导航是指支持用户导航、进入和退出应用中不同内容片段的交互。Android Jetpack 的导航组件可帮助您实现导航,无论是简单的按钮点击,还是应用栏和抽屉式导航栏等更为复杂的模式,该组件均可应对。导航组件还通过遵循一套既定原则来确保一致且可预测的用户体验。
5、Paging
分页库可帮助您一次加载和显示一小块数据。按需载入部分数据会减少网络带宽和系统资源的使用量。
使用 PagedList 对象的 LiveData 存储器加载和显示数据:
class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
val concertList: LiveData<PagedList<Concert>> =
concertDao.concertsByDate().toLiveData(pageSize = 50)
}
每个 PagedList 实例都会从对应的 DataSource 对象加载应用数据
@Dao
interface ConcertDao {
// The Int type parameter tells Room to use a PositionalDataSource object.
@Query("SELECT * FROM concerts ORDER BY date DESC")
fun concertsByDate(): DataSource.Factory<Int, Concert>
}
要详细了解如何将数据加载到 PagedList 对象中,请参阅有关如何加载分页数据的指南。
示例
- Android 架构组件分页示例
- 网络分页展示示例
界面:
PagedList 类使用 PagedListAdapter 将项加载到 RecyclerView。这些类共同作用,在内容加载时抓取和显示内容,预取不在视线范围内的内容以及针对内容更改添加动画。
要了解详情,请参阅有关如何显示分页列表的指南。
支持不同的数据架构
获取数据的3种方式:网络,本地数据库,网络+本地数据库
网络(Retrofit):
注意:由于不同的应用处理和显示错误界面的方式不同,因此分页库的 DataSource 对象不提供任何错误处理。如果发生错误,请遵循结果回调,并在稍后重试请求。有关此行为的示例,请参阅 PagingWithNetwork 示例。
本地数据库
设置您的 RecyclerView 以观察本地存储空间,最好使用 Room 持久性库。这样,无论您何时在应用数据库中插入或修改数据,这些更改都会自动反映在显示此数据的 RecyclerView 中。
网络和数据库
在开始观察数据库之后,您可以使用 PagedList.BoundaryCallback 监听数据库中的数据何时耗尽。然后,您可以从网络中获取更多项目并将它们插入到数据库中。如果界面正在观察数据库,则您只需执行此操作即可。
处理网络错误
由于服务器不稳定或者网络异常,如果数据刷新步骤不起作用,您可以提供“重试”按钮供用户选择。如果在数据分页步骤中发生错误,则最好自动重新尝试分页请求。
更新现有应用
1.在应用中将 List 对象替换成 PagedList 对象,后者不需要对应用界面结构或数据更新逻辑进行任何更改。 2.使用 CursorAdapter 3.使用 AsyncListUtil 异步加载内容
数据库示例
使用 LiveData 观察分页数据
@Dao
interface ConcertDao {
// The Int type parameter tells Room to use a PositionalDataSource
// object, with position-based loading under the hood.
@Query("SELECT * FROM concerts ORDER BY date DESC")
fun concertsByDate(): DataSource.Factory<Int, Concert>
}
class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
val concertList: LiveData<PagedList<Concert>> =
concertDao.concertsByDate().toLiveData(pageSize = 50)
}
class ConcertActivity : AppCompatActivity() {
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel = ViewModelProviders.of(this)
.get<ConcertViewModel>()
val recyclerView = findViewById(R.id.concert_list)
val adapter = ConcertAdapter()
viewModel.livePagedList.observe(this, PagedList(adapter::submitList))
recyclerView.setAdapter(adapter)
}
}
class ConcertAdapter() :
PagedListAdapter<Concert, ConcertViewHolder>(DIFF_CALLBACK) {
fun onBindViewHolder(holder: ConcertViewHolder, position: Int) {
val concert: Concert? = getItem(position)
// Note that "concert" is a placeholder if it's null.
holder.bindTo(concert)
}
companion object {
private val DIFF_CALLBACK = object :
DiffUtil.ItemCallback<Concert>() {
// Concert details may have changed if reloaded from the database,
// but ID is fixed.
override fun areItemsTheSame(oldConcert: Concert,
newConcert: Concert) = oldConcert.id == newConcert.id
override fun areContentsTheSame(oldConcert: Concert,
newConcert: Concert) = oldConcert == newConcert
}
}
}
使用 RxJava2 观察分页数据
如果您倾向于使用 RxJava2 而不是 LiveData,则可以改为创建 Observable 或 Flowable 对象:
class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
val concertList: Observable<PagedList<Concert>> =
concertDao.concertsByDate().toObservable(pageSize = 50)
}
然后,您可以使用以下代码段中的代码来开始和停止观察数据:
class ConcertActivity : AppCompatActivity() {
private val adapter: ConcertAdapter()
private lateinit var viewModel: ConcertViewModel
private val disposable = CompositeDisposable()
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val recyclerView = findViewById(R.id.concert_list)
viewModel = ViewModelProviders.of(this)
.get<ConcertViewModel>()
recyclerView.setAdapter(adapter)
}
override fun onStart() {
super.onStart()
disposable.add(viewModel.concertList
.subscribe(adapter::submitList)))
}
override fun onStop() {
super.onStop()
disposable.clear()
}
}
示例
- Android 架构组件分页示例
- 网络分页展示示例
6、Room - 流畅地访问 SQLite 数据库
Room 持久性库在 SQLite 的基础上提供了一个抽象层,让用户能够在充分利用 SQLite 的强大功能的同时,获享更强健的数据库访问机制。
Room 包含 3 个主要组件:
- 数据库:包含数据库持有者,并作为应用已保留的持久关系型数据的底层连接的主要接入点。使用 @Database 注释的类应满足以下条件:
- 是扩展 RoomDatabase 的抽象类。
- 在注释中添加与数据库关联的实体列表。
- 包含具有 0 个参数且返回使用 @Dao 注释的类的抽象方法。
在运行时,您可以通过调用 Room.databaseBuilder() 或 Room.inMemoryDatabaseBuilder() 获取 Database 的实例。
- Entity:表示数据库中的表。
- DAO:包含用于访问数据库的方法。
应用使用 Room 数据库来获取与该数据库关联的数据访问对象 (DAO)。然后,应用使用每个 DAO 从数据库中获取实体,然后再将对这些实体的所有更改保存回数据库中。最后,应用使用实体来获取和设置与数据库中的表列相对应的值。
使用 Room 将数据保存到本地数据库
基本用法
User
@Entity
data class User(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
UserDao
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): List<User>
@Query("SELECT * FROM user WHERE uid IN (:userIds)")
fun loadAllByIds(userIds: IntArray): List<User>
@Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
"last_name LIKE :last LIMIT 1")
fun findByName(first: String, last: String): User
@Insert
fun insertAll(vararg users: User)
@Delete
fun delete(user: User)
}
AppDatabase
@Database(entities = arrayOf(User::class), version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
调用数据库实例:
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "database-name"
).build()
注意:如果您的应用在单个进程中运行,则在实例化 AppDatabase 对象时应遵循单例设计模式。每个 RoomDatabase 实例的成本相当高,而您几乎不需要在单个进程中访问多个实例。
如果您的应用在多个进程中运行,请在数据库构建器调用中包含 enableMultiInstanceInvalidation()。这样,如果您在每个进程中都有一个 AppDatabase 实例,就可以在一个进程中使共享数据库文件失效,并且这种失效会自动传播到其他进程中的 AppDatabase 实例。
示例
- Sunflower,这是一款园艺应用,展示了使用 Android Jetpack 进行 Android 开发的最佳做法。
- Room 迁移示例
- Room 和 RxJava 示例 (Java) (Kotlin)
博客
- Android Sunflower 简介
- Room + Time
- 从 SQLite 逐步迁移到 Room
- 有关 Room 的 7 个专业提示
- 了解借助 Room 进行的迁移
- 测试 Room 迁移
- Room + RxJava
7、ViewModel -以注重生命周期的方式管理界面相关的数据
ViewModel 解决的问题:
1、某个 Activity 包含用户列表。因配置更改而重新创建 Activity 后,新 Activity 必须重新提取用户列表 2、界面控制器经常需要异步调用,并确保系统在其销毁后清理这些调用以避免潜在的内存泄露,并且在因配置更改而重新创建对象的情况下,会造成资源的浪费,因为对象可能需要重新发出已经发出过的调用。 3、如果要求界面控制器也负责从数据库或网络加载数据,那么会使类越发膨胀。
从界面控制器逻辑中分离出视图数据所有权的做法更易行且更高效。
实现 ViewModel
架构组件为界面控制器提供了 ViewModel辅助程序类,该类负责为界面准备数据。 在配置更改期间会自动保留 ViewModel 对象,以便它们存储的数据立即可供下一个 Activity 或 Fragment 实例使用。 例如,如果您需要在应用中显示用户列表,请确保将获取和保留该用户列表的责任分配给 ViewModel,而不是 Activity 或 Fragment,如以下示例代码所示:
class MyViewModel : ViewModel() {
private val users: MutableLiveData<List<User>> by lazy {
MutableLiveData().also {
loadUsers()
}
}
fun getUsers(): LiveData<List<User>> {
return users
}
private fun loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
然后,您可以从 Activity 访问该列表,如下所示:
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Create a ViewModel the first time the system calls an activity's onCreate() method.
// Re-created activities receive the same MyViewModel instance created by the first activity.
val model = ViewModelProviders.of(this)[MyViewModel::class.java]
model.getUsers().observe(this, Observer<List<User>>{ users ->
// update UI
})
}
}
如果重新创建了该 Activity,它接收的 MyViewModel 实例与第一个 Activity 创建的实例相同。当所有者 Activity 完成时,框架会调用 ViewModel 对象的 onCleared() 方法,以便它可以清理资源。
注意:ViewModel 绝不能引用视图、Lifecycle 或可能存储对 Activity 上下文的引用的任何类。
ViewModel 的生命周期
ViewModel 对象存在的时间范围是获取 ViewModel 时传递给 ViewModelProvider 的 Lifecycle。ViewModel 将一直留在内存中,直到限定其存在时间范围的 Lifecycle 永久消失:对于 Activity,是在 Activity 完成时;而对于 Fragment,是在 Fragment 分离时。
系统首次调用 Activity 对象的 onCreate()方法时请求 ViewModel,系统可能会在 Activity 的整个生命周期内多次调用 onCreate(),ViewModel存在的时间范围是从您首次请求 ViewMode 直到 Activity 完成并销毁。
在 Fragment 之间共享数据
AFragment和BFragment数据传递,两个 Fragment 都需要定义接口描述,这两个 Fragment 都必须处理另一个 Fragment 尚未创建或不可见的情况。
可以使用 ViewModel对象解决这一常见的难点。这两个 Fragment 可以使用其 Activity 范围共享 ViewModel来处理此类通信,如以下示例代码所示:
class SharedViewModel : ViewModel() {
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
}
class MasterFragment : Fragment() {
private lateinit var itemSelector: Selector
private lateinit var model: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = activity?.run {
ViewModelProviders.of(this)[SharedViewModel::class.java]
} ?: throw Exception("Invalid Activity")
itemSelector.setOnClickListener { item ->
// Update the UI
}
}
}
class DetailFragment : Fragment() {
private lateinit var model: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = activity?.run {
ViewModelProviders.of(this)[SharedViewModel::class.java]
} ?: throw Exception("Invalid Activity")
model.selected.observe(this, Observer<Item> { item ->
// Update the UI
})
}
}
请注意,这两个 Fragment 都会检索包含它们的 Activity。这样,当这两个 Fragment 各自获取 ViewModelProvider 时,它们会收到相同的 SharedViewModel 实例(其范围限定为该 Activity)。
此方法具有以下优势:
- Activity 不需要执行任何操作,也不需要对此通信有任何了解。
- 除了 SharedViewModel 约定之外,Fragment 不需要相互了解。如果其中一个 Fragment 消失,另一个 Fragment 将继续照常工作。
- 每个 Fragment 都有自己的生命周期,而不受另一个 Fragment 的生命周期的影响。如果一个 Fragment 替换另一个 Fragment,界面将继续工作而没有任何问题。
将加载器替换为 ViewModel
诸如 [CursorLoader](https://developer.android.google.cn/reference/android/content/CursorLoader) 之类的加载器类经常用于使应用界面中的数据与数据库保持同步。您可以将 ViewModel 与一些其他类一起使用来替换加载器。使用 ViewModel 可将界面控制器与数据加载操作分离,这意味着类之间的强引用更少
ViewModel 与 Room 和 LiveData 一起使用可替换加载器。ViewModel 确保数据在设备配置更改后仍然存在。Room 在数据库发生更改时通知 LiveData,LiveData 进而使用修订后的数据更新界面。
将协程与 ViewModel 一起使用
ViewModel 支持 Kotlin 协程。如需了解详情,请参阅将 Kotlin 协程与 Android 架构组件一起使用。
示例
- Kotlin 协程与架构组件
如需更多与协程相关的信息,请参阅以下链接:
- 利用 Kotlin 协程提升应用性能
- 协程概览
- 在 CoroutineWorker 中进行线程处理
8、WorkManager - 管理您的 Android 后台作业
当前稳定版: | 2020 年 1 月 22 日 | 2.3.0 |
dependencies {
def work_version = "2.3.0"
// (Java only)
implementation "androidx.work:work-runtime:$work_version"
// Kotlin + coroutines
implementation "androidx.work:work-runtime-ktx:$work_version"
// optional - RxJava2 support
implementation "androidx.work:work-rxjava2:$work_version"
// optional - GCMNetworkManager support
implementation "androidx.work:work-gcm:$work_version"
// optional - Test helpers
androidTestImplementation "androidx.work:work-testing:$work_version"
}
WorkManager 旨在用于可延迟运行(即不需要立即运行)并且在应用退出或设备重启时必须能够可靠运行的任务。例如:
- 向后端服务发送日志或分析数据
- 定期将应用数据与服务器同步
WorkManager 不适用于应用进程结束时能够安全终止的运行中后台工作,也不适用于需要立即执行的任务。请查看后台处理指南,了解哪种解决方案符合您的需求。
创建后台任务
要创建后台任务,请扩展 Worker 类并替换 doWork() 方法。例如,要创建上传图像的 Worker:
class UploadWorker(appContext: Context, workerParams: WorkerParameters)
: Worker(appContext, workerParams) {
override fun doWork(): Result {
// Do the work here--in this case, upload the images.
uploadImages()
// Indicate whether the task finished successfully with the Result
return Result.success()
}
}
从 doWork() 返回的 Result 会通知 WorkManager 任务是否:
- 已成功完成:Result.success()
- 已失败:Result.failure()
- 需要稍后重试:Result.retry()
注意:Worker 是运行工作的最简单方式。要了解 Worker 的更多高级选项,请参阅 WorkManager 线程指南。
WorkManager 提供了四种不同类型的工作基元:
- Worker 是最简单的实现,前面几节已经有所介绍。WorkManager 会在后台线程上自动运行它(您可以将它替换掉)。请参阅工作器中的线程处理,详细了解 Worker 中的线程处理。
- 建议 Kotlin 用户实现 CoroutineWorker。CoroutineWorker 针对后台工作公开挂起函数。默认情况下,它们运行默认的 Dispatcher,您可以对其进行自定义。请参阅 CorventineWorker 中的线程处理,详细了解 CoroutineWorker 中的线程处理。
- 建议 RxJava2 用户实现 RxWorker。如果您有很多现有异步代码是用 RxJava 建模的,则应使用 RxWirkers。与所有 RxJava2 概念一样,您可以自由选择所需的线程处理策略。请参阅 RxWorker 中的线程处理,详细了解 RxWorker 中的线程处理。
- ListenableWorker 是 Worker、CoroutineWorker 和 RxWorker 的基类。该类专为需要与基于回调的异步 API(例如 FusedLocationProviderClient)进行交互并且不使用 RxJava2 的 Java 开发者而设计。请参阅 ListenableWorker 中的线程处理,详细了解 ListenableWorker 中的线程处理。
配置运行任务的方式和时间
Worker 定义工作单元,WorkRequest 则定义工作的运行方式和时间。任务可以是一次性的,也可以是周期性的。对于一次性 WorkRequest,请使用 OneTimeWorkRequest,对于周期性工作,请使用 PeriodicWorkRequest。
val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.build()
WorkRequest 中还可以包含其他信息,例如任务在运行时应遵循的约束、工作输入、延迟,以及重试工作的退避时间政策。关于这些选项,在定义工作指南中有更详细的说明。
在本指南中,您将了解如何自定义工作请求来处理常见用例:
- 处理网络可用性等任务约束
- 保证任务执行的延迟时间最短
- 处理任务重试和退避
- 处理任务输入和输出
- 使用标记对任务进行分组
1、工作约束:
您可以向工作添加 Constraints,以指明工作何时可以运行:
// Create a Constraints object that defines when the task should run
val constraints = Constraints.Builder()
.setRequiresDeviceIdle(true)
.setRequiresCharging(true)
.build()
// ...then create a OneTimeWorkRequest that uses those constraints
val compressionWork = OneTimeWorkRequestBuilder<CompressWorker>()
.setConstraints(constraints)
.build()
2、初始延迟
任务设置为在加入队列后至少经过 10 分钟再运行:
val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.setInitialDelay(10, TimeUnit.MINUTES)
.build()
3、重试和退避政策
如果您需要让 WorkManager 重新尝试执行您的任务,可以从工作器返回 Result.retry()。
val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.setBackoffCriteria(
BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS)
.build()
4、定义任务的输入/输出
输入和输出值以键值对的形式存储在 Data 对象中。下面的代码展示了如何在 WorkRequest 中设置输入数据。
// workDataOf (part of KTX) converts a list of pairs to a [Data] object.
val imageData = workDataOf(Constants.KEY_IMAGE_URI to imageUriString)
val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.setInputData(imageData)
.build()
Worker 类可通过调用 Worker.getInputData() 访问输入参数。
类似地,Data 类可用于输出返回值。要返回 Data 对象,请将它包含到 Result 的 Result.success() 或 Result.failure() 中,如下所示。
class UploadWorker(appContext: Context, workerParams: WorkerParameters)
: Worker(appContext, workerParams) {
override fun doWork(): Result {
// Get the input
val imageUriInput = getInputData().getString(Constants.KEY_IMAGE_URI)
// TODO: validate inputs.
// Do the work
val response = uploadFile(imageUriInput)
// Create the output of the work
val outputData = workDataOf(Constants.KEY_IMAGE_URL to response.imageUrl)
// Return the output
return Result.success(outputData)
}
}
5、标记工作
以下代码展示了如何使用 WorkRequest.Builder.addTag(String) 向任务添加“cleanup”标记:
val cacheCleanupTask =
OneTimeWorkRequestBuilder<CacheCleanupWorker>()
.setConstraints(constraints)
.addTag("cleanup")
.build()
将您的任务提交给系统
定义 WorkRequest 之后,您现在可以通过 WorkManager 使用 enqueue() 方法来调度它。
WorkManager.getInstance(myContext).enqueue(uploadWorkRequest)
执行 Worker 的确切时间取决于 WorkRequest 中使用的约束以及系统优化。WorkManager 的设计目的就是要在这些限制下提供尽可能好的表现。
方法指南
- 定义您的 WorkRequest
- 观察工作状态
- 观察工作器的中间进度
- 将工作链接在一起
- 取消和停止工作
- 处理重复性工作
- 处理特有的工作
- 测试工作器
高级概念
- 自定义和初始化
- 在 WorkManager 中进行线程处理
- 在工作器中进行线程处理
- 在 CoroutineWorker 中进行线程处理
- 在 RxWorker 中进行线程处理
- 在 ListenableWorker 中进行线程处理
迁移指南
- 从 Firebase JobDispatcher 迁移
- 从 GCMNetworkManager 迁移
示例
- WorkManagerSample,一个简单的图像处理应用
- Sunflower,这是一款演示应用,演示包括 WorkManager 在内的各种架构组件的最佳做法。