Пусть каждый наблюдатель получает * новые * LiveData только после подписки / наблюдения

Каждый раз, когда вы вызываете .observe() в LiveData, Observer получает последнее значение этой LiveData. В некоторых случаях это может быть полезно, но не в моем.

  1. Каждый раз, когда я вызываю .observe(), я хочу, чтобы Observer получал только будущие изменения LiveData, но не то значение, которое он сохраняет при вызове .observe().

  2. У меня может быть несколько наблюдателей для экземпляра LiveData. Я хочу, чтобы все они получали обновления LiveData, когда они появляются.

  3. Я хочу, чтобы каждое обновление LiveData использовалось каждым наблюдателем только один раз. Я думаю, что это всего лишь переформулировка первого требования, но у меня уже кружится голова, и я не уверен в этом.


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

  1. Оберните данные в LiveData<SingleEvent<Data>> и проверьте этот SingleEvent класс, если он уже был использован.

  2. Расширьте MediatorLiveData и используйте карту поиска, если Наблюдатель уже получил Событие

Примеры этих подходов можно найти здесь: https://gist.github.com/JoseAlcerreca/5b661f1800e1e654f07cc54fe87441af#gistcomment-2783677

К сожалению, ни один из этих примеров не решает всех моих требований. В большинстве случаев проблема заключается в том, что любой новый Observer по-прежнему получает последнее значение LiveData после подписки. Это означает, что уже показанная панель закусок отображается снова и снова, когда пользователь перемещается между экранами.


Чтобы дать вам некоторое представление о том, о чем я говорю / о чем пишу:

Я слежу за дизайном LiveData MVVM для компонентов архитектуры Android:

  • 2 ListFragment показывают список записей.
  • Они используют 2 экземпляра одного и того же класса ViewModel для наблюдения за LiveData, связанным с пользовательским интерфейсом.
  • Пользователь может удалить запись в таком ListFragment. Удаление выполняется ViewModel, вызывающим Repository.delete()
  • ViewModel наблюдает за репозиторием для RepositoryEvents.

Поэтому, когда удаление выполнено, репозиторий сообщает об этом ViewModel, а ViewModel сообщает об этом ListFragment.

Теперь, когда пользователь переключается на второй ListFragment, происходит следующее:

  • Создается второй фрагмент и вызывает .observe() в своей ViewModel.
  • Создается ViewModel и вызывает .observe() в репозитории.

  • Репозиторий отправляет текущий RepositoryEvent в ViewModel

  • ViewModel отправляет соответствующее событие пользовательского интерфейса во фрагмент
  • Фрагмент показывает Snackbar подтверждения для удаления, которое произошло в другом месте.

Вот упрощенный код:

Фрагмент:

viewModel.dataEvents.observe(viewLifecycleOwner, Observer { showSnackbar() })
viewModel.deleteEntry()

ViewModel:

val dataEvents: LiveData<EntryListEvent> = Transformations.switchMap(repository.events, ::handleRepoEvent)
fun deleteEntry() = repository.deleteEntry()
private fun handleRepoEvent(event: RepositoryEvent): LiveData<EntryListEvent> {
    // convert the repository event to an UI event
}

Репозиторий:

private val _events = MutableLiveData<RepositoryEvent>()
val events: LiveData<RepositoryEvent>
    get() = _events

fun deleteEntry() {
    // delete it from database
    _events.postValue(RepositoryEvent.OnDeleteSuccess)
}

comment
Используйте отдельные LiveData для состояния просмотра (например, элементы в списке) и для событий (например, показывать панель подтверждения), используя для последнего шаблон одиночного живого события. IMHO, ваш первый критерий (я хочу, чтобы Observer получал только будущие изменения LiveData) не годится - при изменении конфигурации вы не получите текущие данные.   -  person CommonsWare    schedule 27.04.2019
comment
Я уже использую другую LiveData для элементов в списке. Это нормально работает. UI-LiveData, о котором я здесь говорю, используется только для подтверждающих сообщений. Они не должны отображаться второй раз (что происходит прямо сейчас при изменении конфигурации или возвратно-поступательной навигации). Паттерна с одним живым событием было бы достаточно, если бы у меня был только один наблюдатель. Но у меня их 2, и когда первый наблюдатель потребляет одиночное live-событие, второй наблюдатель не обрабатывает его. Это как раз та проблема, с которой я столкнулся.   -  person muetzenflo    schedule 27.04.2019
comment
Но у меня их два, и когда первый наблюдатель потребляет одиночное live-событие, второй наблюдатель не обрабатывает его - это особенность, а не ошибка. Если наблюдатель не может обработать событие, он не должен наблюдать LiveData или не должен отмечать событие как использованное. Итак, в вашем случае первый наблюдатель должен обрабатывать событие, поэтому второму наблюдателю событие не нужно.   -  person CommonsWare    schedule 27.04.2019
comment
с обработкой события я имею в виду, что 2-й наблюдатель больше не имеет доступа к событию, поскольку оно уже было использовано 1-м наблюдателем (как в одиночном событии). Оба наблюдателя могут и должны обрабатывать событие (см. Мои 3 требования в верхней части моего вопроса).   -  person muetzenflo    schedule 27.04.2019
comment
Оба наблюдателя могут и должны обрабатывать событие - это противоречит этому. Это означает, что Snackbar, который уже был показан, отображается снова и снова всякий раз, когда пользователь перемещается между экранами. Вам нужно решить, хотите ли вы показывать закусочную один или два раза. Если ответ - один раз, то событие обрабатывает только один наблюдатель. Если ответ положительный, я хочу показать снэкбар только один раз, но я хочу делать другие вещи в обоих наблюдателях, тогда другие вещи являются частью вашего состояния просмотра и должны быть представлены в другом LiveData.   -  person CommonsWare    schedule 27.04.2019
comment
Вот пример, когда мне нужны 2 наблюдателя, и оба они обрабатывают событие: пользователь редактирует элемент списка и нажимает кнопку сохранения в текущем фрагменте. Соответствующее событие пользовательского интерфейса - закрыть фрагмент, вызвав onBackPressed (). При закрытии этого фрагмента редактирования немедленно отображается соответствующий фрагмент списка, и его задача - отображать Snackbar. = ›Требование 2. Теперь, когда я поворачиваю устройство, событие принимается снова, и Snackbar показывает второй раз, чего не должно быть =› Требование 1   -  person muetzenflo    schedule 27.04.2019
comment
Это отдельные события, поэтому нужны отдельные LiveData.   -  person CommonsWare    schedule 27.04.2019
comment
Позвольте нам продолжить это обсуждение в чате.   -  person muetzenflo    schedule 27.04.2019


Ответы (2)


ОБНОВЛЕНИЕ 2021:

Используя библиотеку сопрограмм и Flow, теперь очень легко добиться этого, реализовав Channels:

MainActivity

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar
import com.plcoding.kotlinchannels.databinding.ActivityMainBinding
import kotlinx.coroutines.flow.collect

class MainActivity : AppCompatActivity() {

    private lateinit var viewModel: MainViewModel

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)

        binding.btnShowSnackbar.setOnClickListener {
            viewModel.triggerEvent()
        }

        lifecycleScope.launchWhenStarted {
            viewModel.eventFlow.collect { event ->
                when(event) {
                    is MainViewModel.MyEvent.ErrorEvent -> {
                        Snackbar.make(binding.root, event.message, Snackbar.LENGTH_LONG).show()
                    }
                }
            }
        }

    }
}

MainViewModel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch

class MainViewModel : ViewModel() {

    sealed class MyEvent {
        data class ErrorEvent(val message: String): MyEvent()
    }

    private val eventChannel = Channel<MyEvent>()
    val eventFlow = eventChannel.receiveAsFlow()

    fun triggerEvent() = viewModelScope.launch {
        eventChannel.send(MyEvent.ErrorEvent("This is an error"))
    }
}
person muetzenflo    schedule 12.03.2021

Для меня проблема была решена следующим образом:

Класс-оболочка событий для хранения данных, связанных с событиями (скопируйте из примеров Google)

public class Event<T> {

    private T mContent;

    private boolean hasBeenHandled = false;


    public Event( T content) {
        if (content == null) {
            throw new IllegalArgumentException("null values in Event are not allowed.");
        }
        mContent = content;
    }

    @Nullable
    public T getContentIfNotHandled() {
        if (hasBeenHandled) {
            return null;
        } else {
            hasBeenHandled = true;
            return mContent;
        }
    }

    public boolean hasBeenHandled() {
        return hasBeenHandled;
    }
}

Затем я создаю класс наблюдателя событий, который обрабатывает проверки данных (null и т. Д.):

public class EventObserver<T> implements Observer<Event<T>> {

  @Override
  public void onChanged(Event<T> tEvent) {
    if (tEvent != null && !tEvent.hasBeenHandled())
      onEvent(tEvent.getContentIfNotHandled());
  }

  protected void onEvent(@NonNull T content) {}
}

И класс обработчика событий, чтобы упростить доступ из модели просмотра:

public class EventHandler<T> {

  private MutableLiveData<Event<T>> liveEvent = new MutableLiveData<>();

  public void observe(@NonNull LifecycleOwner owner, @NonNull EventObserver<T> observer){
      liveEvent.observe(owner, observer);
  }

    public void create(T content) {
    liveEvent.setValue(new Event<>(content));
  }
}

Пример:

В ViewModel.class:

private EventHandler<Boolean> swipeEventHandler = new EventHandler<>();

  public EventHandler<Boolean> getSwipeEventHandler() {
    return swipeEventHandler;
  }

В действии / фрагменте:

Начните наблюдать:

 viewModel
    .getSwipeEventHandler()
    .observe(
        getViewLifecycleOwner(),
        new EventObserver<Boolean>() {
          @Override
          protected void onEvent(@NonNull Boolean content) {
            if(content)confirmDelete(modifier);
          }
        });

Создать событие:

viewModel.getSwipeEventHandler().create(true);
person Jurij Pitulja    schedule 09.05.2019
comment
не работает при создании модели представления с фабрикой во фрагментах. - person rana_sadam; 16.02.2021