Как заменить BehaviorSubject на ConflatedBroadcastChannel в приложении Android - PullRequest
3 голосов
/ 23 января 2020

В моем текущем 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

    }
}
...