3 个回答
看见这个问题没什么人回答,正好最近也在吃惊于Navigation的强大功能,就顺手过来回答一下吧。 本篇答案专注于Navigation组件该如何使用。
参考文献:
https:// developer.android.com/t opic/libraries/architecture/navigation/
前言
关于为什么要使用Navigation的原因,我认为有二:
- 需要构建单Activity多Fragment架构APP
- 需要使用深度链接Deep Link功能
#关于为什么要构建单Activity多Fragment架构的APP
我认为这是一个工程学上的Best Practice——业界权威Jake Wharton推荐,公认的优秀APP Airbnb在用,更不用提Google为此专门推出了Navigation组件。
#关于什么是深度链接
举一个例子:当我们在浏览知乎网页版知乎时,有一个「在APP中打开」,然后我们直接打开就是APP中该回答的那页,这使用的就是深度链接技术。
#回顾下传统的打开Activity和打开Fragment方式
- Intent 打开Activity
- 没有直接打开Fragment的方法,需要让Activity继承一个接口实现切换Fragment功能,然后在Fragment中使用Activity转型为该接口后的实例进行回调
#展望下现在的打开Activity和打开Fragment方式
- 统一的navigate()方法
一、使用Navigation前我们要知道的三个名词
- destination
- actions
- navigation graph
#1 destination
destination,中文直译为“目的地”,在这里可以指代三个内容:
- Activity
- Fragment
- Navigation Graph
其中,destination通常应该代表Fragment。而Navigation Graph为何物此时我们先不必管。
#2 actions
actions,中文直译为“行为”,在这里指代 将两个destination连接起来的线 :
如图所示,每一个矩形即destination,而连接它们的线就是action。
#3 Navigation Graph
准确的说,上面这张图就是一个Navigation Graph的预览图。Navigation Graph包含了Destination和Action,有时还会包含其它的Navigation Graph。下面是一个Navigation Graph的代码,以及它在项目中位置的截图:
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/blankFragment1">
<fragment
android:id="@+id/blankFragment1"
android:name="com.tipchou.navigationdemo.BlankFragment1"
android:label="fragment_blank1"
tools:layout="@layout/fragment_blank1">
<action
android:id="@+id/action_blankFragment1_to_blankFragment2"
app:destination="@id/blankFragment2"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_exit_anim"
app:popExitAnim="@anim/nav_default_pop_enter_anim" />
<argument
android:name="fragment1"
android:defaultValue="0" />
</fragment>
<fragment
android:id="@+id/blankFragment2"
android:name="com.tipchou.navigationdemo.BlankFragment2"
android:label="fragment_blank2"
tools:layout="@layout/fragment_blank2">
<argument
android:name="fragment2"
android:defaultValue="0" />
<deepLink app:uri="www.hearfresh.com/fuck" />
</fragment>
</navigation>
二、配置Navigation到你的项目中
遵循以下步骤,在Android Studio中进行设定:
- 点击File->Setting,选择Experimental,勾选Enable Navigation Editor,然后重启Android Studio
- 配置Gradle添加Navigation组件,如下图所示:
3. 右键点击res文件夹然后选择New->Android Resource File,找到navigation并创建
三、介绍一些关键图标
#1 创建新的Destination
#2 使用Action连接Destination
#3 指定起始 Destination
四、指定一个Activity去Host整个Navigation Graph
#通过在Activity的layout文件中添加如下内容
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<fragment
android:id="@+id/my_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
</android.support.constraint.ConstraintLayout>
#app:defaultNavHost="true"
这个属性会确保你的NavHostFragment拦截系统的返回按钮,让它不会一下子退出Activity,而是退回到上一个Fragment。
五、导航至某一Destination
我们使用navigate()方法去导航至某一Destination。此方法接收一个resource ID,这个ID可以是navigation graph中的:
- action的ID(推荐)
- destination的ID
使用Action的ID更被推荐是因为——我们可以绑定一些transitions(过渡动画)。
#navigate方法
textview.setOnClickListener { view1 ->
view1.findNavController().navigate(R.id.action_blankFragment1_to_blankFragment2)
}
#我们还可以通过创建一个OnClickListener对象来导航至某一Destination
button.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.blankFragment1))
六、在Destination之间传递数据
我认为在Destination之间传输数据可以使用共享Acitivity的ViewModel的方式,这也是Google推荐的方式。但是Destination之间也有原生的传递数据的方式:
- Bundle方式
- type-safe方式
这里只介绍Bundle方式:
注:本例所用的bundleOf()方法是Jake Wharton所写的KTX库中函数
#1 选择一个Destination,点击Arguments上的小加号,输入name\type\default value
#2 在代码中,使用nativgate()方法发送数据
var bundle = bundleOf("amount" to "shit") //amount是一个String类型的数据
view.findNavController().navigate(R.id.confirmationAction, bundle)
#3 在代码中,使用getArguments()方法接收数据
val tv = view.findViewById(R.id.textViewAmount)tv.text = arguments.getString("amount")
七、添加一个Listener来确认目前屏幕上显示的是哪个Destination
这个技术我认为是比较有用的,我们使用单一Activity多Fragment架构时,会遇到一个这样的问题:
- 有些界面是需要顶边栏和底边栏的,有些界面是不需要顶边栏和底边栏的
于是我们可以在Activity中添加一个监听器,动态的显示/隐藏顶边栏和底边栏。
#使用方法addOnNavigatedListener()
findNavController(R.id.my_nav_host_fragment)
.addOnNavigatedListener { _, destination ->
when (destination.id) {
R.id.blankFragment1 -> {
Log.e(MainActivity::javaClass.name, "Fragment1")
R.id.blankFragment2 -> {
Log.e(MainActivity::javaClass.name, "Fragment2")
}
八、创建一个深度链接(Deep Link)
这块我就不搬运谷歌的内容了,大家可以在文章上面给出的参考链接中自己寻找答案,我相信功夫不负有心人,如果作为一个Android开发者,连Google访问这个问题都解决不了的话————
那你还是要先解决一下为妙,因为国内的技术文章都是搬运,源头在Google。
就这样,再见。
Work Manager组件
如果工作始终要通过应用重启和系统重新启动来调度,则称之为永久性的工作。大多数应用都有在后台执行任务的需求,即通过持久性工作能够完成。Android为后台任务提供了多种解决方案,例如
JobScheduler
,
Loader
,
Service
等,但是这些Api若是使用不当,则可能导致耗电等系列问题。
WorkManager
是适用于持久性工作的推荐解决方案,其可以处理三种类型的永久性工作:
- 立即执行 :必须立即开始且很快就完成的任务,可以加急
- 长时间运行 :运行时间可能较长(有可能超过 10 分钟)的任务。
- 可延期执行 :延期开始并且可以定期运行的预定任务。
WorkManager
最低能兼容API Level 14,并且不需要你的设备安装有Google Play Services。
WorkManager
能依据设备的情况,选择不同的执行方案。在API Level 23+,通过
JobScheduler
来完成任务;而在API Level 23以下的设备中,通过
AlarmManager
和
Broadcast Receivers
组合完成任务。但无论采用哪种方案,任务最终都是交由
Executor
来完成。
WorkManager
无缝集成了
Coroutines
和
RxJava
,可以很方便插入异步API。
WorkManager
的一系列特性使其适用于需要可靠运行的工作,即使用户导航离开屏幕、退出应用或重启设备也不影响工作执行,例如:
- 向后端服务发送日志或分析数据
- 定期将应用数据与服务器同步
- 退出应用后还应继续执行的未完成任务
WorkManager
不适用于那些可在应用进程结束时安全终止的进程内后台工作
二、基本使用
使用
WorkManager
前,先将相关库导入项目中
dependencies {
def work_version = "xxx"
// (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"
// optional - Multiprocess support
implementation "androidx.work:work-multiprocess:$work_version"
WorkManager
的使用流程主要为:
-
创建一个后台任务
Worker
-
定义
WorkRequest
,配置运行任务的方式和时间 - 将任务提交给系统处理
-
观察
Worker
的进度或状态
2.1 核心类
Worker
类
Work
定义工作,即指定需要执行的任务,可以继承抽象的
Worker
类。
doWork()
方法在
WorkManager
提供的后台线程上异步运行任务,在其中实现具体逻辑
class UploadWorker(appContext: Context, workerParams: WorkerParameters):
Worker(appContext, workerParams) {
override fun doWork(): Result {
//......
//@return 任务的执行情况,成功,失败,还是需要重新执行
return Result.success()
根据
doWork()
返回值判断任务执行情况:
-
Worker.Result.SUCCESS
:任务执行成功。 -
Worker.Result.FAILURE
:任务执行失败。 -
Worker.Result.RETRY
:任务失败需要重新执行。需要配合WorkRequest.Builder
里面的setBackoffCriteria()
函数使用
WorkRequest
类
定义完工作,需要使用
WorkManager
服务进行调度才能使得工作执行。
WorkRequest
及其子类则定义了工作运行方式和时间,例如指定任务应该运行的环境,任务的输入参数,任务只有在有网的情况下执行等等。
WorkRequest
是抽象类,其有两个子类:
-
OneTimeWorkRequest
(任务只执行一遍) -
PeriodicWorkRequest
(任务周期性的执行)
其中
WorkRequest.Builder
为创建
WorkRequest
对象的帮助类;
Constraints
为指定任务运行的限制条件;
Constraint.Builder
来创建
Constraints
val uploadWorkRequest: WorkRequest =
OneTimeWorkRequestBuilder<UploadWorker>()
.build()
WorkManager
类
用于排队和管理工作请求,将
WorkRequest
对象传递到
WorkManager
的任务队列
WorkManager
.getInstance(myContext)
.enqueue(uploadWorkRequest)
WorkInfo
类
工作在整个生命周期内会经历一系列
State
更改,而
WorkInfo
用于包含其特定的任务信息与状态。
工作状态分为:
ENQUEUED
、
RUNNING
、
SUCCEEDED
、
FAILED
、
BLOCKED
与
CANCELLED
。
-
一次性工作的状态
对于一次性工作请求,初始状态为ENQUEUED
。
ENQUEUED
状态下,工作满足Constraints
和初始延迟计时要求后立即运行,工作状态转为RUNNING
状态,然后根据结果转为SUCCEEDED
、FAILED
。如果结果是retry
,重回到ENQUEUED
状态。在此过程中可随时取消工作,状态变更为CANCELLED
。
SUCCEEDED
、
FAILED
和
CANCELLED
均表示此工作的终止状态。处于此状态时,
WorkInfo.State.isFinished()
返回
true
。
-
定期工作的状态
成功和失败状态仅适用于一次性工作和链式工作,定期工作只有一个终止状态CANCELLED
-
链式工作的状态
关于链式工作,其多了个BLOCKED
状态。关于链式工作状态,可直接参看 链接和工作状态
可以使用
LiveData
或
Future
保存
WorkInfo
对象,监听任务状态。
public abstract @NonNull LiveData<WorkInfo> getWorkInfoByIdLiveData(@NonNull UUID id);
public abstract @NonNull ListenableFuture<WorkInfo> getWorkInfoById(@NonNull UUID id);
public abstract @NonNull LiveData<List<WorkInfo>> getWorkInfosByTagLiveData(
@NonNull String tag);
......
2.2 WorkRequest定义
先定义一个
Worker
,继承
Worker
类,实现
doWork()
方法,编写需要在任务中执行的代码
class TestWorker(context:Context,workerParameters: WorkerParameters):Worker(context,workerParameters) {
override fun doWork(): Result {
Log.d("TestWorker","doWork ${System.currentTimeMillis()}")
return Result.success()
工作是通过
WorkRequest
在
WorkManager
中进行定义和配置才能运行
任务触发约束配置
可以通过
WorkRequest
设置任务触发条件,如设备处于充电、网络已连接等环境才触发任务
var constraints: Constraints = Constraints.Builder()
.setRequiresCharging(true)
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
var workRequest = OneTimeWorkRequestBuilder<TestWorker>().setConstraints(constraints).build()
关于
WorkManager
的约束如下所示
NetworkType | 约束运行工作所需的网络类型。例如 Wi-Fi (UNMETERED)。 |
---|---|
BatteryNotLow | 如果设置为 true,那么当设备处于“电量不足模式”时,工作不会运行。 |
RequiresCharging | 如果设置为 true,那么工作只能在设备充电时运行。 |
DeviceIdle | 如果设置为 true,则要求用户的设备必须处于空闲状态,才能运行工作。在运行批量操作时,此约束会非常有用;若是不用此约束,批量操作可能会降低用户设备上正在积极运行的其他应用的性能。 |
StorageNotLow | 如果设置为 true,那么当用户设备上的存储空间不足时,工作不会运行。 |
如果在工作运行时不再满足某个约束,
WorkManager
将停止工作器。系统将在满足所有约束后重试工作。
任务调度周期配置
WorkRequest
是抽象类,其有两个子类:
OneTimeWorkRequest
(任务只执行一遍)与
PeriodicWorkRequest
(任务周期性的执行)
调度一次性工作:
如果无需额外配置,可使用静态方法
from
val workRequest = OneTimeWorkRequest.from(TestWorker::class.java)
对于复杂的工作,则可以使用构建器
val workRequest = OneTimeWorkRequestBuilder<TestWorker>()
//...... 添加额外配置
.build()
调度定期工作:
有时可能需要定期运行某些工作,例如定期备份数据,定期上传日志等,可用
PeriodicWorkRequest
//工作运行时间间隔设置为1小时
val workRequest = PeriodicWorkRequestBuilder<TestWorker>(1,TimeUnit.HOURS).build()
时间间隔定义为两次重复执行之间的最短时间,可以定义的最短重复间隔是 15 分钟。
如果需要运行的工作对运行时间敏感,可以将
PeriodicWorkRequest
配置为在每个时间间隔的灵活时间段内运行
灵活时间段从
repeatInterval - flexInterval
开始,一直到间隔结束
//在每小时的最后 15 分钟内运行的定期工作
val workRequest = PeriodicWorkRequestBuilder<TestWorker>(1,TimeUnit.HOURS,15,TimeUnit.MINUTES).build()
其中间隔必须大于或等于
PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS
,而灵活间隔必须大于或等于
PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS
延迟工作配置
如果工作没有约束,或者当工作加入队列时所有约束都得到了满足,那么系统可能会选择立即运行该工作。如果您不希望工作立即运行,可以将工作指定为在经过一段最短初始延迟时间后再启动。
可以通过
setInitialDelay()
方法,延后任务的执行
var workRequest = OneTimeWorkRequestBuilder<TestWorker>().setConstraints(constraints)
.setInitialDelay(10,TimeUnit.MINUTES)//符合触发条件后,延迟10 minutes执行
.build()
定期工作只有首次运行时会延迟。
工作重试与退避配置
假如
Worker
线程的执行出现了异常,比如服务器宕机,你可能希望过一段时间,重试该任务,那么你可以在
Worker
的
doWork()
方法中返回
Result.retry()
,系统会有默认的指数退避策略来帮你重试任务
- 退避延迟时间指定了首次尝试后重试工作前的最短等待时间。此值不能超过 10 秒(或 MIN_BACKOFF_MILLIS )。
-
退避政策定义了在后续重试过程中,退避延迟时间随时间以怎样的方式增长。WorkManager 支持 2 个退避政策,即
LINEAR
和EXPONENTIAL
。
默认政策是
EXPONENTIAL
,延迟时间为10S
workRequest = OneTimeWorkRequestBuilder<TestWorker>()
.setBackoffCriteria(BackoffPolicy.LINEAR,OneTimeWorkRequest.MIN_BACKOFF_MILLIS,TimeUnit.MILLISECONDS)
.build()
示例中,最短退避延迟时间设置为允许的最小值,即 10 秒。由于政策为
LINEAR
,每次尝试重试时,重试间隔都会增加约 10 秒。例如,第一次运行以
Result.retry()
结束并在 10 秒后重试;然后,如果工作在后续尝试后继续返回
Result.retry()
,那么接下来会在 20 秒、30 秒、40 秒后重试,以此类推。如果退避政策设置为
EXPONENTIAL
,那么重试时长序列将接近 20、40、80 秒,以此类推。
工作标记配置
每个工作请求都有一个 唯一标识符 ,该标识符可用于在以后标识该工作,以便 取消 工作或 观察其进度
如果有一组在逻辑上相关的工作,对这些工作项进行标记可能也会很有帮助。通过标记,您一起处理一组工作请求。
例如,
WorkManager.cancelAllWorkByTag(String)
会取消带有特定标记的所有工作请求,
WorkManager.getWorkInfosByTag(String)
会返回一个 WorkInfo 对象列表,该列表可用于确定当前工作状态。
var workRequest = OneTimeWorkRequestBuilder<TestWorker>()
.addTag("者文静斋")
.build()
可以向单个工作请求添加多个标记。这些标记在内部以一组字符串的形式进行存储。您可以使用
WorkInfo.getTags()
获取与
WorkRequest
关联的标记集。
从
Worker
类中,您可以通过
ListenableWorker.getTags()
检索其标记集。
数据交互配置
在实际开发业务中,执行后台任务的时候,都会传递参数。
WorkManager
和
Worker
之间的参数传递。数据的传递通过
Data
对象来完成。
Worker
类可通过调用
Worker.getInputData()
访问输入参数
class TestWorker(context:Context,workerParameters: WorkerParameters):Worker(context,workerParameters) {
override fun doWork(): Result {
val message = inputData.getString("个人公众号")
Log.d("TestWorker","doWork $message ${System.currentTimeMillis()}")
return Result.success()
var workRequest = OneTimeWorkRequestBuilder<TestWorker>()
.setInputData(workDataOf("者文公众号" to "者文静斋"))
.addTag("者文静斋")
.build()
同样,可以使用
Data
类输出返回值。可以在任务执行完成后,向
WorkManager
传递数据,即
Result.success(outputData)
class TestWorker(context:Context,workerParameters: WorkerParameters):Worker(context,workerParameters) {
override fun doWork(): Result {
val message = inputData.getString("个人公众号")
Log.d("TestWorker","doWork $message ${System.currentTimeMillis()}")
val output: Data = workDataOf("output" to "do work success")
return
Result.success(output)
WorkManager
通过
LiveData
的
WorkInfo.getOutputData()
,得到
Worker
传递过来的数据
注意
:
Data
只能用于传递一些小的基本类型数据,且数据最大不能超过
10kb
加急工作
WorkManager 2.7.0
引入了加急工作的概念,系统必须先为加急作业分配应用执行时间,然后才能运行作业。执行时间并非无限制,而是受配额限制。如果您的应用使用其执行时间并达到分配的配额,在配额刷新之前,您无法再执行加急工作。
可以调用
setExpedited()
来声明
WorkRequest
应该使用加急作业,以尽可能快的速度运行。
var workRequest = OneTimeWorkRequestBuilder<TestWorker>()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
配额不足时:
-
OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST
:导致作业作为普通工作请求运行 -
OutOfQuotaPolicy.DROP_WORK_REQUEST
:配额不足时取消请求
以 Android 12 为目标平台的应用在后台运行时无法再启动
前台服务
,但
一些特殊情况
除外。如果应用在后台运行时尝试启动前台服务,并且前台服务不符合任何特殊情况,则系统会抛出
ForegroundServiceStartNotAllowedException
。
对于Android12之后,可以使用
WorkManager
的加急作业替代;对于Android 12之前,
WorkManager
会在其平台上运行前台服务。
2.3 管理工作
定义完
Worker
和
WorkRequest
,需要将工作加入队列。将工作加入队列的最简单方法是调用
WorkManager
enqueue()
方法,然后传递要运行的
WorkRequest
。
......
WorkManager.getInstance(this).enqueue(workRequest)
唯一工作
将工作加入队列时需小心谨慎,避免重复。例如,应用可能会每 24 小时尝试将其日志上传到后端服务。如果不谨慎,即使作业只需运行一次,您最终也可能会多次将同一作业加入队列。为了实现此目标,可以将工作调度为 唯一工作 。
唯一工作可确保同一时刻只有一个具有特定名称的工作实例。唯一名称由开发者指定,仅与一个工作实例相关联。
唯一工作可用于一次性工作,也可用于定期工作。创建方法为
-
WorkManager.enqueueUniqueWork()
)(用于一次性工作) -
WorkManager.enqueueUniquePeriodicWork()
)(用于定期工作)
参数 | 含义 |
---|---|
uniqueWorkName | 唯一标识工作请求的String |
existingWorkPolicy | 此 enum 可告知 WorkManager:如果已有使用该名称且尚未完成的唯一工作链,应执行什么操作 |
work | 要调度的WorkRequest |
调度唯一工作时,您必须告知
WorkManager
在发生冲突时要执行的操作,其处理冲突的策略选项有4个:
-
REPLACE
:用新工作替换现有工作。此选项将取消现有工作 -
KEEP
:保留现有工作,并忽略新工作。 -
APPEND
:将新工作附加到现有工作的末尾。此政策将导致您的新工作 链接 到现有工作,在现有工作完成后运行。
现有工作将成为新工作的先决条件。如果现有工作变为CANCELLED
或FAILED
状态,新工作也会变为CANCELLED
或FAILED
。如果您希望无论现有工作的状态如何都运行新工作,请改用APPEND_OR_REPLACE
。 -
APPEND_OR_REPLACE
函数类似于APPEND
,不过它并不依赖于 先决条件 工作状态。即使现有工作变为CANCELLED
或FAILED
状态,新工作仍会运行
val sendLogsWorkRequest =
PeriodicWorkRequestBuilder<SendLogsWorker>(24, TimeUnit.HOURS)
.setConstraints(Constraints.Builder()
.setRequiresCharging
(true)
.build()
.build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
"sendLogs",
ExistingPeriodicWorkPolicy.KEEP,
sendLogsWorkRequest
上述代码在
sendLogs
作业已处于队列中的情况下运行,系统会保留现有的作业,并且不会添加新的作业。
观察任务状态
任务提交给系统后,通过
WorkInfo
获知任务状态。
WorkInfo
包含了任务的
id
,
tag
以及
Worker
对象传递过来的
outputData
,以及任务当前的状态。
// by id
workManager.getWorkInfoById(syncWorker.id) // ListenableFuture<WorkInfo>
// by name
workManager.getWorkInfosForUniqueWork("sync") // ListenableFuture<List<WorkInfo>>
// by tag
workManager.getWorkInfosByTag("syncTag") // ListenableFuture<List<WorkInfo>>
该查询会返回
WorkInfo
对象的
ListenableFuture
,该值包含工作的
id
、其标记、其当前的
State
以及通过
Result.success(outputData)
设置的任何输出数据。
利用每个方法的
LiveData
变种,您可以通过注册监听器来观察
WorkInfo
的变化。
//在某项工作成功完成后向用户显示消息
workManager.getWorkInfoByIdLiveData(syncWorker.id)
.observe(viewLifecycleOwner) { workInfo ->
if(workInfo?.state == WorkInfo.State.SUCCEEDED) {
Snackbar.make(requireView(),
R.string.work_completed, Snackbar.LENGTH_SHORT)
.show()
WorkManager 2.4.0
及更高版本支持使用
WorkQuery
对象对已加入队列的作业进行复杂查询。
WorkQuery
支持按工作的标记、状态和唯一工作名称的组合进行查询。
val workQuery = WorkQuery.Builder
.fromTags(listOf("syncTag"))
.addStates(listOf(WorkInfo.State.FAILED, WorkInfo.State.CANCELLED))
.addUniqueWorkNames(listOf("preProcess", "sync")
.build()
val workInfos: ListenableFuture<List<WorkInfo>> = workManager.getWorkInfos(workQuery)
WorkQuery
中的每个组件(标记、状态或名称)与其他组件都是
AND
逻辑关系。组件中的每个值都是
OR
逻辑关系。
WorkQuery
也适用于等效的 LiveData 方法
getWorkInfosLiveData()
。
取消和停止任务
取消任务
若不再需要运行先前加入队列的工作,可按照工作的
name
、
id
或与其关联的
tag
取消工作。
// by id
workManager.cancelWorkById(syncWorker.id)
// by name
workManager.cancelUniqueWork("sync")
// by tag
workManager.cancelAllWorkByTag("syncTag")
如果工作已经
完成
,系统不会执行任何操作。否则,工作的状态会更改为
CANCELLED
,之后就不会运行这个工作。
停止任务
正在运行的
Worker
可能会由于以下几种原因而停止运行:
- 明确要求取消它
-
如果是
唯一工作
,明确地将
ExistingWorkPolicy
为REPLACE
的新WorkRequest
加入到了队列中。旧的WorkRequest
会立即被视为已取消。 - 工作约束条件已不再满足
- 系统出于某种原因指示您的应用停止工作。如果超过 10 分钟的执行期限,可能会发生这种情况。该工作会调度为在稍后重试。
上述情况,工作器会停止。可通过
onStopped()
回调或
isStopped()
属性了解工作器何时停止,进行相关资源释放操作
-
onStopped()
回调
工作器停止后,WorkManager
会立即调用ListenableWorker.onStopped()
。替换此方法可关闭可能保留的所有资源。 -
isStopped()
属性
可以调用ListenableWorker.isStopped()
方法以检查工作器是否已停止。若工作器执行长时间运行操作或重复操作,应经常检查此属性,用作停止工作的信号。
任务进度更新与观察
WorkManager 2.3.0-alpha01
为设置和观察工作器的中间进度添加了一流的支持。
ListenableWorker
支持
setProgressAsync()
API,允许保留中间进度。开发者能可通过界面观察到的中间进度。进度由
Data
类型表示够设置
更新进度
在
Kotlin
中,您可以使用
CoroutineWorker
对象的
setProgress()
扩展函数来更新进度信息。
class ProgressWorker(context: Context, parameters: WorkerParameters) :
CoroutineWorker(context, parameters) {
companion object {
const val Progress = "Progress"
private const val delayDuration = 1L
override suspend fun doWork(): Result {
val firstUpdate = workDataOf(Progress to 0)
val lastUpdate = workDataOf(Progress to 100)
setProgress(firstUpdate)
delay(delayDuration)
setProgress(lastUpdate)
return Result.success()
观察进度
可以使用
getWorkInfoBy…() 或 getWorkInfoBy…LiveData()
方法,并引用
WorkInfo
。
WorkManager.getInstance(applicationContext)
// requestId is the WorkRequest id
.getWorkInfoByIdLiveData(requestId)
.observe(observer, Observer { workInfo: WorkInfo? ->
if (workInfo != null) {
val progress = workInfo.progress
val value = progress.getInt(Progress, 0)
// Do something with progress information
2.4 任务链
可以使用
WorkManager
创建工作链并将其加入队列。工作链用于指定多个依存任务并定义这些任务的运行顺序,尤其是需要以特定顺序运行多个任务时。
创建任务链可以使用
WorkManager.beginWith(OneTimeWorkRequest)
或
WorkManager.beginWith(List)
等方法
WorkManager.getInstance(this)
.beginWith(workA)
.then(workB)
.then(workC)
.enqueue()
任务会按照设置的顺序依次执行A、B、C。
WorkManager
在执行过程中,遇到其中一个
Work
不成功,会停止执行。比如代码执行到
WorkB
返回
FAILURE
状态,代码结束,
WorkC
不执行
WorkManager.getInstance(this)
.beginWith(listof(plantName1, plantName2, plantName3) // 三个对象将并行作业
.then(cache)// 执行完3个plantName后,再执行cache
.then(upload) //...
.enqueue()
输入合并器
链接
OneTimeWorkRequest
实例时,父级工作请求的输出将作为子级的输入传入。上面的示例中,
plantName1
、
plantName2
和
plantName3
的输出将作为
cache
请求的输入传入。
WorkManger
使用
InputMerger
管理多个父级工作请求输入。
WorkManager 提供两种不同类型的
InputMerger
:
-
OverwritingInputMerger
会尝试将所有输入中的所有键添加到输出中。如果发生冲突,它会覆盖先前设置的键。 -
ArrayCreatingInputMerger
会尝试合并输入,并在必要时创建数组。
OverwritingInputMerger
是默认的合并方法。如果合并过程中存在键冲突,键的最新值将覆盖生成的输出数据中的所有先前版本。
例如,如果每种植物的输入都有一个与其各自变量名称(
"plantName1"
、
"plantName2"
和
"plantName3"
)匹配的键,传递给
cache
工作器的数据将具有三个键值对。
如果存在冲突,那么最后一个工作器将在争用中“取胜”,其值将传递给
cache
。
假设我们要保留所有植物名称工作器的输出,则应使用
ArrayCreatingInputMerger
val cache: OneTimeWorkRequest = OneTimeWorkRequestBuilder<PlantWorker>()
.setInputMerger(ArrayCreatingInputMerger::class)
.setConstraints(constraints)
.build()
ArrayCreatingInputMerger
将每个键与数组配对。如果每个键都是唯一的,您会得到一系列一元数组
如果存在任何键冲突,所有对应的值会分组到一个数组中
2.5 自定义WorkManager
默认情况下,应用启动时,
WorkManager
使用适合大多数应用的合理选项自动进行配置。如果需要进一步控制
WorkManager
管理和调度工作的方式,可以通过自行初始化
WorkManager
来自定义
WorkManager
配置。
按需初始化
通过按需初始化,可以在仅需要
WorkManager
时创建该组件,不用每次应用启动都创建,提高应用启动性能。
如需提供自己的配置,必须先移除默认初始化程序,使用合并规则
tools:node="remove"
更新
AndroidManifest.xml
。
从
WorkManager 2.6
开始,应用启动功能在
WorkManager
内部使用。若需提供自定义初始化程序,需移除
androidx.startup
节点
<!-- 如果您不在应用中使用应用启动功能,则可以将其彻底移除 -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove">
</provider>
<!-- 仅移除 WorkManagerInitializer 节点 -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<!-- If you are using androidx.startup to initialize other components -->
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
实现
Configguration.Provider
让您的
Application
类实现
Configuration.Provider
接口,并提供您自己的
Configuration.Provider.getWorkManagerConfiguration()
实现。
class App : Application(), Configuration.Provider {
override fun getWorkManagerConfiguration() =
Configuration.Builder()
.setWorkerFactory(RenameWorkerFactory())
.setMinimumLoggingLevel(Log.VERBOSE)
.
build()
如需查看可用自定义的完整列表,请参阅
Configuration.Builder()
参考文档
2.6 自定义WorkerFactory
使用
WorkManager
时,需要定义一个
Worker
的子类。在某个时刻,
WorkManager
会通过
WorkerFactory
反射实例化你定义的
Worker
。默认的
WorkerFactory
创建
Worker
需要两个参数:
-
Application’s Context
-
WorkerParameters
实际开发中,可能需要为
Worker
构造函数添加其他参数,满足我们的功能需求,例如需要引用
Retrofit
服务跟远程服务器进行通讯,这时就需要自定义
WorkerFactory
。
创建自定义
WorkerFactory
的步骤其实就是在自定义
WorkManager
步骤基础上加上对自定义的
WorkerFactory
的设置
class MyWorkerFactory(private val service: UpvoteStoryHttpApi) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker? {
return UpvoteStoryWorker(appContext, workerParameters, DesignerNewsService)
class MyApplication : Application(), Configuration.Provider {
override fun getWorkManagerConfiguration(): Configuration =
Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.DEBUG)
.setWorkerFactory(MyWorkerFactory(DesignerNewsService))
.build()
DelegatingWorkerFactory
使用
实际开发中可能会有多个
Worker
类,示例中直接返回某个特定
Worker
实例的操作就不是很适用,可以通过
DelegatingWorkerFactory
解决。
可以使用
DelegatingWorkerFactory
,将其设置到我们的
WorkerFactory
中,从而支持多个多个工厂。这种情况下,如果一个
WorkerFactory
没有创建
Worker
,
DelegatingFactory
会去找到下一个
WorkerFactory
class MyWorkerFactory(private val service: DesignerNewsService) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker? {
return when(workerClassName) {
UpvoteStoryWorker::class.java.name ->
ConferenceDataWorker(appContext, workerParameters, service)
else ->
// 返回 null,这样基类就可以代理到默认的 WorkerFactory
上述情况,工厂会检查是否知道如何处理作为参数传入的
workerClassName
,如果不知道,就返回
null
,而
DelegatingWorkerFactory
便会去寻找下一个注册的工厂。如果没有任何被注册的工厂知道如何处理某个类,那么它将回退到使用反射的默认工厂。
同时我们需要修改一下前述的
WorkManager
配置
class MyApplication : Application(), Configuration.Provider {
override fun getWorkManagerConfiguration(): Configuration {
val myWorkerFactory = DelegatingWorkingFactory()
myWorkerFactory.addFactory(MyWorkerFactory(service))
// 在这里添加您应用中可能会使用的其他 WorkerFactory
return Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.INFO)
.setWorkerFactory(myWorkerFactory)
.build()
2.7 线程处理
WorkManager
提供了四种不同类型的工作基元:
-
Worker
最简单的实现,WorkManager
会在后台线程中自动运行该基元(您可以将它替换掉) -
CoroutineWorker
Kotlin
用户建议实现。CoroutineWorker
实例公开了后台工作的一个挂起函数。默认情况下,这些实例运行默认的Dispatcher
,也可以自行自定义 -
RxWorker
RxJava
建议实现。如果很多异步代码是用RxJava
建模的,则应使用RxWorker
。与所有RxJava
概念一样,可以自由选择所需的线程处理策略 -
ListenableWorker
Worker
、CoroutineWorker
和RxWorker
的基类,专为需要与基于回调的异步 API(例如FusedLocationProviderClient
)进行交互并且不使用RxJava
的Java
开发者而设计
Worker
中线程处理
使用
Worker
时,
WorkManager
会自动在后台线程中调用
Worker.doWork()
。该后台线程来自
WorkManager
的
Configuration
中指定的
Executor
。你可以通过自定义的方式修改
Executor
WorkManager.initialize(
context,
Configuration.Builder()
// Uses a fixed thread pool of size 8 threads.
.setExecutor(Executors.newFixedThreadPool(8))
.build())
Worker.doWork()
是同步调用,应以阻塞方式完成整个后台工作,并在方法退出时完成工作。如果在
doWork()
中调用异步API并返回
Result
,回调可能无法正常允许,这种情况,考虑使用
ListenableWorker
class DownloadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): ListenableWorker.Result {
repeat(100) {
if (isStopped) {
break
try {
downloadSynchronously("https://www.google.com")
} catch (e: IOException) {
return ListenableWorker.Result.failure()
return ListenableWorker.Result.success()
CoroutineWorker
中线程处理
WorkManager
为
协程
提供了一流的支持。
CoroutineWorker
中包含了
doWork
的挂起版本。
class CoroutineDownloadWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val data = downloadSynchronously("https://www.google.com")
saveData(data)
return Result.success()
CoroutineWorker.doWork()
是一个“挂起”函数。此代码不同于
Worker
,不会在
Configuration
中指定的
Executor
中运行,而是默认为
Dispatchers.Default
。您可以提供自己的
CoroutineContext
来自定义这个行为。
class CoroutineDownloadWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
withContext(Dispatchers.IO) {
val data = downloadSynchronously("https://www.google.com")
saveData(data)
return Result.success()
CoroutineWorker
通过取消协程并传播取消信号来自动处理停工情况。
可以使用
RemoteCoroutineWorker
(
ListenableWorker
的实现)将工作器绑定到特定进程。详细操作可参看
WorkManagerMultiProcessSample
ListenableWorker
中线程处理
如果需要处理基于回调的异步操作,这种情况无法依靠
Worker
来完成,因为器无法以阻塞方式完成工作,这时就需要
ListenableWorker
抽象方法
ListenableWorker.startWork()
会返回一个将使用操作的
Result
设置的
ListenableFuture
,其是一个
Future
,用于提供附加监听器和传播异常的功能。
class CallbackWorker(
context: Context,
params: WorkerParameters
) : ListenableWorker(context, params) {
override fun startWork(): ListenableFuture<Result> {
return CallbackToFutureAdapter.getFuture { completer ->
val callback = object : Callback {
var successes = 0
override
fun onFailure(call: Call, e: IOException) {
completer.setException(e)
override fun onResponse(call: Call, response: Response) {
++successes
if (successes == 100) {
completer.set(Result.success())
//添加取消监听器
completer.addCancellationListener(cancelDownloadsRunnable, executor)
repeat(100) {
downloadAsynchronously("https://example.com", callback)
callback
}
Navigation 组件
1. Navigation到底该如何正确的使用
相信大家对 Navigation 都有所耳闻,我不细说怎么用了,官方的讲解也很详细。我是想说一下到底该如何更好的使用这个组件。
这个组件其实是需要配合官方的 MVVM 架构使用的, ViewModel + LiveData 结合才能更好的展现出 Navigation 的优势。
在官方的讲解示例中没有用到 ViewModel 和 LiveData ,官方只是演示了 Navigation 怎么用怎么在页面之间传值,和这个组件的一些特性之类的。但真正用好还是要结合 ViewModel 和 LiveData 。
2. Navigation大家都以为的缺陷
起初我用
Navigation
的时候,最头疼的是当按下返回键回到上个页面的时候整个页面被重建了,这是开发中不想要的结果,很多时候大家都会去寻求一种方式:将官方的
replace
方式替换为
Hide
和
Show
。起初也是想到这个方式,然后结合在网上得到的资料自己写了一个方式
FragmentNavigatorHideShow
。
3. 然而这不是缺陷
但是很快啊,我发现这个方式(
Hide
和
Show
)存在严重的逻辑问题。
这里可以看到,有一些场景下,我们有某个页面可以打开和自己相同的页面,只不过是展示的数据不同而已。当我用
hide
和
show
的方式展示下个页面的时候,会发现打开的还是上个页面。当按下返回键之后,上个相同的页面不见了,新打开的页面和上个页面尽然是同一个对象,这肯定不符合业务逻辑。于是我又开始研究起
replace
的方式,当然我在使用这个
Navigation
的时候就采用了
MVVM
+
ViewModel
+
LiveData
,这时候我想起ViewModel是不受Fragment重建影响的。于是我打印了一下在使用
replace
方式下页面生命周期的变化。
从
HomeFragment
进入
MyFragmen
t生命周期变化:
可以看到,在
replace
之后
HomeFragment
并没有执行
onDestory
而是执行了
onDestoryView
这也使得页面必须要重建。而
onDestoryView
不会导致
ViewModel
的销毁。也就是说
ViewModel
还在,
ViewModel
中的
LiveData
所保存的数据也是存在的。当我按下返回键,重新回到
HomeFragment
页面理所当然的执行了
onViewCreated
,此时代码中页面对
ViewModel
中的
LiveData
所观察数据又重新进行了
observe
观察,因为
LiveData
之前保存过数据所以这段代码也理所当然的被执行了。页面上也重新填充了数据。
override fun initLiveData() {
viewModel.liveData.observe(this) {
Log.d(TAG, "data change : $it ")
textView.text = it
这个时候,你会发现,页面好像没有重建一样。我这才理解了谷歌的用意。它这步棋下的很巧啊。
也里所当然的我抛弃了 FragmentNavigatorHideShow ,又拥抱回了谷歌爸爸。
说回上面那个问题,当一个页面中可以打开自己的时候,在
FragmentNavigator
源码中只要是导航到下一个目的地就会重新创建一个新的
Fragment
,上一个
Fragment
会被加入回退栈里,所以才可以在
Fragment
中打开一个新的自己,来展示不同的信息。而
hide
和
show
的方式会每次都去查找之前有没有创建过这个页面,如果有,就Show,如果没有就创建。所以才会导致自己打开自己,永远都是同一个
Fragment
对象。
4. 那么到底该如何正确使用
到底该如何正确使用 Navigation ,这也是我这段时间使用的一点点经验。
Fragment
中的所有动态数据都由
ViewModel
中的
LiveData
保存。我们只监听
LiveData
的数据变化,这也符合
MVVM
的架构麻,当然还有一个Model我没说
Repository
,这个我就不解释了。
Fragment
之间传递的数据都交给
Bundle
页面重建的时候这些数据也会被保存,再次走一遍从
Bundle
中取数据的过程是完全不会报错的。所以页面上的数据不会被丢失了,而像RecyclerView,ViewPager之类的控件它们也会保存自己之前的状态,页面重建后,RecyclerView,ViewPager会记录自己滑动的位置的,这个不用担心,还有一点就是有一些控件,比如
CoordinatorLayout
你可能需要给它和它的子View控件一个Id才能保存滑动状态。
遵循这样的一个规则之后呢,就可以忽略这个页面重建的问题了。
5. Navigation的页面转场动画的一些问题
用过 Navigation 的都知道,页面转场动画要一个一个的添加,就像这样:
<!--这是官方的Demo示例-->
<fragment
android:id="@+id/title_screen"
android:name="com.example.android.navigationsample.TitleScreen"
android:label="fragment_title_screen"
tools:layout="@layout/fragment_title_screen">
<action
android:id="@+id/action_title_screen_to_register"
app:destination="@id/register"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"/>
<action
android:id="@+id/action_title_screen_to_leaderboard"
app:destination="@id/leaderboard"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"/>
</fragment>
每一个标签都要写一遍一样的代码,让我很头疼。于是我还是想到了,重写
FragmentNavigator
将所有的增加一个判断如果标签中没有设置专场动画,那么我就给这个
Fragment
添加上专场动画。
//我一开始设想的载源码位置处添加的动画操作
int enterAnim = navOptions != null ? navOptions.getEnterAnim() : 动画id;
int exitAnim = navOptions != null ? navOptions.getExitAnim() : 动画id;
int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : 动画id;
int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : 动画id;
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
然而我太天真了,我们想到的,谷歌爸爸都考虑过了。因为如果像我一样天真的加上这样的判断之后,你会发现,第一个默认
Fragment
也拥有了动画属性。而且做隐式链接跳转的时候,这个动画会非常影响观感。所以第一个默认
Fragment
不能有转场动画。当然后来我想到了判断返回栈是否存在为空,通过这个判断是否是第一个页面。但是我都能想到谷歌爸爸肯定也想到了。他们不这么做肯定是有原因的吧。还是等待官方优化,于是我放弃了,老老实实的挨个复制粘贴,
不过后来我在 Navigation 的 issues 找到了这个问题,因该在优化的计划中吧。
6. Replace在重建Fragment的时候,过度动画卡顿
在使用
Navigation
的时候,按下返回键回到上个页面,页面重建,这个时候会发现过度动画会有那么几百毫秒卡那么一下,一个转场动画也就400毫秒左右,卡那么一下效果是非常明显的。这也归功于
Fragment
重建的原因了,页面展示的数据量巨大的时候,重建时的绘制工作量也是相当的大,所以肯定会卡那么一下下啦。
后来我发现了一个方法:
override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
return super.onCreateAnimation(transit, enter, nextAnim)
我们可以把数据加载的过程放在动画执行之后再请求。
override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
if (enter) {
if (nextAnim > 0) {
val animation = AnimationUtils.loadAnimation(requireActivity(), nextAnim)
animation.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationEnd(animation: Animation?) {
onEnterAnimEnd()//动画结束后再去请求网络数据、或者初始化LiveData
return animation
} else {
onEnterAnimEnd()
} else {
if (nextAnim > 0) {
return AnimationUtils.loadAnimation(requireActivity(), nextAnim)
return super.onCreateAnimation(transit, enter, nextAnim)
* 子类重写,判断是否需要加载数据,或者初始化LiveData
fun onEnterAnimEnd(){
Log.d(TAG, "onEnterAnimEnd: ")
然后我们再找到
onViewCreated
方法,因为Base类我们通常会将初始化方法进行抽象所以我们要进行两个事情:
1: 在View进行绘制初始化的时候暂停过场动画
2: 在View与Data初始化结束后再开始动画的执行
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//暂停过场动画
postponeEnterTransition()
//View与数据初始化
initViewAndData(view)
initLiveData()//LiveData的初始化可以放到动画结束之后
//最后使用这个方法监听视图结构,并开始执行过场动画
(view.parent as? ViewGroup)?.apply {
OneShotPreDrawListener.add(this){