У меня есть приложение, использующее класс Android ViewModel и компонент навигации для навигации между фрагментами. Как мне управлять навигацией из ViewModel? Я использую RxJava, и я думал о том, чтобы фрагменты прослушивали события навигации, а затем запускали навигацию таким образом. Как нормально с этим справиться? Я также использую Dagger для внедрения зависимостей, если это помогает.
Согласно LiveData с SnackBar, навигацией и другими сообщениями в блоге о событиях:
Some data should be consumed only once, like a Snackbar message, a navigation event or a dialog trigger.
Instead of trying to solve this with libraries or extensions to the Architecture Components, it should be faced as a design problem. We recommend you treat your events as part of your state.
Они подробно описывают использование класса SingleLiveEvent, который гарантирует, что каждое событие Navigation будет получено только один раз наблюдателем (то есть вашим фрагментом, имеющим доступ к вашему NavController).
Другой альтернативой является использование модели «Обертка событий», в которой событие должно быть явно помечено как обработанное.
Если у меня есть много возможных пунктов назначения с текущего экрана. Должен ли я иметь 1 SingleLiveEvent или LiveData с Event Wrapper для каждого возможного места назначения? Что для этого лучше всего?
Разве SingleLiveData не антипаттерн?
Если бы вы использовали MVP, P просто вызвал бы метод на V, который запускает навигацию.
Эквивалентным подходом в MVVM было бы наличие выделенного наблюдаемого / слушателя / обратного вызова, если бы это было сделано с RxJava, им мог бы управлять PublishSubject. Это удовлетворяет единовременному требованию. Если вместо этого вам нужно реагировать на события, которые могут быть сгенерированы до того, как вы подписались, вы можете использовать BehaviorSubject<Optional<T>> и создать его с помощью createDefault(absent<T>()), запустить его с помощью onNext(Optional.of(navigationObject)), а затем сообщить виртуальной машине, когда происходит навигация, а затем виртуальная машина может очистить его с помощью onNext(absent()).
В качестве альтернативы, если вы хотите превратить его в какое-то всеобъемлющее состояние, подобное redux / mvi, у вас может быть некоторый класс состояния со всем состоянием, включая некоторое свойство, которое указывает представлению, чтобы куда-то перемещаться, при получении / действии на это представление будет скажите виртуальной машине, что она это сделала, и виртуальная машина установит такое же состояние, как текущее, но без навигации. например (в Котлине) state = state.copy(navigateToX = false)
для ваших событий вы можете использовать что-то вроде этого:
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event<out T>(private val content: T) {
@Suppress("MemberVisibilityCanBePrivate")
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
с этим вы можете легко убедиться, что использовали контент однажды следующим образом:
viewModel.getShowAlertFragment().observe(viewLifecycleOwner, Event{
event?.getContentIfNotHandled()?.let {
// your code here
}
})
но чтобы сделать это еще проще и фактически избежать проверки того, использовался ли контент раньше, вы можете создать другой класс, который расширяет наблюдателя и выполняет проверку там:
/**
* An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s content has
* already been handled.
*
* [onEventUnhandledContent] is *only* called if the [Event]'s contents has not been handled.
*/
class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
override fun onChanged(event: Event<T>?) {
event?.getContentIfNotHandled()?.let {
onEventUnhandledContent(it)
}
}
}
теперь с этим вы можете упростить свой код примерно так:
viewModel.getShowAlertFragment().observe(viewLifecycleOwner, EventObserver {
// do you stuff here and you know it will only be called once
}
})
Идея взята из этого файла в образцах Android Здесь
Хотя я согласен, что это официальная рекомендация, я думаю, что Google допустил здесь большую ошибку. Навигация не является частью "состояния" конкретной модели просмотра и не является событием. Это отдельная сущность (мне нравится думать о ней как о службе), которая может содержать собственное состояние и события. Доказательством этого является тот факт, что им нужно изобрести SingleLiveEvent только для этого.