В моем текущем Android приложении занято WorkerManager
Рабочих для обработки фоновой работы.
Всякий раз, когда Работник активен, я sh показываю счетчик прогресса на панели инструментов моего приложения,
Кроме того, при сбое рабочего я показываю Toast
и SnackBar
с сообщением об ошибке
В настоящее время я контролирую как счетчик хода выполнения, так и Toast / SnackBar с помощью io.reactivex.subjects.BehaviorSubject<T>
Я пытаюсь заменить BehaviorSubject
на kotlinx.coroutines.channels.ConflatedBroadcastChannel
.
Мое текущее решение очень близко к тому, что мне требуется, однако я вижу некоторое неожиданное поведение.
Я ожидал увидеть Рабочее состояние изменяется следующим образом: -
WhatIsGoingOn -> Enqueued -> Running -> Succeeded -> Dormant
однако на самом деле происходит следующее
WhatIsGoingOn -> Enqueued -> Running -> Succeeded
, даже если Dormant
имеет значение offer
после Succeeded
.
Мне нужно было delay
"offer
" сделать Dormant, чтобы получить желаемую последовательность состояний.
Еще один неожиданный случай - это переход между действиями в моем приложении.
Моя заявка имеет четыре действия следующим образом
Root -> MainActivity -> DummyOne -> DummyTwo
Оба MainActivity
и DummyTwo
могут запускать фоновые рабочие, однако я хочу, чтобы все четыре действия реагировали на изменения состояния при переходе пользователя к ним.
Все четыре из вышеперечисленных операций расширяются следующая Base
активность: -
@ExperimentalCoroutinesApi
@FlowPreview
abstract class Base : AppCompatActivity(), CoroutineScope by MainScope() {
internal lateinit var mainLayout: View
internal lateinit var anyBusy: ProgressBar
internal lateinit var numericBusy: ProgressBar
internal lateinit var alphabeticBusy: ProgressBar
val endeavorManager = Endeavor4EverManager
init {
lifecycleScope.launchWhenResumed {
Endeavor4EverManager.workerStateFlow.distinctUntilChanged().collect { consumeWorkerState(it) }
}
}
@ExperimentalCoroutinesApi
fun showError(view: View?, stringId: Int) {
hideProgress()
showErrorToast(stringId)
showErrorSnackBar(view, stringId)
}
@ExperimentalCoroutinesApi
open fun hideProgress() {
endeavorManager.resetWorkerState()
}
private fun showErrorToast(stringId: Int) {
val toast = Toast.makeText(this, stringId, Toast.LENGTH_LONG)
toast.setGravity(Gravity.CENTER, 0, 0)
toast.show()
}
private fun showErrorSnackBar(view: View?, stringId: Int) {
if (view == null) {
return
}
Snackbar.make(view, stringId, Snackbar.LENGTH_SHORT).show()
}
abstract fun consumeWorkerState(workerState: WorkerState)
}
У меня есть запечатанный класс, который представляет возможные состояния, которые может иметь работник
sealed class WorkerState {
object WhatIsGoingOn : WorkerState()
object Dormant : WorkerState()
object Blocked : WorkerState()
object Cancelled : WorkerState()
object Enqueued : WorkerState()
object Failed : WorkerState()
object Running : WorkerState()
object Succeeded : WorkerState()
}
У меня есть одноэлементный WorkerManager: -
object Endeavor4EverManager : CoroutineScope by GlobalScope {
const val LAST_WORKER_UUID: String = "LAST_WORKER_UUID"
private const val TAG = "EndeavorManager"
@ExperimentalCoroutinesApi
private val workerStateChannel = ConflatedBroadcastChannel<WorkerState>(WorkerState.WhatIsGoingOn)
@ExperimentalCoroutinesApi
@FlowPreview
val workerStateFlow: Flow<WorkerState> = workerStateChannel.asFlow()
private val mutex = Mutex()
private const val NUMERIC_UNIQUE_WORK_NAME = "NUMERIC-UNIQUE-WORK-NAME"
private lateinit var finalWorkerWorkInfo: LiveData<WorkInfo>
private var finalWorkerObserver: Observer<WorkInfo>? = null
@ExperimentalCoroutinesApi
@MainThread
suspend fun doNumericWork(applicationContext: Context) {
mutex.withLock {
manageNumericWork(applicationContext)
}
}
@ExperimentalCoroutinesApi
private fun manageNumericWork(applicationContext: Context) {
val finalWorkerRequest: OneTimeWorkRequest = OneTimeWorkRequestBuilder<FinalWorker>().build()
val initialInputData: Data = workDataOf(LAST_WORKER_UUID to finalWorkerRequest.id.toString())
val initialWorkerRequest: OneTimeWorkRequest = OneTimeWorkRequestBuilder<InitialWorker>().setInputData(initialInputData).build()
val taskOneWorkerRequest: OneTimeWorkRequest = OneTimeWorkRequestBuilder<TaskOneWorker>().build()
val taskTwoWorkerRequest: OneTimeWorkRequest = OneTimeWorkRequestBuilder<TaskTwoWorker>().build()
val taskThreeWorkerRequest: OneTimeWorkRequest = OneTimeWorkRequestBuilder<TaskThreeWorker>().build()
val taskFourWorkerRequest: OneTimeWorkRequest = OneTimeWorkRequestBuilder<TaskFourWorker>().build()
WorkManager.getInstance(applicationContext)
.beginUniqueWork(NUMERIC_UNIQUE_WORK_NAME, ExistingWorkPolicy.KEEP, initialWorkerRequest)
.then(listOf(taskOneWorkerRequest, taskTwoWorkerRequest))
.then(taskThreeWorkerRequest)
.then(taskFourWorkerRequest)
.then(finalWorkerRequest)
.enqueue()
WorkManager.getInstance(applicationContext).getWorkInfoByIdLiveData(initialWorkerRequest.id).observeForever { workInfo ->
when {
workInfo == null -> {
//Intentionally left blank
}
workInfo.state == WorkInfo.State.SUCCEEDED -> {
observeFinalWorker(applicationContext, workInfo)
}
workInfo.state == WorkInfo.State.ENQUEUED ||
workInfo.state == WorkInfo.State.FAILED ||
workInfo.state == WorkInfo.State.BLOCKED ||
workInfo.state == WorkInfo.State.CANCELLED ||
workInfo.state == WorkInfo.State.RUNNING -> {
//Intentionally left blank
}
}
}
}
@ExperimentalCoroutinesApi
private fun observeFinalWorker(applicationContext: Context, workInfo: WorkInfo) {
val initialOutputData: Data = workInfo.outputData
val finalWorkerUuid: UUID = UUID.fromString(initialOutputData.getString(LAST_WORKER_UUID))
finalWorkerWorkInfo = WorkManager.getInstance(applicationContext).getWorkInfoByIdLiveData(finalWorkerUuid)
finalWorkerWorkInfo.observeForever(constructObserver())
}
@ExperimentalCoroutinesApi
private fun constructObserver(): Observer<in WorkInfo> {
workerStateChannel.offer(WorkerState.Enqueued)
finalWorkerObserver = Observer { lastWorkInfo ->
when {
lastWorkInfo == null -> {
}
lastWorkInfo.state == WorkInfo.State.ENQUEUED -> workerStateChannel.offer(WorkerState.Running)
lastWorkInfo.state == WorkInfo.State.BLOCKED -> workerStateChannel.offer(WorkerState.Running)
lastWorkInfo.state == WorkInfo.State.RUNNING -> workerStateChannel.offer(WorkerState.Running)
lastWorkInfo.state == WorkInfo.State.CANCELLED -> workerStateChannel.offer(WorkerState.Failed)
lastWorkInfo.state == WorkInfo.State.FAILED -> workerStateChannel.offer(WorkerState.Failed)
lastWorkInfo.state == WorkInfo.State.SUCCEEDED -> workerStateChannel.offer(WorkerState.Succeeded)
}
}
return finalWorkerObserver!!
}
@ExperimentalCoroutinesApi
fun resetWorkerState() {
val currentValue = workerStateChannel.valueOrNull
if (currentValue != null && currentValue == WorkerState.Dormant) {
return
}
launch {
delay(10)
workerStateChannel.offer(WorkerState.Dormant)
}
}
fun cancelWorkers(applicationContext: Context) {
WorkManager.getInstance(applicationContext).cancelUniqueWork(NUMERIC_UNIQUE_WORK_NAME)
}
}
Журналы, которые я вижу при запуске приложения и переходе к каждому действию в свою очередь, имеют смысл, как показано здесь
2020-01-23 12:45:13.118 14182-14182/ E/Root: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$WhatIsGoingOn@61c20c9: WorkerState) {} 2b02e732-c078-4074-8591-f45e8221f179
2020-01-23 12:45:14.272 14182-14182/ E/MainActivity: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$WhatIsGoingOn@61c20c9: WorkerState) {} 7075dcb3-55cd-487e-8f8a-0636d7651882
2020-01-23 12:45:23.104 14182-14182/ E/DummyOne: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$WhatIsGoingOn@61c20c9: WorkerState) 2d517932-ba90-4d4e-a4b8-8e22949a342f
2020-01-23 12:45:24.094 14182-14182/ E/DummyTwo: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$WhatIsGoingOn@61c20c9: WorkerState) {} 7bd185ad-69f3-4dc7-bea4-53c551233cd3
Когда я затем перемещаюсь из каждого занятия с помощью кнопки НАЗАД Root
активность, журналы не создаются
Когда я затем возвращаюсь к MainActivity
-> DummyOne
-> DummyTwo
, я вижу эти журналы
2020-01-23 12:53:18.231 14820-14820/ E/MainActivity: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$WhatIsGoingOn@205b7a9: WorkerState) {} 8198b7bc-e903-47c2-8d6f-845d92bec223
2020-01-23 12:53:19.547 14820-14820/ E/DummyOne: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$WhatIsGoingOn@205b7a9: WorkerState) 99df1067-4427-4ba7-aac8-e27cee6e7492
2020-01-23 12:53:20.459 14820-14820/ E/DummyTwo: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$WhatIsGoingOn@205b7a9: WorkerState) {} 1830072b-806d-4e97-8ecc-01aaa5e42c36
Почему бы не Я вижу журнал из Root
Активность?
При запуске фоновой работы в MainActivity
и оставаясь там, я вижу эти журналы
2020-01-23 12:54:56.760 14820-14820/ E/MainActivity: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$Enqueued@e5e0efa: WorkerState) {} 3f1a768d-0d68-4da3-8e46-45a454f025ac
2020-01-23 12:54:56.773 14820-14820/ E/MainActivity: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$Running@d20bbdd: WorkerState) {} c70cf107-4468-4324-851f-26ff46bb69bc
2020-01-23 12:55:12.723 14820-14820/ E/MainActivity: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$Succeeded@42a0c14: WorkerState) {} 973fddcd-74a6-4255-b430-2418e64e214e
2020-01-23 12:55:12.737 14820-14820/ E/MainActivity: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$Dormant@10d6cbd: WorkerState) {} b100edc9-7b15-4c9f-aae4-ba840bb57d7c
Когда я перехожу к DummyOne
-> DummyTwo
Я получаю эти журналы
2020-01-23 12:56:19.816 14820-14820/ E/DummyOne: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$Dormant@10d6cbd: WorkerState) 32a20aa1-442f-406f-8dfe-c146722ca1a7
2020-01-23 12:56:20.597 14820-14820/ E/DummyTwo: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$Dormant@10d6cbd: WorkerState) {} ecd3ebe4-88fd-4d57-b046-89b4c7cf6325
Когда я возвращаюсь назад на Root
Активность с DummyTwo
, я вижу только эти журналы с Root
Активность: -
2020-01-23 12:57:14.844 14820-14820/ E/Root: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$Enqueued@e5e0efa: WorkerState) {} c055869a-55b3-47df-9d5d-671ea91b0b04
2020-01-23 12:57:14.844 14820-14820/ E/Root: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$Dormant@10d6cbd: WorkerState) {} df39cf27-9e67-4633-b9a8-dbd390c2f66c
Почему я не вижу журналы с DummyOne
, MainActivity
?
Почему Root
Активность регистрирует два значения?
Когда я перехожу из Root
-> MainActivity
и начинаю фоновую работу, затем перехожу к DummyOne
и жду завершения фоновой работы. Я вижу эти журналы
2020-01-23 13:33:51.251 16063-16063/ E/Root: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$Dormant@60966d: WorkerState) {} 8675c8c6-b92c-4241-8808-e92dace2d25d
2020-01-23 13:33:52.327 16063-16063/ E/MainActivity: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$Dormant@60966d: WorkerState) {} 1e5080b7-ca33-4abc-81ba-69b5ae18cad4
2020-01-23 13:33:54.043 16063-16063/ E/MainActivity: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$Enqueued@8f05427: WorkerState) {} 8a495dde-7d7c-43d3-93d3-75440ac21927
2020-01-23 13:33:54.056 16063-16063/ E/MainActivity: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$Running@fbaacbe: WorkerState) {} 1a170d0c-01c3-4e7b-a770-a0dc0bba94ad
2020-01-23 13:33:54.814 16063-16063/ E/DummyOne: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$Running@fbaacbe: WorkerState) 933bf7db-cf4d-499d-bdaa-0f8cdd9bb40d
2020-01-23 13:34:10.003 16063-16063/ E/DummyOne: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$Succeeded@76e0e84: WorkerState) 6176c299-bf44-4c97-b537-e5d2f606d19a
2020-01-23 13:34:10.016 16063-16063/ E/DummyOne: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$Dormant@60966d: WorkerState) 9a5300ec-d376-496e-beef-02e369b45ea2
, когда я возвращаюсь назад от DummyOne
-> MainActivity
-> Root
Я вижу эти журналы
2020-01-23 13:35:30.689 16063-16063/ E/MainActivity: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$Dormant@60966d: WorkerState) {} bacb097b-52d1-41bd-aeca-2856dd0fcbb6
2020-01-23 13:35:31.578 16063-16063/ E/Root: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$Enqueued@8f05427: WorkerState) {} 313611d7-cc5a-4740-aaca-465ff92c81a4
2020-01-23 13:35:31.578 16063-16063/ E/Root: override fun consumeWorkerState(org.vulgaris.endeavor.state.WorkerState$Dormant@60966d: WorkerState) {} 12605ce9-47f5-4584-b6e1-a6680e5ab9c1
Почему Root Активность записать два значения?
Root
Активность похожа на это: -
class Root : Base() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_root)
findViewById<Button>(R.id.start_work_button).setOnClickListener {
startActivity(Intent(this@Root, MainActivity::class.java))
}
}
override fun consumeWorkerState(workerState: WorkerState) {
Log.e("Root", "override fun consumeWorkerState($workerState: WorkerState) {} ${UUID.randomUUID()}")
}
}
MainActivity
похожа на это: -
class MainActivity : Base() {
@ExperimentalCoroutinesApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mainLayout = findViewById(R.id.main_layout)
anyBusy = findViewById(R.id.any_busy)
numericBusy = findViewById(R.id.numeric_busy)
alphabeticBusy = findViewById(R.id.alphabetic_busy)
findViewById<Button>(R.id.start_work_tags_button).setOnClickListener {
anyBusy.visibility = View.VISIBLE
numericBusy.visibility = View.VISIBLE
launch {
endeavorManager.doNumericWork(applicationContext)
}
}
findViewById<Button>(R.id.start_work_button).setOnClickListener {
startActivity(Intent(this, DummyOne::class.java))
}
findViewById<Button>(R.id.cancel_unique_work_button).setOnClickListener {
endeavorManager.cancelWorkers(applicationContext)
}
}
@ExperimentalCoroutinesApi
override fun consumeWorkerState(workerState: WorkerState) {
Log.e("MainActivity", "override fun consumeWorkerState($workerState: WorkerState) {} ${UUID.randomUUID()}")
when(workerState) {
WorkerState.Dormant -> hideProgress()
WorkerState.Blocked -> showProgress()
WorkerState.Cancelled -> showError(mainLayout, R.string.worker_error)
WorkerState.Enqueued -> showProgress()
WorkerState.Failed -> showError(mainLayout, R.string.worker_error)
WorkerState.Running -> showProgress()
WorkerState.Succeeded -> hideProgress()
}
}
private fun showProgress() {
anyBusy.visibility = View.VISIBLE
numericBusy.visibility = View.VISIBLE
}
@ExperimentalCoroutinesApi
override fun hideProgress() {
super.hideProgress()
anyBusy.visibility = View.INVISIBLE
numericBusy.visibility = View.INVISIBLE
alphabeticBusy.visibility = View.INVISIBLE
}
}
DummyOne
Активность похожа на это: -
class DummyOne : Base() {
private lateinit var dummyOneLayout: View
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_dummy_one)
dummyOneLayout = findViewById(R.id.dummy_one_layout)
anyBusy = findViewById(R.id.any_busy)
numericBusy = findViewById(R.id.numeric_busy)
alphabeticBusy = findViewById(R.id.alphabetic_busy)
findViewById<Button>(R.id.start_work_button).setOnClickListener {
startActivity(Intent(this@DummyOne, DummyTwo::class.java))
}
}
@ExperimentalCoroutinesApi
override fun consumeWorkerState(workerState: WorkerState) {
Log.e("DummyOne", "override fun consumeWorkerState($workerState: WorkerState) ${UUID.randomUUID()}")
when (workerState) {
WorkerState.Dormant -> hideProgress()
WorkerState.Blocked -> showProgress()
WorkerState.Cancelled -> showError(mainLayout, R.string.worker_error)
WorkerState.Enqueued -> showProgress()
WorkerState.Failed -> showError(mainLayout, R.string.worker_error)
WorkerState.Running -> showProgress()
WorkerState.Succeeded -> hideProgress()
}
}
private fun showProgress() {
anyBusy.visibility = View.VISIBLE
numericBusy.visibility = View.VISIBLE
}
@ExperimentalCoroutinesApi
override fun hideProgress() {
super.hideProgress()
anyBusy.visibility = View.INVISIBLE
numericBusy.visibility = View.INVISIBLE
alphabeticBusy.visibility = View.INVISIBLE
}
}