LiveData – это отличный инструмент для связывания состояния ваших данных и объектов с жизненным циклом (LifecycleOwner, обычно это Fragment или Activity).

Обычно LiveData помещаются во ViewModel и используются для обновления состояния вашего UI. Часто ViewModel может пережить LifecycleOwner и сохранить состояние LiveData. Такой механизм подходит, когда вам нужно сохранить данные и восстановить их через некоторое время, например, после смены конфигурации.

Но что, если мы хотим использовать механизм событий, а не состояний? Причем обязательно в контексте жизненного цикла обозревателя (LifecycleOwner). Например, нам нужно вывести сообщение после асинхронной операции при условии, что LifecycleOwner еще жив, имеет активных обозревателей и готов обновить свой UI. Если мы будем использовать LiveData, то мы будем получать одно и то же сообщение после каждой смены конфигурации, или при каждом новом подписчике. Одно из решений, которое напрашивается, это после обработки данных в некотором обозревателе обнулить эти данные в LiveData.

Например, такой код:

Observer {
	handle(it)
	yourViewModel.liveData.value = null
}

Но такой подход имеет ряд недостатков и не отвечает всем необходимым требованиям.

Мне бы хотелось иметь механизм событий, который:

  1. оповещает только активных подписчиков,
  2. в момент подписки не оповещает о предыдущих данных,
  3. имеет возможность выставить флаг 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)


  1. agent10
    24.09.2019 20:14

    Почему не RX? LiveData это же не до RX.
    Под требования «оповещает только активных подписчиков, в момент подписки не оповещает о предыдущих данных» в RX уже всё есть. PublishProcessor


    1. proninyaroslav
      25.09.2019 00:03

      Для тех проектов, где применяют mvvm и тащить rx ради фетчинга данных из базы или сети нет смысла, и при этом всё несколько проще чем в rx.


      1. agent10
        25.09.2019 00:07

        rx никак не связан с MVVM. Как раз сейчас у меня в одном из проектов MVVM вместе с Rx. Rx всё таки обладает более полноценной поддержкой ReactiveStreams. Всякие zip, combineLatest и многие прочие другие операторы…


      1. ar2code Автор
        25.09.2019 09:58

        Я пишу большую статью об архитектуре и дизайне Android – приложения, где собраны многие инструменты: ViewModel, LiveData, RX и т.д. Там я отвечу на этот вопрос подробно. Но, если коротко, то RX используется, например, внутри UseCases или провайдеров для асинхронной работы, маппинга и прочих прелестей, которые дает RX. Но когда данные выводятся пользователю, то данные прослаиваются через LiveData. И именно эта прослойка позволила избежать множества ошибок, связанных с жизненным циклом. Поэтому статья про события на основе LiveData, если нужно опять же работать в контексте жизненного цикла. А других независимых способов передачи событий полно: RX, какой-нибудь EventBus, что угодно.


        1. agent10
          25.09.2019 10:11

          Не понял, либо использовать везде rx либо LiveData. Если использовать и то и то, то опять будет эта граница с проблемами ЖЦ.


          1. ar2code Автор
            25.09.2019 10:21

            Нет, смотрите: если грубо, сделайте во ViewModel вызов RX, внутри subscribe, когда RX успешно вернула результат своего запроса, данные выведите в LiveData. А LiveData уже сам оповестит своих подписчиков, если такие есть, т.е. где-то в фрагменте есть подписка на эту LiveData. И с ЖЦ проблем нет. Например, такой кейс: RX возвращает данные в момент переворота девайса, View уже уничтожена, если бы вы напрямую из RX обновляли View, то будет ошибка. А так, с LiveData, все пройдет как надо, переворот девайса завершится, View будет создана заново, и View обновит свое состояние из LiveData. LiveData и RX работают вместе.


            1. agent10
              25.09.2019 10:25

              1) У вас также будет проблема кто будет вызывать dispose() в RX
              2)

              View уже уничтожена, если бы вы напрямую из RX обновляли View

              Тут не так, в этом случае есть другая прослойка в виде того же DataBinding. Поэтому RX меняет View не напрямую, а через DataBinding. В этом случае LiveData уже лишняя


              1. ar2code Автор
                25.09.2019 10:41

                1. Не вижу проблемы, вызываем Dispose, операция просто отменяется
                2. Тогда вы заменяете LiveData на DataBinding, а не LiveData на RX. В общем, такой вариант тоже рабочий, никто с этим не спорит.


            1. proninyaroslav
              25.09.2019 10:40

              Вопрос в том, целесообразны ли эти лишние конвертации, или проще везде использовать rx. Всё зависит от задачи.


              1. ar2code Автор
                25.09.2019 10:43

                Да, абсолютно с вами согласен. Все всегда зависит от задачи, вариантов решения может быть множество, в статье я описал лишь один из них.


              1. anegin
                25.09.2019 16:20

                Целесообразно, когда приложение модульное, и не хочется тянуть в domain/data модули андроидовские зависимости (LiveData)


                1. proninyaroslav
                  25.09.2019 17:16

                  Ну так можно и не использовать livedata, его пока не навязывают как обязательную зависимость. Я имел ввиду что не всегда имеет смысл работать с livedata, если в проекте уже rx.


    1. advance
      25.09.2019 12:14

      Почитал ветку. Попробую дать наиболее корректный ответ.
      Rx (если говорить об RxJava) не только про реактивщину, но еще и про функциональщину.
      Функциональный подход имеет свои преимущества и недостатки в равной степени с другими. Если Вам функциональный подход больше нравится- то да, возможно, LiveData Вам использовать смысла нет.
      Зато LiveData хорошо стыкуется с корутинами (которые, на сегодняшний день, являются рекомендуемым подходом к построению многопоточности). Такая связка хорошо укладывается в императивный или декларативный стиль.
      Сам имел опыт работы с приложениями, построенными на Rx (включая mvp на rx). И связка Android Architecture Components + корутины, на мой личный взгляд, имеет больше преимуществ.


  1. ar2code Автор
    24.09.2019 20:23

    Потому что нужна привязка к LifecycleOwner. Нужно получать сообщения, когда LifecycleOwner активен. RX может прислать событие уже после того, как LifecycleOwner будет уничтожен. У LifecycleOwner много нюансов. Когда появились LiveData, жить с фрагментами и активити стало заметно легче.


    1. proninyaroslav
      24.09.2019 23:56

      Разве в rx после dispose(), к примеру, в onStop(), будет продолжать отправлять события?


      1. agent10
        25.09.2019 00:04

        Но надо в ручную дёргать dispose(). А LiveData сама обработает состояния из LifecycleOwner


      1. ar2code Автор
        25.09.2019 09:49

        В общем, можно вызвать самому dispose. Но LiveData и сделана для того, чтобы нам забыть про ручную обработку жизненного цикла. Кстати, не всегда dispose решает некоторые тонкие моменты. Например, если использовать LiveData, то после OnStop, данные придут в LiveData, и при следующем OnResume наш observer сработает и мы увидим сообщение. RX и LiveData решают разные задачи, на мой взгляд.


        1. anegin
          25.09.2019 16:23

          Например, если использовать LiveData, то после OnStop, данные придут в LiveData, и при следующем OnResume наш observer сработает и мы увидим сообщение.

          Для этого в Rx есть BehaviorSubject/BehaviorProcessor — хранит текущее значение и возвращает его сразу при подписке на него (и само собой все последующие изменения тоже эмитит)