Android и привязка к жизненному циклу компонентов

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

В данной статье хочу поделиться подходом, который использую для выполнения действий в определенные моменты жизненного цикла компонентов. Сам по себе подход не новый, но в такой реализации не встречал его на просторах интернета.

Для тех, кто ценит код больше слов, вот ссылка на репозиторий с простеньким приложением, на примере которого можно увидеть применения подхода на практике.

Гугл, несколько лет назад представив Lifecycle, сразу же начал пропагандировать построение компонентов, работа которых опирается на жизненный цикл (lifecycle-aware components). Нужно сразу отметить, что в android существует несколько компонентов, имеющих жизненный цикл, которых называют владельцами (LifecycleOwner): Acitvity, Fragment, Fragment view и другие. Диаграмма жизненного цикла представлена на следующей схеме.

И фреймворк позволяет нам подписываться на жизненный цикл компонентов для получения событий о переходе из одного состояния в другое. Для этого реализуется интерфейс LifecycleObserver и передается в метод Lifecycle#addObserver.

Давайте посмотрим, как можем использовать данный механизм для выполнения типичных задач и без необходимости генерировать кучу наблюдателей.
Создадим класс Configurator, со следующим API:

class Configurator {
		fun addOperation(triggerOnEvent: Lifecycle.Event, operation: (LifecycleOwner) -> Unit)
		fun manageBy(lifecycleOwner: LifecycleOwner)
}

Реализация такого класса представляет собой набор массивов, соответствующий количеству событий жизненного цикла, в которые будем добавлять нужные нам действия. Метод addOperation() будем использовать для привязки какого-либо действия к событию жизненного цикла, а manageBy() поможет нам активировать подписку на жизненный цикл android компонента. Такой API удобен для настройки выполнения разовых действий, но часто приходится после выполнения действия отменять его потом. Например, если мы подписываемся на реактивный источник (Observable) в onStart(), то потом должны отменить подписку в onStop(). Давайте для подобного случая создадим функцию-расширение:

fun <T, R: Any?> Configurator.bind(
    target: T,
    bindAction: (LifecycleOwner, T) -> R,
    bindOnEvent: Lifecycle.Event,
    unbindAction: (LifecycleOwner, T, R) -> Unit,
    unbindOnEvent: Lifecycle.Event = bindOnEvent.oppositeEvent()
) {
    var result: R? = null
    addOperation(bindOnEvent) { result = bindAction(it, target) }
    addOperation(unbindOnEvent) { unbindAction(it, target, result!!) }
}

Первым параметром передается целевой элемент, над которым требуется выполнить действие. Событие жизненного цикла, тригерящее отмену ранее выполненного действия, по умолчанию берется противоположным, но может быть и передано явно. Важным моментом является возможность обратиться к результату выполненного начального действия при выполнении отмены (третий аргумент лямбды unbindAction). Для удобства использования с Observable создадим отдельную функцию:

fun <T> Configurator.Builder.bindObservable(
    observable: Observable<T>,
    action: (Observable<T>) -> Disposable,
    bindOnEvent: Lifecycle.Event = Lifecycle.Event.ON_CREATE,
    unbindOnEvent: Lifecycle.Event = bindOnEvent.oppositeEvent()
) {
    bind(
        target = observable to action,
        bindAction = { _, (stream, act) ->
            act(stream)
        },
        bindOnEvent = bindOnEvent,
        unbindAction = { _, _, subscription ->
            subscription.dispose()
        },
        unbindOnEvent
    )
}

В данном примере видно, как можно в качестве целевого элемента передавать больше одного объекта.

На текущий момент получившийся API класса Configurator позволяет добавлять действия в любой момент времени, но этого хотелось избежать, чтобы сосредоточить (искуственно навязать) конфигурирование действий в одном месте, а не разбрасывать их по коду. Для решения подобной задачи введем промежуточное звено - Builder, который будет возможность сконфигурировать, но после создания объект Configurator будет неизменяемым. В итоге получим:

class Configurator {
  	fun manageBy(lifecycleOwner: LifecycleOwner)
    
    class Builder {
      	fun addOperation(triggerOnEvent: Lifecycle.Event, operation: Operation)
        fun build(): Configurator
    }
}

Соответственно наши функции-расширения будут определены для Configurator.Builder. Добавим еще синтаксического сахара для удобства работы с LifecycleOwner:

fun LifecycleOwner.configure(block: Configurator.Builder.() -> Unit) {
    Configurator.Builder()
        .apply(block)
        .build()
        .manageBy(this)
}

После того как мы проделали подготовительную работу, давайте рассмотрим применение на примере фрагмента и для демонстрации всех возможностей подхода задействуем все переходы между состояниями. Итак, код в студию:

class TimeWastingFragment : Fragment() {
    private val timeSpent = MutableLiveData<Long>()
    private val color = MutableLiveData<Int>()
  
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val binding = FragmentTimeWastingBinding.inflate(inflater, container, false)

        viewLifecycleOwner.configure {
            bindState(timeSpent, binding.txtTime) { txtView, time ->
              	txtView.text = "$time"
            }

            bindState(color, binding.root) { view, color ->
              	view.setBackgroundColor(color)
            }

            bindObservable(
                observable = Observable.interval(1, TimeUnit.SECONDS),
                action = { source ->
                  source
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(
                      {
                        timeSpent.value = timeSpent.value!! + 1
                      },
                      Throwable::printStackTrace
                    )
                },
                bindOnEvent = Lifecycle.Event.ON_START
            )

            val colorChangeStream = PublishSubject.create<Unit>()

            bindClicks(
                view = binding.root,
                clickListener = {
                  color.value = generateRandomColor()
                  colorChangeStream.onNext(Unit)
                }
            )

            bindObservable(
                observable = colorChangeStream
                  .startWith(Unit)
                  .switchMap { Observable.interval(5,5, TimeUnit.SECONDS) },
                action = { source ->
                  source
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(
                      {
                        color.value = generateRandomColor()
                      },
                      Throwable::printStackTrace
                    )
                },
                bindOnEvent = Lifecycle.Event.ON_RESUME
            )
        }

        return binding.root
    }

}

В данном случае при конфигурировании мы cлушаем один реактивный поток только в состоянии Started, другой - только в Resumed. Функция-расширение bindState() добавлена для консистентности подхода, bindClicks() - в качестве примера установки/сброса слушателя.

Отмечу, что подход можно также применять и для других LifecycleOwner'ов с учетом особенностей их жизненного цикла. Важным моментом является когда нужно выполнять конфигурирование компонента (непосредственно вызов configure()). Так для элементов отображения я придерживаюсь следующего правила:

  • Для Activity в методе onCreate(), используя активити как LifecycleOwner

  • Для Fragment в методе onCreate(), используя фрагмент как LifecycleOwner

  • Для Fragment View в методе onCreateView(), используя viewLifecycleOwner

На мой взгляд, подход позволяет добиться следующих преимуществ:

  • в связке с использованием LivedData / Flow позволяет сосредоточиться на хранении в Activity/Fragment объектов состояния и избавиться от хранения ссылок на View-элементы и других промежутоных переменных (например, подписки на Rx-потоки);

  • код лучше сгруппирован, большая часть действий по конфигурированию сосредоточена в одном месте (останется еще обработка onActivityResult/onPermissionResult/onSaveInstanceState, но для этого уже есть соответствующие инструменты).

Из недостатков стоит отметить:

  • Отсутствие возможности дополнить список выполняемых действий после формирования объекта Configurator. (Но это сделано умышленно при формировании API. На мой взгяд, это может создать больше проблем, чем принесет пользы).

Надеюсь статья оказалась полезной. Все исходники лежат в репозитории.
Спасибо, что дочитали до конца. Всем добра!

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

Захотелось ли Вам применить данный подход?

  • 0,0%Да0
  • 100,0%И так все устраивает1
Источник: https://habr.com/ru/post/548566/


Интересные статьи

Интересные статьи

Набор полезных, но не очень известных инструментов и библиотек Android.Работая над статьями о 30 лучших библиотеках и проектах Android 2019 г. и 25 лучших библиотеках и проектах Andr...
Вот перевод второго урока учебного курса по Vue.js. Здесь речь пойдёт о привязке атрибутов, о подключении данных, хранящихся в экземпляре Vue, к атрибутам HTML-элементов. → Пер...
Ленивая загрузка компонентов в Angular? Может, речь идёт о ленивой загрузке модулей с помощью маршрутизатора Angular? Нет, мы говорим именно о компонентах. Текущая версия Angular поддерживает лиш...
Доброе утро! Начинаем понедельник с материала, перевод которого подготовлен специально для студентов курса «Android-разработчик. Продвинутый курс». Недавно я переносил кодовую базу Android...
Практически все коммерческие интернет-ресурсы создаются на уникальных платформах соответствующего типа. Среди них наибольшее распространение получил Битрикс24.