Итак, всем привет!
Меня зовут Алексей, я андроид-разработчик в Банке «Хоум Кредит».
В данной статье поделюсь нашим способом реализации отправления и получения событий от ViewModels к Views (Activities/Fragments).
В нашем приложении «Товары в рассрочку от Банка Хоум Кредит» мы используем фрагменты, поэтому будем говорить о них, но всё актуально и для Activity.
Что мы хотим?
У нас есть Fragment, включающий в себя несколько ViewModel-ей, данные связываются
DataBinding
-ом. Все события пользователей получают ViewModel-и. Если событие является навигационным: необходимо открыть другой fragment/activity, показать AlertDialog
, Snackbar
, системный запрос на Permissions и т.п., то такое событие должно выполниться во фрагменте.А в чем, собственно, проблема?
Жизненные циклы Views и ViewModels не связаны. Связывать колбеками (Listeners) нельзя, так как ViewModel не должны ничего знать о фрагментах, а также не должны содержать в себе ссылку на фрагменты, иначе, как известно, память начнет «ликовать».
Стандартным подходом взаимодействия Fragments и ViewModels является подписка на
LiveData
, находящуюся во ViewModel. Сделать передачу событий напрямую через LiveData
нельзя, всвязи с тем, что такой подход не учитывает, было уже выполнено событие или нет. Какие существуют решения:
1. Использовать
SingleLiveEvent
Плюсы: событие выполняется один раз.
Минусы: одно событие – один
SingleLiveEvent
. При большом количестве событий во ViewModel появляется N event-объектов, на каждый из которых придется подписываться во фрагменте.2. Неплохой пример.
Плюсы: одно событие выполняется также единожды, можно передавать данные из viewModel во fragment.
Минусы: данные в событии обязательны, если же необходимо выполнить событие без данных
(val content: T)
, необходимо будет создать еще один класс. Не решает проблему исполнения одного типа события один раз (само событие выполняется один раз, но данный тип события будет выполнятся столько раз, сколько мы его запустим из ViewModel). Например, у нас асинхронно уходят N-запросов, но сети нет. Каждый запрос вернется с ошибкой сети, и запулит во фрагмент N событий об ошибке сети, во фрагменте откроется N алертов. Пользователь не одобрит такое решение :). Мы ему должны показать один раз сообщение с данной ошибкой. Другими словами данный тип события должен выполнится один раз.Решение
За основу возьмем идею
SingleLiveEvent
по сохранению информации о хендлинге события.Определим возможные типы событий
enum class Type {
EXECUTE_WITHOUT_LIMITS, //Самый распространенный тип события – выполнять столько раз, сколько было вызвано без проверки существует ли хоть один подписчик или нет
EXECUTE_ONCE, //Выполнить данный тип события один раз
WAIT_OBSERVER_IF_NEEDED,//Событие должно быть обязательно обработано, поэтому необходимо дождаться первого подписчика-обзёрвера на получение события и выполнить его
WAIT_OBSERVER_IF_NEEDED_AND_EXECUTE_ONCE //Событие должно быть обязательно обработано, поэтому необходимо дождаться первого подписчика-обзёрвера на получение события и выполнить данный тип события один раз
}
Создаем базовый класс события – NavigationEvent
isHandled
указывает на то, было ли событие получено (мы считаем, что оно выполнено, если было получено Observer во фрагменте).open class NavigationEvent(var isHandled: Boolean = false, var type: Events.Type)
Создаем класс Эмиттера – Emitter
Класс эмиттера событий наследуется от
LiveData
<NavigationEvent
>. Данный класс будет использоваться во ViewModel для отправки событий.class Emitter : MutableLiveData<NavigationEvent>() {
private val waitingEvents: ArrayList<NavigationEvent> = ArrayList()
private var isActive = false
override fun onInactive() {
isActive = false
}
override fun onActive() {
isActive = true
val postingEvents = ArrayList<NavigationEvent>()
waitingEvents
.forEach {
if (hasObservers()) {
this.value = it
postingEvents.add(it)
}
}.also { waitingEvents.removeAll(postingEvents) }
}
private fun newEvent(event: NavigationEvent, type: Type) {
event.type = type
this.value = when (type) {
Type.EXECUTE_WITHOUT_LIMITS,
Type.EXECUTE_ONCE -> if (hasObservers()) event else null
Type.WAIT_OBSERVER_IF_NEEDED,
Type.WAIT_OBSERVER_IF_NEEDED_AND_EXECUTE_ONCE -> {
if (hasObservers() && isActive) event
else {
waitingEvents.add(event)
null
}
}
}
}
/** Clear All Waiting Events */
fun clearWaitingEvents() = waitingEvents.clear()
}
isActive
– необходим нам для понимания – подписан ли на Emitter
хотя бы один Observer
. И в случае, когда подписчик появился и накопились события, ожидающие его, мы отправляем эти события. Важное уточнение: отправлять события необходимо не через this.postValue(event)
, а через сеттер this.value = event
. Иначе, подписчик получит только последнее событие в списке.Сам метод отправки нового события
newEvent(event, type)
– принимает два параметра – собственно, само событие и тип этого события. Чтобы не запоминать все типы событий (длинные названия), создадим отдельные public-методы, которые будут принимать только само событие:
class Emitter : MutableLiveData<NavigationEvent>() {
…
/** Default: Emit Event for Execution */
fun emitAndExecute(event: NavigationEvent) = newEvent(event, Type.EXECUTE_WITHOUT_LIMITS)
/** Emit Event for Execution Once */
fun emitAndExecuteOnce(event: NavigationEvent) = newEvent(event, Type.EXECUTE_ONCE)
/** Wait Observer Available and Emit Event for Execution */
fun waitAndExecute(event: NavigationEvent) = newEvent(event, Type.WAIT_OBSERVER_IF_NEEDED)
/** Wait Observer Available and Emit Event for Execution Once */
fun waitAndExecuteOnce(event: NavigationEvent) = newEvent(event, Type.WAIT_OBSERVER_IF_NEEDED_AND_EXECUTE_ONCE)
}
Формально, уже можно подписываться на
Emitter
во ViewModel и получать события без учета их хендлинга (было ли событие уже обработано или нет).Создадим класс наблюдателя событий – EventObserver
class EventObserver(private val handlerBlock: (NavigationEvent) -> Unit) : Observer<NavigationEvent> {
private val executedEvents: HashSet<String> = hashSetOf()
/** Clear All Executed Events */
fun clearExecutedEvents() = executedEvents.clear()
override fun onChanged(it: NavigationEvent?) {
when (it?.type) {
Type.EXECUTE_WITHOUT_LIMITS,
Type.WAIT_OBSERVER_IF_NEEDED -> {
if (!it.isHandled) {
it.isHandled = true
it.apply(handlerBlock)
}
}
Type.EXECUTE_ONCE,
Type.WAIT_OBSERVER_IF_NEEDED_AND_EXECUTE_ONCE -> {
if (it.javaClass.simpleName !in executedEvents) {
if (!it.isHandled) {
it.isHandled = true
executedEvents.add(it.javaClass.simpleName)
it.apply(handlerBlock)
}
}
}
}
}
}
На вход данный Observer принимает функцию высшего порядка – обработка событий будет написана во фрагменте (пример ниже).
Метод
clearExecutedEvents()
для очистки выполненных событий (те, которые должны были выполнится один раз). Необходим при обновлении экрана, например, в swipeToRefresh()
.Ну и собственно, главный метод
onChange()
, который наступает при получении новых данных эмиттера, на который подписывается данный наблюдатель.В случае, если событие имеет тип выполнения неограниченного количества раз, то мы проверяем, было ли событие выполнено и обрабатываем его. Выполняем событие и указываем, что оно получено и обработано.
if (!it.isHandled) {
it.isHandled = true
it.apply(handlerBlock)
}
Если событие с типом, который должен выполнится единожды, то проверяем, находится ли класс данного события в хеш-таблице. В случае отсутствия – выполняем событие и добавляем класс данного события в хэш-таблицу.
if (it.javaClass.simpleName !in executedEvents) {
if (!it.isHandled) {
it.isHandled = true
executedEvents.add(it.javaClass.simpleName)
it.apply(handlerBlock)
}
}
А как же передавать данные внутри событий?
Для этого создается интерфейс
MyFragmentNavigation
, который будет состоять из классов, наследуемых от NavigationEvent()
. Ниже созданы различные классы с передаваемыми параметрами и без них.interface MyFragmentNavigation {
class ShowCategoryList : NavigationEvent()
class OpenProduct(val productId: String, val productName: String) : NavigationEvent()
class PlayVideo(val url: String) : NavigationEvent()
class ShowNetworkError : NavigationEvent()
}
Как это работает на практике
Отправка событий из ViewModel:
class MyViewModel : ViewModel() {
val emitter = Events.Enitter()
fun doOnShowCategoryListButtonClicked() = emitter.emitAndExecute(MyNavigation.ShowCategoryList())
fun doOnPlayClicked() = emitter.waitAndExecuteOnce(MyNavigation.PlayVideo(url = "https://site.com/abc"))
fun doOnProductClicked() = emitter.emitAndExecute(MyNavigation.OpenProduct(
productId = "123",
productTitle = "Часы Samsung")
)
fun doOnNetworkError() = emitter.emitAndExecuteOnce(MyNavigation.ShowNetworkError())
fun doOnSwipeRefresh(){
emitter.clearWaitingEvents()
..//loadData()
}
}
Получение событий во фрагменте:
class MyFragment : Fragment() {
private val navigationEventsObserver = Events.EventObserver { event ->
when (event) {
is MyFragmentNavigation.ShowCategoryList -> ShowCategoryList()
is MyFragmentNavigation.PlayVideo -> videoPlayerView.loadUrl(event.url)
is MyFragmentNavigation.OpenProduct -> openProduct(id = event.productId, name = event.otherInfo)
is MyFragmentNavigation.ShowNetworkError -> showNetworkErrorAlert()
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//Один Observer на несколько ViewModels в рамках одного фрагмента
myViewModel.emitter.observe(viewLifecycleOwner, navigationEventsObserver)
myViewModelSecond.emitter.observe(viewLifecycleOwner, navigationEventsObserver)
myViewModelThird.emitter.observe(viewLifecycleOwner, navigationEventsObserver)
}
private fun ShowCategoryList(){
...
}
private fun openProduct(id: String, name: String){
...
}
private fun showNetworkErrorAlert(){
...
}
}
По сути получился аналог Rx-
BehaviorSubjec
t-a и EventBus
-a, только основанный на LiveData
, в котором Emitter
умеет собирать события до появления исполнителя-Observer-а, и в котором Observer умеет следить за типами событий и по необходимости вызывать их только один раз.Велком в комментарии с предложениями.
Ссылка на исходники.
Рассрочка от Банка Хоум Кредит.
Комментарии (4)
turlir
27.12.2019 21:37Что будете делать, когда у разных событий возникнут одинаковые аргументы? Расширять иерархию
MyFragmentNavigation
абстрактным классом? Кажется тут самое место sealed class. Вы пишите, что получился аналог Rx-BehaviorSubject-a, но RxJava хороша своей расширяемостью. Для ее существует куча операторов, можно и свой какой-нибудь написать. Если речь про взаимодействие Presenter и View не проще использовать Moxy? Там организованы похожие стратегии, которые можно расширять.Klukwist Автор
27.12.2019 21:50Разные события — разные классы с необходимыми аргументами в данном событии. Иначе, как отличать события и их последующую обработку, если события разные? А если обработка событий одинаковая и аргументы одинаковые, то может и событие одно? :)
Использовать можно хоть sealed class, хоть просто class (но не data class).
Вы пишите, что получился аналог Rx-BehaviorSubject-a, но RxJava хороша своей расширяемостью.
У нас в проекте нет RxJava.
Если речь про взаимодействие Presenter и View не проще использовать Moxy? Там организованы похожие стратегии, которые можно расширять.
Здесь про MVVM, не MPV.
Pro-invader
Как раз они связаны. ViewModel живет до onDestroy() в Activity, потом вызывается onCleared().
Klukwist Автор
Здесь о фрагментах думал, конечно, т. к. мы с ними. Уточнение верное, спасибо!