Jetpack组件库
ViewMode
是专门存放与界面相关数据的,只要是界面看得到的数据,都应该存放在ViewMode中。
它的一个重要特性是,只有当界面退出时才会被销毁,界面的旋转不会被重新创建。
一般创建方法
- 弃用
ViewModelProviders.of()
:已弃用ViewModelProviders.of()
。您可以将 Fragment 或 FragmentActivity 传递给新的ViewModelProvider(ViewModelStoreOwner)
构造函数,以便在使用 Fragment 1.2.0 时实现相同的功能。 - 弃用
lifecycle-extensions
工件:在上面弃用ViewModelProviders.of()
后,此版本标志着弃用lifecycle-extensions
中的最后一个 API,因此现在该工件已完全被弃用。我们强烈建议依赖于您需要的特定 Lifecycle 工件(例如,如果您使用的是 LifecycleService,则依赖于 lifecycle-service;如果您使用的是 ProcessLifecycleOwner,则依赖于 lifecycle-process)而不是lifecycle-extensions
,因为将来不会有lifecycle-extensions
的 2.3.0 版本。 - Activity/Frangment只能获取viewMode实例,而不能创建,因为后者周期长于前两者,这样会导致重复创建的可能。
- 更多创建用法见:创建ViewMode
先定义一个字段用于计数:
class MainActivityViewMode : ViewModel()
{
var counter = 0
}
再于mainactivity写入:
class MainActivity : AppCompatActivity()
{
private lateinit var textView: TextView
lateinit var viewMode: MainActivityViewMode
@SuppressLint("SetTextI18n")
@RequiresApi(Build.VERSION_CODES.S)
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
// 获取实例,不能创建
viewMode = ViewModelProvider(this)[MainActivityViewMode::class.java]
...
linearLayout.addView(Button(this).apply {
setOnClickListener {
viewMode.counter++
refreshCount()
}
})
// 创建时刷新
refreshCount()
...
}
private fun refreshCount()
{
textView.text = viewMode.counter.toString()
}
}
向ViewMode传递值
上文的默认值是0,有没有办法在初始时,赋予默认值给viewMode呢?这就是本篇要介绍的情况。
下述代码就能保证不管程序进入后台还是退出,计数器值都不会丢失
// 在构造器添加需要初始化赋予的对象或类型
class MainActivityViewMode(countReserved : Int) : ViewModel()
{
var counter = countReserved
}
class MainViewModeFactory(
private val countReserved: Int
) : ViewModelProvider.Factory
{
override fun <T : ViewModel> create(modelClass: Class<T>): T
{
// 因为这里实例化执行时机和Activity/Fragment生命周期无关,
// 所以可以创建
return MainActivityViewMode(countReserved) as T
}
}
对应地,只需要修改下列代码就能完成操作:
viewMode = ViewModelProvider(
this,
MainViewModeFactory(countReserved)
)[MainActivityViewMode::class.java]
完整代码:
class MainActivity : AppCompatActivity()
{
private lateinit var textView: TextView
lateinit var viewMode: MainActivityViewMode
lateinit var sp : SharedPreferences
@SuppressLint("SetTextI18n")
@RequiresApi(Build.VERSION_CODES.S)
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
sp = getPreferences(Context.MODE_PRIVATE)
val countReserved = sp.getInt("counter", 0)
viewMode = ViewModelProvider(
this,
MainViewModeFactory(countReserved)
)[MainActivityViewMode::class.java]
val scrollView = ScrollView(this)
val linearLayout = LinearLayout(this)
linearLayout.orientation = LinearLayout.VERTICAL
scrollView.addView(linearLayout)
textView = TextView(this).apply {
textSize = 40f
}
linearLayout.addView(textView)
linearLayout.addView(Button(this).apply {
setOnClickListener {
viewMode.counter++
refreshCount()
}
})
// 创建时刷新
refreshCount()
setContentView(scrollView)
}
private fun refreshCount()
{
textView.text = viewMode.counter.toString()
}
override fun onPause()
{
super.onPause()
sp.edit {
putInt("counter", viewMode.counter)
}
}
}
Lifecycles
简介
Lifecycles可以用来感知Activity生命周期。它独特的地方在于,可以再一个非Activity类中感知Activity的生命周期。
有一种解决方案是,自己创建一个监听器,重写Activity的onStart()、onCreate()方法等建立监听:
class MyObserver
{
fun onStart() {}
fun onStop() {}
}
class MyActivity : AppCompatActivity()
{
lateinit var observer: MyObserver
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
observer = MyObserver()
}
override fun onStart()
{
super.onStart()
observer.onStart()
}
override fun onStop()
{
super.onStop()
observer.onStop()
}
}
而Lifecycles可以轻易实现,又不用些大量逻辑处理。
简单的实现方法是:
class MyObserver : LifecycleObserver
{
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onStart() {}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onStop() {}
}
只需要添加注解,就能实现对应方法监听,Lifecycles内置了七种枚举字段,它们分别是:
public enum Event {
/**
*LifecycleOwner的onCreate事件的常量。
*/
ON_CREATE,
/**
* LifecycleOwner的onStart事件的常量。
*/
ON_START,
/**
* LifecycleOwner的onResume事件的常量。
*/
ON_RESUME,
/**
* LifecycleOwner的onPause事件的常量。
*/
ON_PAUSE,
/**
* LifecycleOwner的onStop事件的常量。
*/
ON_STOP,
/**
* LifecycleOwner的onDestroy事件的常量。
*/
ON_DESTROY,
/**
*可用于匹配所有事件的事件常量。
*/
ON_ANY;
....
}
如何监听?
由于继承自AppCompatActivity
或andeoidx的Fragment
已经是LifecycleOwner实例,所以可以这么监听:
lifecycle.addObserver(MyObserver())
否则,你需要自己实现一个LifecycleOwner实例,调用方法如下:
lifecycleOwner.lifecycle.addObserver(MyObserver())
主动获取状态
class MyObserver(val lifecycle: Lifecycle) : LifecycleObserver
{
...
}
只需要传递Lifecycle,调用Lifecycle.currentState
就能主动获取五种不同的生命周期状态,具体值的意思这里不详细说明,请自行查阅。
比如,获取的是CAEATED,说明onCreate()
已经执行了,但onStart()
还没有。获取的是STARTED,则onStart()
执行了,而onResume()
还没有。
LiveData
是响应式组件,它能够在数据发生变化时通知给观察者。LiveData大多情况是与ViewMode配合使用,尽管它能单独使用。
基本用法
如果说,ViewMode的作用是解耦,让Activity界面数据分离的话,那么LiveData就是去给响应数据变化提醒(因为在ViewMode中,并没有主动响应变化的值,而是修改后调用refreshCount()
方法)。
主动提醒值变化,有利于耗时操作和统一管理数据修改,不至于界面数据修改写的一塌糊涂。
比如说,如果ViewMode内部开启了线程去执行耗时逻辑,那么在点击按钮后去执行refreshCount()
获取值,显然没有新值,而处理完后的结果值,又恰恰无法主动显示。这就是LiveData重心。
不要把Activity传给ViewMode这样会导致回收困难,内存泄露。一个周期较短的类不能把实例传给较长的类保管,也不能静态引用周期短的类。
LiveData基本使用
class MainActivityViewMode(countReserved : Int) : ViewModel()
{
var counter = MutableLiveData<Int>()
init {
// 这样以前保存的默认值就能恢复
counter.value = countReserved
}
fun add() {
counter.value = counter.value?.plus(1)
}
}
MutableLiveData 是一种可变的LiveData,他有三种读写数据的方式:
getValue()
获取数据setValue()
设置数据,只能在主线程调用postValue()
非主线程中设置数据
这样设置好的值,响应值写法如下:
linearLayout.addView(Button(this).apply {
setOnClickListener {
viewMode.add()
}
})
viewMode.counter.observe(this) {
textView.text = it.toString()
}
也就是只要修改值即可,不用手动更新。
暴露的问题
counter
这个可变的变量暴露出去了,这就导致设置形同虚设:既然想方设法把修改内容局限在只能引用ViewMode的方法以便于响应数据,就不能暴露其他任何修改的可能,解决方法之一是:只暴露不可变的LiveData给外部,因此如下改动:
// 在构造器添加需要初始化赋予的对象或类型
class MainActivityViewMode(countReserved : Int) : ViewModel()
{
val counter : LiveData<Int>
get() = privateCounter
private var privateCounter = MutableLiveData<Int>()
init {
privateCounter.value = countReserved
}
fun add() {
privateCounter.value = privateCounter.value?.plus(1)
}
}
这种写法最为规范,也是官方推荐的写法。
详细代码:
class MainActivity : AppCompatActivity()
{
private lateinit var textView: TextView
lateinit var viewMode: MainActivityViewMode
lateinit var sp : SharedPreferences
@SuppressLint("SetTextI18n")
@RequiresApi(Build.VERSION_CODES.S)
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
sp = getPreferences(Context.MODE_PRIVATE)
val countReserved = sp.getInt("counter", 0)
viewMode = ViewModelProvider(
this,
MainViewModeFactory(countReserved)
)[MainActivityViewMode::class.java]
// ui操作
val scrollView = ScrollView(this)
val linearLayout = LinearLayout(this)
linearLayout.orientation = LinearLayout.VERTICAL
scrollView.addView(linearLayout)
textView = TextView(this).apply {
textSize = 40f
}
linearLayout.addView(textView)
linearLayout.addView(Button(this).apply {
setOnClickListener {
viewMode.add()
}
})
viewMode.counter.observe(this) {
textView.text = it.toString()
}
setContentView(scrollView)
}
override fun onPause()
{
super.onPause()
sp.edit {
putInt("counter", viewMode.counter.value ?:0 )
}
}
}
MainActivityViewMode.kt内:
// 在构造器添加需要初始化赋予的对象或类型
class MainActivityViewMode(countReserved : Int) : ViewModel()
{
val counter : LiveData<Int>
get() = privateCounter
private var privateCounter = MutableLiveData<Int>()
init {
privateCounter.value = countReserved
}
fun add() {
privateCounter.value = privateCounter.value?.plus(1)
}
}
class MainViewModeFactory(
private val countReserved: Int
) : ViewModelProvider.Factory
{
override fun <T : ViewModel> create(modelClass: Class<T>): T
{
// 因为这里实例化执行时机和Activity/Fragment生命周期无关,
// 所以可以创建
return MainActivityViewMode(countReserved) as T
}
}
map() 与 switchMap()
map()
&emspmap()
方法作用是:将实际包含数据的LiveData转换成仅用于观察的数据,也就是不照搬LiveData内数据,而是进行一定的修改。
比如说数据类包含了用户的姓名、年龄、性别:
data class User(var name : String, var age : Int, var sex : String)
由上一节内容知道,不能直接暴露,要用一个只读变量做桥梁:
class MainActivityViewMode(countReserved : Int) : ViewModel()
{
val user : LiveData<User>
get() = privateUser
private val privateUser = MutableLiveData<User>()
}
但假如只需要用户姓名和性别就够了,而再暴露额外的年龄不符合开发规范,map就解决了这个问题:
这里,只需要观察user就可以了
class MainActivityViewMode(countReserved : Int) : ViewModel()
{
// 顺序不能变 - Bug? 2022/7/7
private val privateUser = MutableLiveData<User>()
val user : LiveData<String> = Transformations.map(privateUser) {
"${it.name}, ${it.sex}"
}
}
switchMap()
虽然switchMap()比map()使用场景更加固定,但它更为常见:上文的User是在ViewMode创建的,这并不常见,更多的,是调用其他方法获取而来。
这里先定义类:
object Repository
{
// 每次调用,都会获取新的实例
fun getUser(userId : Int = 0): LiveData<User>
{
val liveData = MutableLiveData<User>()
liveData.value = User(age = userId)
return liveData
}
}
然后再在ViewMode添加方法:
fun getUser(userId: Int): LiveData<User> = Repository.getUser(userId)
这样子在主Activity中,如下引用是完全错误的,因为getUser(id)
调用获取的值是不变的,而再次调用,依然会重新观察老的实例,这样的情况下,这个类是不可观察的。
viewMode.getUser(id).observe(this) {}
而switchMap的作用就出来了:将函数
viewMode.getUser(id)
中返回的对象变成一个可观察的对象
这里,在适当的时机调用getUser()
,只需要观察user就可以了。
class MainActivityViewMode(countReserved : Int) : ViewModel()
{
private val userIdData = MutableLiveData<Int>()
val user : LiveData<User> = Transformations.switchMap(userIdData) {
Repository.getUser(it)
}
fun getUser(userId: Int) {
userIdData.value = userId
}
}
工作流程总结
- 外部调用
MainActivityViewMode
的getUser()
获取用户数据,不会发送任何对外请求,只是userIdData 数据发生变化而提醒 - userIdData 数据发生变化,
switchMap
就会执行,才会对外请求对象并存放到user变量中。 - 对于Activity/Fragment而言,只要观察
user
就行
没有参数
工作流程中有这么一段话:userIdData 数据发生变化而提醒,假如,并没有传递的参数,是不是无从谈起?答案是否定的,可以如下写:
class MainActivityViewMode(countReserved : Int) : ViewModel()
{
private val refreshData = MutableLiveData<Any?>()
val user : LiveData<User> = Transformations.switchMap(refreshData) {
Repository.refresh()
}
fun getUser(userId: Int) {
refreshData.value = refreshData.value
}
}
也就是说,LiveData并不关心数据是否相同,只要有调用setValue
或postValue
,就一定触发数据变化
后记
- LiveData能作为Activity与ViewMode之间的通信桥梁,并且不存在内存泄漏的风险,靠的就是Lifecycles组件进行自我感知,在Activity销毁时及时释放,避免产生内存泄露放入问题
- Activity在不可见的时候(息屏、被其他Activity遮挡),LiveData如果数据发生变化,也不会通知给观察者,只有恢复时,才进行通知
- Activity在不可见的时候,如果数据发生了多次变化,当Activity恢复时,LiveData只会保留最新的一份数据并提供给观察者,其他旧的数据会被丢弃,这些功能也少不了Lifecycles组件
Room
这是一个基于数据库的ORM框架。这部分自行百度。
WorkManager
WorkManager适用于处理一些定时任务,他可以根据操作系统版本自动选择底层是AlarmManager实现还是JobScheduler,从而减低使用成本。它还支持周期性任务、链式任务处理等。
与Service没有直接联系,并不相同。Serivce在没有被销毁时是一直保持后台运行的,而WorkManager只是一个定时处理工具,它可以保证即使应用程序退出,甚至手机重启,之前的任务仍然会得到执行,因此适合执行一些定期和服务器交互内容。
另外,它所注册的周期性任务不一定准时执行。系统为了减少电池消耗,可能会触发时间临近的几个任务一起执行,这样以减少CPU被唤醒次数。
基本用法
添加依赖:
implementation 'androidx.work:work-runtime:2.7.0-alpha04'
主要工作分三步:
- 定义一个后台任务,实现具体业务逻辑
- 配置后台任务运行条件和约束信息,并构建后台任务请求
- 将后台任务传递给
WorkManager
的enqueue()
方法,系统就能得以执行。
第一步:
class SimpleWork(context: Context, params: WorkerParameters) : Worker(context, params)
{
override fun doWork(): Result
{
return Result.success()
}
}
doWork()
不会在主线程运行,要求返回的对象,可以有Result.success()
、Result.failure()
,此外,还有Result.retry()
,在表示失败的同时,可以结合WorkResult.Builder
的 setBackoffCriteria() 重新执行任务,之后会说。
第二步,这里只进行简单配置:
val request = OneTimeWorkRequest.Builder(SimpleWork::class.java).build()
OneTimeWorkRequest用于一次请求,是WorkRequest的子类。也有子类PeriodicWorkRequest,用于创建周期性后台任务请求,下列示例代码是周期不小于15分钟的写法:
val request = PeriodicWorkRequest.Builder(
SimpleWork::class.java,
15,
TimeUnit.MINUTES
).build()
最后提交就行:
WorkManager.getInstance(context).enqueue(request)
使用WorkManager处理复杂任务
上文并不能控制具体时间,其实用处也不大,这里进行更复杂的控制设置。
延时执行
延时5分钟
val request = OneTimeWorkRequest.Builder(SimpleWork::class.java)
.apply {
setInitialDelay(5, TimeUnit.MINUTES)
build()
}
标签与关闭
添加标签,最主要的功能是通过标签取消后台请求:
val request = OneTimeWorkRequest.Builder(SimpleWork::class.java)
.apply {
setInitialDelay(5, TimeUnit.MINUTES)
addTag("tag")
build()
}
没有标签,也可以用id来取消:
WorkManager.getInstance(this).apply {
cancelAllWorkByTag("tag")
cancelWorkById(request.id)
// 取消所有任务
cancelAllWork()
}
使用标签,可以把同一标签的所有任务取消(也就是标签可以重复命名,区别于id)
Result.retry()
结合WorkResult.Builder
的 setBackoffCriteria() 重新执行任务:
val request = OneTimeWorkRequest.Builder(SimpleWork::class.java)
.apply {
setBackoffCriteria(
BackoffPolicy.LINEAR,
10,
TimeUnit.MINUTES
)
build()
}
setBackoffCriteria()
后两个参数很好理解,就是指定多久后执行任务。第一个参数是,多次失败后,下次执行失败后,以什么样的延迟方式执行。LINEAR
表示线性延迟,EXPONENTIAL
则是指数型。
Result的success和failure
这两个返回值是用于通知任务运行结果的,可以如下对后台监听:
WorkManager.getInstance(this).apply {
getWorkInfoByIdLiveData(request.id).observe(this@MainActivity) {
when(it.state)
{
WorkInfo.State.SUCCEEDED -> {
TODO("成功时执行")
}
WorkInfo.State.FAILED -> {
TODO("失败时执行")
}
else -> {}
}
}
}
这样就能监听数据结果了,当然,你知道了有getWorkInfoByIdLiveData() 自然也有基于Target的方法。
链式任务(任务链)
https://juejin.cn/post/6966852605825777678
总结
前面介绍的WorkManager功能,在国产手机上都可能得不到正确运行,因为大多数厂商会给予用户一键清除所有非白名单的功能,被杀死的程序既没法接收广播,也不能运行WorkManager任务。
所以建议是,不要依赖它实现核心功能,这不可靠。