Обычно LiveData помещаются во ViewModel и используются для обновления состояния вашего UI. Часто ViewModel может пережить LifecycleOwner и сохранить состояние LiveData. Такой механизм подходит, когда вам нужно сохранить данные и восстановить их через некоторое время, например, после смены конфигурации.
Но что, если мы хотим использовать механизм событий, а не состояний? Причем обязательно в контексте жизненного цикла обозревателя (LifecycleOwner). Например, нам нужно вывести сообщение после асинхронной операции при условии, что LifecycleOwner еще жив, имеет активных обозревателей и готов обновить свой UI. Если мы будем использовать LiveData, то мы будем получать одно и то же сообщение после каждой смены конфигурации, или при каждом новом подписчике. Одно из решений, которое напрашивается, это после обработки данных в некотором обозревателе обнулить эти данные в LiveData.
Например, такой код:
Observer {
handle(it)
yourViewModel.liveData.value = null
}
Но такой подход имеет ряд недостатков и не отвечает всем необходимым требованиям.
Мне бы хотелось иметь механизм событий, который:
- оповещает только активных подписчиков,
- в момент подписки не оповещает о предыдущих данных,
- имеет возможность выставить флаг handled в true, чтобы прервать дальнейшую обработку события.
Я реализовал класс MutableLiveEvent, который обладает всеми вышеперечисленными свойствами и который может работать, как обычный LiveData.
Как использовать:
//Создайте свой экземпляр EventArgs для передачи своих типов данных в событии
class MyIntEventArgs(data: Int) : EventArgs<Int>(data)
//Создайте обычную viewModel
class MainViewModel : ViewModel() {
private val myEventMutable = MutableLiveEvent<MyIntEventArgs>()
val myEvent = myEventMutable as LiveData<MyIntEventArgs>
fun sendEvent(data: Int) {
myEventMutable.value = MyIntEventArgs(data)
}
}
val vm = ViewModelProviders.of(this).get(MainViewModel::class.java)
vm.myEvent.observe(this, Observer {
//Здесь обработка события
/*
* Если событие обработано, и вы не хотите,
* чтобы оно дошло до других обозревателей, то укажите handled = true
*/
it.handled = true
})
Весь код доступен на GitHub, а ниже я немного расскажу о реализации.
class MutableLiveEvent<T : EventArgs<Any>> : MutableLiveData<T>() {
internal val observers = ArraySet<PendingObserver<in T>>()
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
val wrapper = PendingObserver(observer)
observers.add(wrapper)
super.observe(owner, wrapper)
}
override fun observeForever(observer: Observer<in T>) {
val wrapper = PendingObserver(observer)
observers.add(wrapper)
super.observeForever(observer)
}
@MainThread
override fun removeObserver(observer: Observer<in T>) {
when (observer) {
is PendingObserver -> {
observers.remove(observer)
super.removeObserver(observer)
}
else -> {
val pendingObserver = observers.firstOrNull { it.wrappedObserver == observer }
if (pendingObserver != null) {
observers.remove(pendingObserver)
super.removeObserver(pendingObserver)
}
}
}
}
@MainThread
override fun setValue(event: T?) {
observers.forEach { it.awaitValue() }
super.setValue(event)
}
}
Идея заключается в том, чтобы внутри класса MutableLiveEvent, в методах observe и observeForever, оборачивать обозреватели в специальный внутренний класс PendingObserver, который вызывает реальный обозреватель только один раз и только если выставлен флаг pending в true, и событие еще не обработано.
internal class PendingObserver<T : EventArgs<Any>>(val wrappedObserver: Observer<in T>) : Observer<T> {
private var pending = false
override fun onChanged(event: T?) {
if (pending && event?.handled != true) {
pending = false
wrappedObserver.onChanged(event)
}
}
fun awaitValue() {
pending = true
}
}
В PendingObserver флаг pending выставлен в false по умолчанию. Это решает п.2 (не оповещать о старых данных) из нашего списка.
А код в MutableLiveEvent
override fun setValue(event: T?) {
observers.forEach { it.awaitValue() }
super.setValue(event)
}
Сначала выставляет pending в true и только потом обновляет данные внутри себя. Это обеспечивает выполнение п.1. (оповещение только активных подписчиков).
Последний момент, о котором я еще не рассказал, — это EventArgs. Это класс — обобщение, в котором есть флаг handled для прерывания дальнейшей обработки события (п.3.).
open class EventArgs<out T>(private val content: T?) {
var handled: Boolean = false
val data: T?
get() {
return if (handled) {
null
} else {
content
}
}
}
На этом все, спасибо за внимание!
Комментарии (18)
ar2code Автор
24.09.2019 20:23Потому что нужна привязка к LifecycleOwner. Нужно получать сообщения, когда LifecycleOwner активен. RX может прислать событие уже после того, как LifecycleOwner будет уничтожен. У LifecycleOwner много нюансов. Когда появились LiveData, жить с фрагментами и активити стало заметно легче.
proninyaroslav
24.09.2019 23:56Разве в rx после dispose(), к примеру, в onStop(), будет продолжать отправлять события?
agent10
25.09.2019 00:04Но надо в ручную дёргать dispose(). А LiveData сама обработает состояния из LifecycleOwner
ar2code Автор
25.09.2019 09:49В общем, можно вызвать самому dispose. Но LiveData и сделана для того, чтобы нам забыть про ручную обработку жизненного цикла. Кстати, не всегда dispose решает некоторые тонкие моменты. Например, если использовать LiveData, то после OnStop, данные придут в LiveData, и при следующем OnResume наш observer сработает и мы увидим сообщение. RX и LiveData решают разные задачи, на мой взгляд.
anegin
25.09.2019 16:23Например, если использовать LiveData, то после OnStop, данные придут в LiveData, и при следующем OnResume наш observer сработает и мы увидим сообщение.
Для этого в Rx есть BehaviorSubject/BehaviorProcessor — хранит текущее значение и возвращает его сразу при подписке на него (и само собой все последующие изменения тоже эмитит)
agent10
Почему не RX? LiveData это же не до RX.
Под требования «оповещает только активных подписчиков, в момент подписки не оповещает о предыдущих данных» в RX уже всё есть. PublishProcessor
proninyaroslav
Для тех проектов, где применяют mvvm и тащить rx ради фетчинга данных из базы или сети нет смысла, и при этом всё несколько проще чем в rx.
agent10
rx никак не связан с MVVM. Как раз сейчас у меня в одном из проектов MVVM вместе с Rx. Rx всё таки обладает более полноценной поддержкой ReactiveStreams. Всякие zip, combineLatest и многие прочие другие операторы…
ar2code Автор
Я пишу большую статью об архитектуре и дизайне Android – приложения, где собраны многие инструменты: ViewModel, LiveData, RX и т.д. Там я отвечу на этот вопрос подробно. Но, если коротко, то RX используется, например, внутри UseCases или провайдеров для асинхронной работы, маппинга и прочих прелестей, которые дает RX. Но когда данные выводятся пользователю, то данные прослаиваются через LiveData. И именно эта прослойка позволила избежать множества ошибок, связанных с жизненным циклом. Поэтому статья про события на основе LiveData, если нужно опять же работать в контексте жизненного цикла. А других независимых способов передачи событий полно: RX, какой-нибудь EventBus, что угодно.
agent10
Не понял, либо использовать везде rx либо LiveData. Если использовать и то и то, то опять будет эта граница с проблемами ЖЦ.
ar2code Автор
Нет, смотрите: если грубо, сделайте во ViewModel вызов RX, внутри subscribe, когда RX успешно вернула результат своего запроса, данные выведите в LiveData. А LiveData уже сам оповестит своих подписчиков, если такие есть, т.е. где-то в фрагменте есть подписка на эту LiveData. И с ЖЦ проблем нет. Например, такой кейс: RX возвращает данные в момент переворота девайса, View уже уничтожена, если бы вы напрямую из RX обновляли View, то будет ошибка. А так, с LiveData, все пройдет как надо, переворот девайса завершится, View будет создана заново, и View обновит свое состояние из LiveData. LiveData и RX работают вместе.
agent10
1) У вас также будет проблема кто будет вызывать dispose() в RX
2)
Тут не так, в этом случае есть другая прослойка в виде того же DataBinding. Поэтому RX меняет View не напрямую, а через DataBinding. В этом случае LiveData уже лишняя
ar2code Автор
1. Не вижу проблемы, вызываем Dispose, операция просто отменяется
2. Тогда вы заменяете LiveData на DataBinding, а не LiveData на RX. В общем, такой вариант тоже рабочий, никто с этим не спорит.
proninyaroslav
Вопрос в том, целесообразны ли эти лишние конвертации, или проще везде использовать rx. Всё зависит от задачи.
ar2code Автор
Да, абсолютно с вами согласен. Все всегда зависит от задачи, вариантов решения может быть множество, в статье я описал лишь один из них.
anegin
Целесообразно, когда приложение модульное, и не хочется тянуть в domain/data модули андроидовские зависимости (LiveData)
proninyaroslav
Ну так можно и не использовать livedata, его пока не навязывают как обязательную зависимость. Я имел ввиду что не всегда имеет смысл работать с livedata, если в проекте уже rx.
advance
Почитал ветку. Попробую дать наиболее корректный ответ.
Rx (если говорить об RxJava) не только про реактивщину, но еще и про функциональщину.
Функциональный подход имеет свои преимущества и недостатки в равной степени с другими. Если Вам функциональный подход больше нравится- то да, возможно, LiveData Вам использовать смысла нет.
Зато LiveData хорошо стыкуется с корутинами (которые, на сегодняшний день, являются рекомендуемым подходом к построению многопоточности). Такая связка хорошо укладывается в императивный или декларативный стиль.
Сам имел опыт работы с приложениями, построенными на Rx (включая mvp на rx). И связка Android Architecture Components + корутины, на мой личный взгляд, имеет больше преимуществ.