В данной статье хочу поделиться подходом, который использую для выполнения действий в определенные моменты жизненного цикла компонентов. Сам по себе подход не новый, но в такой реализации не встречал его на просторах интернета.
Для тех, кто ценит код больше слов, вот ссылка на репозиторий с простеньким приложением, на примере которого можно увидеть применения подхода на практике.
Гугл, несколько лет назад представив 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. На мой взгяд, это может создать больше проблем, чем принесет пользы).
Надеюсь статья оказалась полезной. Все исходники лежат в репозитории.
Спасибо, что дочитали до конца. Всем добра!