company_banner

На поводу у трендов, или движение навстречу RxJava и LiveData



    На дворе 2018 год. Всё чаще встречаются слова RxJava и LiveData. Но если вдруг так случилось, что в вашем приложении до сих пор балом правят старомодные решения вроде библиотеки android-priority-jobqueue или AsyncTask (да, бывает и так), то эта статья специально для вас. Я разделяю эти подходы, исходя из заложенной в них философии. Первый предполагает некоторую зависимость выполнения работы от отображения, второй — выполнение задачи, при котором View слушает её и она не прерывается в зависимости от событий жизненного цикла (например, при повороте экрана). Под катом я предлагаю рассмотреть миграцию на связку RxJava и LiveData для обоих подходов.

    Для иллюстрации я буду использовать небольшой класс Work, представляющий собой некоторую длительную операцию, которую необходимо вынести в фоновый поток. В тестовом приложении я поместил крутящийся ProgressBar на экран, чтобы видеть, когда работа будет выполняться в главном потоке.

    class Work {
        fun doWork() = try {
            for (i in 0 until 10) {
                Thread.sleep(500)
            }
            "work is done"
        } catch (e: InterruptedException) {
            "work is cancelled"
        }
    }
    

    AsyncTask


    При таком подходе для каждой задачи создаётся свой AsyncTask, который отменяется в onPause() или onStop(). Это делается для того, чтобы не произошла утечка контекста активити. Чтобы показать, что под этим подразумевается, я набросал небольшой пример.

    Для начала немного модифицируем стандартный AsyncTask таким образом, чтобы его можно было отменить и вернуть ошибку из него:

    class AsyncTaskCancellable<Params, Result>(
            private val job: Job<Params, Result>,
            private var callback: AsyncTaskCallback<Result>?)
        : AsyncTask<Params, Void, AsyncTaskCancellable.ResultContainer<Result>>(), WorkManager.Cancellable {
    
        interface Job<Params, Result> {
            fun execute(params: Array<out Params>): Result
        }
    
        override fun doInBackground(vararg params: Params): AsyncTaskCancellable.ResultContainer<Result> {
            return try {
                ResultContainer(job.execute(params))
            } catch (throwable: Throwable) {
                ResultContainer(throwable)
            }
        }
    
        override fun onPostExecute(result: AsyncTaskCancellable.ResultContainer<Result>) {
            super.onPostExecute(result)
            if (result.error != null) {
                callback?.onError(result.error!!)
            } else {
                callback?.onDone(result.result!!)
            }
        }
    
        override fun cancel() {
            cancel(true)
            callback = null
        }
    
        class ResultContainer<T> {
            var error: Throwable? = null
            var result: T? = null
    
            constructor(result: T) {
                this.result = result
            }
    
            constructor(error: Throwable) {
                this.error = error
            }
        }
    }
    

    Добавляем запуск выполнения работы в менеджер:

    class WorkManager {
        fun doWorkInAsyncTask(asyncTaskCallback: AsyncTaskCallback<String>): Cancellable {
            return AsyncTaskCancellable(object : AsyncTaskCancellable.Job<Void, String> {
                override fun execute(params: Array<out Void>) = Work().doWork()
            }, asyncTaskCallback).apply {
                execute()
            }
        }
    }
    

    Запускаем задачу, предварительно отменив текущую, если она есть:

    class MainActivity : AppCompatActivity() {
        ...
            loadWithAsyncTask.setOnClickListener {
                asyncTaskCancellable?.cancel()
                asyncTaskCancellable = workManager.doWorkInAsyncTask(object : AsyncTaskCallback<String> {
                    override fun onDone(result: String) {
                        onSuccess(result)
                    }
    
                    override fun onError(throwable: Throwable) {
                        this@MainActivity.onError(throwable)
                    }
                })
            }
        ...
    }
    

    Не забываем отменить её в onPause():

    override fun onPause() {
        asyncTaskCancellable?.cancel()
        super.onPause()
    }
    

    Тут останавливается работа AsyncTask и обнуляется колбэк, чтобы очистить ссылку на MainActivity. Такой подход применим, когда надо выполнить быструю и незначительную задачу, результат которой не страшно потерять (например, при перевороте экрана, когда активити будет пересоздан).

    На RxJava аналогичная реализация особо ничем отличаться не будет.

    Так же создаём Observable, который будет выполняться на Schedulers.computation(), и возвращаем его для дальнейшей подписки.

    class WorkManager {
        ...
        fun doWorkInRxJava(): Observable<String> {
            return Observable.fromCallable {
                Work().doWork()
            }.subscribeOn(Schedulers.computation())
        }
        ...
    }
    

    Направляем колбэки в главный поток и подписываемся на работу:

    class MainActivity : AppCompatActivity() {
        ...
            loadWithRx.setOnClickListener { _ ->
            rxJavaSubscription?.dispose()
            rxJavaSubscription = workManager.doWorkInRxJava()
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({
                    onSuccess(it)
                }, {
                    onError(it)
                })
            }
        ...
    }
    

    Не забываем почистить за собой в onPause():

    override fun onPause() {
        rxJavaSubscription?.dispose()
        super.onPause()
    }
    

    В целом реализацию с RxJava можно немного дополнить с помощью библиотеки RxBinding. Она предоставляет реактивные обвязки для компонентов Android. В частности, в этом случае можно использовать RxView.clicks() для получения Observable, позволяющего слушать нажатия на кнопку:

    class MainActivity : AppCompatActivity() {
        ...
            rxJavaSubscription = RxView.clicks(loadWithRx)
                .concatMap {
                    workManager.doWorkInRxJava()
                        .observeOn(AndroidSchedulers.mainThread())
                        .doOnNext { result ->
                            onSuccess(result)
                        }
                        .onErrorReturn { error ->
                            onError(error)
                            ""
                        }
                    }
                .subscribe()
        ...
    }
    

    Обработка ошибки происходит в операторе onErrorReturn, чтобы не завершать поток событий кликов по кнопке. Таким образом, если при выполнении работы произойдёт ошибка, то до финального subscribe она не дойдёт, и клики продолжат обрабатываться.
    При реализации данного подхода необходимо помнить, что к хранению Disposable, который возвращает subscribe(), в статике нужно подходить с осторожностью. Пока не вызван метод dispose(), он может хранить неявные ссылки на ваших подписчиков, что может привести к утечкам памяти.
    Также нужно быть аккуратным с обработкой ошибок, чтобы не финишировать случайно исходный поток.

    android-priority-jobqueue


    Тут мы имеем некоего менеджера, который управляет операциями, а отображение подписывается на его текущий статус. На роль прослойки между таким менеджером и UI отлично подходит LiveData, который привязывается к жизненному циклу. Для непосредственного выполнения работы в этом примере я предлагаю использовать RxJava, позволяющий легко перенести выполнение кода в фоновый поток.

    Нам также понадобится вспомогательный класс-обёртка Resource, который будет содержать информацию о статусе операции, ошибку и результат.

    class Resource<T> private constructor(val status: Status,
                                                             val data: T?,
                                                             val error: Throwable?) {
        constructor(data: T) : this(Status.SUCCESS, data, null)
        constructor(error: Throwable) : this(Status.ERROR, null, error)
        constructor() : this(Status.LOADING, null, null)
    
        enum class Status {
            SUCCESS, ERROR, LOADING
        }
    }
    

    Теперь мы готовы написать класс WorkViewModel, который будет содержать инстанс LiveData и оповещать его об изменениях в статусе работы, используя Resource. В примере я немного схитрил и просто сделал WorkViewModel синглтоном. Я использую RxJava в статике, но подписывать на него буду через LiveData, поэтому утечек не будет.

    class WorkViewModel private constructor() {
    
        companion object {
            val instance = WorkViewModel()
        }
    
        private val liveData: MutableLiveData<Resource<String>> = MutableLiveData()
    
        private var workSubscription: Disposable? = null
    
        fun startWork(work: Work) {
            liveData.value = Resource()
    
            workSubscription?.dispose()
            workSubscription = Observable.fromCallable {
                work.doWork()
            }
                .subscribeOn(Schedulers.computation())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({ liveData.value = Resource(it) },
                                { liveData.value = Resource(it) })
        }
    
        fun getWork(): LiveData<Resource<String>> = liveData
    }
    

    Дополняем WorkManager запуском работы для поддержания однородности, т.е. чтобы работа всегда стартовала через этот менеджер:

    class WorkManager {
        ...
        fun doOnLiveData() {
        WorkViewModel.instance.startWork(Work())
        }
        ...
    }
    

    И добавляем взаимодействие со всем этим в MainActivity. Из WorkViewModel мы получаем статус текущей работы, пока живо отображение, а новую работу запускаем по нажатию на кнопку:

    class MainActivity : AppCompatActivity() {
        ...
        WorkViewModel.instance.getWork().observe(this, Observer {
            when {
                it?.status == Resource.Status.SUCCESS ->  onSuccess(it.data!!)
                it?.status == Resource.Status.ERROR -> onError(it.error!!)
                it?.status == Resource.Status.LOADING -> loadWithLiveData.isEnabled = false
            }
        })
        loadWithLiveData.setOnClickListener {
            workManager.doOnLiveData()
        }
        ...
    }
    

    Примерно таким же образом это можно реализовать, используя Subject из RxJava. Однако, на мой взгляд, LiveData лучше справляется с обработкой жизненного цикла, потому что изначально был под это заточен, тогда как с Subject можно напороться на массу проблем с остановкой потока и обработкой ошибок. Я думаю, что симбиоз RxJava и LiveData наиболее жизнеспособен: первый получает и обрабатывает потоки данных и оповещает об изменениях второй, на который уже можно подписываться с оглядкой на жизненный цикл.

    Таким образом, мы рассмотрели переход от архаичных библиотек к более современным для двух наиболее часто встречающихся методов выполнения работы в фоновом потоке. Для разовой незначительной операции отлично подойдёт голый RxJava, потому что он позволяет очень гибко работать с данными и управлять потоками, на которых это должно происходить. В то же время, если требуется более тонкое взаимодействие с жизненным циклом, то лучше воспользоваться LiveData, который изначально проектировался для решения такой задачи.

    Полную версию исходников можно посмотреть тут: GitHub

    Буду рад ответить на ваши вопросы в комментариях!
    • +16
    • 5,2k
    • 4

    FunCorp

    187,00

    Разработка развлекательных сервисов

    Поделиться публикацией

    Похожие публикации

    Комментарии 4
      0
      Эм, а я думал что на дворе 2018 г. и все уже переходят на корутины…
        0
        Думаю, что не всем на 100% нравятся корутины и не все спешат на них перебираться. Особенно учитывая, что стабильный релиз пока только в анонсе. Поэтому, мне кажется, что использование альтернативных решений вполне оправдано
          0
          Корутины вполне стабильны, просто пока находятся в экспериментальном статусе. Как говорят создатели, это означает, что код, написанный сейчас, будет работать и потом. Но если попытаться собрать ранее написанный код под более новыми версиями библиотеки корутин, его работоспособность не гарантируется. Если верить Роману Елизарову, это коснётся только тех, кто хочет писать собственные библиотеки и тому подобное. Если же в проекте сейчас используются корутины, то при неизменности версии их библиотеки стабильность полная.
          Да и вроде как в Kotlin 1.3 обещают убрать экспериментальный статус.
        0
        С LiveData натыкались на какие-то непонятные баги — после onPause — onResume переставали получать новые значения от Room, пришлось полностью переписать на Rx. Не знаю как сейчас — может и починили, но на тот момент работало криво, хотя задумка и хорошая.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое