Так для чего же нам все таки нужен MVI в мобильной разработке

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
Много уже сказано про MVI, о том как его правильно прожарить и настроить. Однако не так много времени уделяется тому, насколько этот метод упрощает жизнь в определенных ситуациях, в сравнении с остальными подходами.

Цель этой статьи


Я не буду углубляться в то как технически реализуется MVI (способов больше одного и у каждого есть свои плюсы и минусы). Моя главная цель в короткой статье заинтересовать тебя изучать эту тему в дальнейшем и возможно побудить внедрить данный паттерн на своих боевых проектах или хотя бы проверить на домашних заготовках.

С какой проблемой можно столкнуться


Мой дорогой друг, давай представим такую ситуацию, у нас имеется интерфейс вью, с которым
предстоит работать:

interface ComplexView { 
   fun showLoading()   
   fun hideLoading()   
   fun showBanner()    
   fun hideBanner()    
   fun dataLoaded(names: List<String>)    
   fun showTakeCreditDialog()
   fun hideTakeCreditDialog()
}

На первый взгляд кажется, что ничего сложного. Ты просто выделяешь для работы с этой вьюшкой отдельную сущность, называешь ее презентером (вуаля вот и MVP готов), а это большие проблемы небольшие сложности и сейчас я постараюсь объяснить почему.

А вот и сам презентер:

interface Presenter {  
   fun onLoadData(dataKey: String)    
   fun onLoadCredit()
}

Все просто, вьюшка дергает методы презентера, когда надо загрузить данные, презентер в свою очередь имеет право дергать вьюшку, для того, чтобы отобразить загруженную информацию, а также отобразить прогресс. Но тут и появляется проблема сложность — это абсолютное отсутствие контроля консистентности твоего UI мой товарищ.

К примеру мы хотим вывести диалог предлагающий кредит выгодное предложение пользователю и делаем этот вызов из презентера, имея на руках ссылку на интерфейс вью:

view.hideTakeCreditDialog()


Но при этом тебе не следует забывать, что при отображении диалога, нужно скрывать лоадинг и не показывать его пока у тебя имеется диалог на экране. Плюс к этому есть метод, показывающий баннер, который мы не должны вызывать пока у нас отображается диалог (либо закрывать диалог и уже после этого отображать баннер, все зависит от требований). Картина у тебя складывается такая.

Тебе ни в коем случае нельзя вызывать:

view.showBanner()


view.showLoading()


Пока идет показ диалога. Иначе будут плакать котики тестировщики и пользователи, от боли в глазах от одного взгляда на баннер поверх важного диалога c кредитом выгодным предложением.

А сейчас давай еще подумаем с тобой и предположим, что все таки захотелось показать баннер (такое уж требование от бизнеса). О чем же надо помнить?
Дело в том, что при вызове сего метода:

view.showBanner()


Обязательно надо вызывать:

view.hideLoading()


view.hideTakeCreditDialog()


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

Вот и всплывает вопрос, кто ударит тебя по рукам, если сделаешь что-то не так? Ответ прост — НИКТО. В такой реализации у тебя нет абсолютно никакого контроля.

Возможно в будущем понадобится добавить еще какую-либо функциональность во вью, которая также будет связано с тем, что уже есть. Какие минусы мы из этого получаем?

  1. Лапша из зависимостей состояний юайных элементов
  2. Логика переходов из одного состояния отображения в другое будет размазана по
    презентеру
  3. Довольно тяжело добавлять новое состояние экрана, так как велик риск того, что
    забудешь что-то скрыть перед тем как отобразить новый баннер или диалог

И это мы с тобой разбирали кейс когда во вью всего 7 методов. И даже тут получилось встрять на проблемы.

А ведь бывают вот такие вью:

interface ChatView : IView<ChatPresenter> {
    fun setMessage(message: String)    
    fun showFullScreenProgressBar()    
    fun updateExistingMessage(model: ChatMessageModel)    
    fun hideFullScreenProgressBar()    
    fun addNewMessage(localMessage: ChatMessageModel)    
    fun showErrorFromLoading(message: String)       
    fun moveChatToStart()    
    fun containsMessage(message: ChatMessageModel): Boolean    
    fun getChatMessagesSize(): Int    fun getLastMessage(): ChatMessageModel?    
    fun updateMessageStatus(messageId: String, status: ChatMessageStatus)    
    fun setAutoLoading(autoLoadingEnabled: Boolean)
    fun initImageInChat(needImageInChat: Boolean)    
    fun enableNavigationButton()   
    fun hideKeyboard()   
    fun scrollToFirstMessage()    
    fun setTitle(@StringRes titleRes: Int)    
    fun setVisibleSendingError(isVisible: Boolean)    
    fun removeMessage(localId: String)    
    fun setBottomPadding(hasPadding: Boolean)    
    fun initMessagesList(pageSize: Int)    
    fun showToast(@StringRes textRes: Int)   
    fun openMessageDialog(message: String)   
    fun showSuccessRating()    
    fun setRatingAvailability(isEnabled: Boolean)    
    fun showSuccessRatingWithResult(ratingValue: String)
}

Добавлять сюда что-либо новое или править старое будет довольно тяжело, придется посмотреть, что и как связано между собой, а потом начать плакать работать и молиться, чтобы тестировщик ничего не пропустил. И в момент твоего отчаяния появляется он.

MVI



image

Вся суть


Суть в том, что мы имеем сущность, которая называется стейт. На основе этого стейта вьюшка будет рендерить свое отображение. Не буду углубляться, так моя задача пробудить в тебе интерес, так что сразу перейду к примерам. А в конце статьи будет список очень полезных источников, если тебя все же появится интерес.

Давай вспомним наше положение в начале статьи, у нас есть вью, на которой мы показываем диалоги, баннеры и волшебство. Опишем ка мы с тобой стейт вьюшки

data class UIState(
       val loading: Boolean = false,
       val names: List<String>? = null,    
       val isBannerShowing: Boolean = false,    
       val isCreditDialogShowing: Boolean = false
)

Установим правило, мы с тобой можем менять вью только с помощью этого стейта, будет такой интерфейс:

interface ComplexView { 
   fun renderState(state: UIState)
}

А сейчас установим еще одно правило. Мы можем обращаться к владельцу стейта (в нашем случае это будет презентер) только через одну точку входа. Путем отправления ему событий. Хорошая идея назавать эти события экшенами.

sealed class UIAction {    
       class LoadNamesAction(dataKey: String) : UIAction()    
       object LoadBannerAction : UIAction()    
       object LoadCreditDialogInfo : UIAction()
}

Только не кидай в меня помидоры за силд классы, они упрощают жизнь в текущей ситуации, избавляя от дополнительных кастов при обрабокте экшенов в презентере, пример будет ниже. Интерфейс презентера будет выглядеть так:

interface Presenter { 
   fun processAction(action: UIAction)
}

А теперь давай подумаем как связать все это дело:

fun processAction(action: UiAction): UIState {    
return when (action) {        
    is UiAction.LoadNamesAction -> state.copy(
                loading = true, 
                isBannerShowing = false,         
                isCreditDialogShowing = false
    )       
    is UiAction.LoadBannerAction -> state.copy(            
                loading = false,           
                isBannerShowing = true,            
                isCreditDialogShowing = false
    )        
    is UiAction.LoadCreditDialogInfo -> state.copy(    
                loading = false,            
                isBannerShowing = false,            
                isCreditDialogShowing = true
    )    
  }
}

Если ты обратил внимание, то перетекание из одного состояния отображения в другое сейчас происходит в одном месте и уже проще сложить в голове картину как все работает.

Это не стало супер просто, однако твоя жизнь должна стать легче. Плюс в моем примере этого не видно, но мы можем решать как запроцесить наш новый стейт на основе предыдущего стейта (для реализации такого также есть несколько придумок). Я уж не говорю о безумной возможности пере использования, которой добились ребята из badoo, одним из их помощников в достижении этой цели был MVI.

Однако не стоит рано радоваться, у всего в этом мире есть как плюсы так и минусы, а вот и они


  1. Нас ломает об ногу обычный показ toast
  2. При обновлении одного флажка весь стейт будет скопирован заново и отправлен во
    вью, то есть произойдет ненужная перерисовка, если ничего с этим не сделать

Предположим, что мы хотим вывести обычный андроидный toast, по текущей логике мы заведем в нашем стейте флаг для вывода нашего тостика.

data class UIState(
       val showToast: Boolean = false,
)

Первое


Берем и меняем стейт в презентере, ставим showToast = true и самое простое, что может произойти это поворот экрана. Все уничтожается взрывы и разрушения активити пересоздается, но так как ты крутой разработчик твой стейт все это дело переживает. А в стейте у нас волшебство флаг, который говорит отобразить toast. Результат — toast показывается дважды. Для решения данной проблемы есть несколько способов и все выглядят как костыли. Опять же об этом будет написано в источниках, приложенных к этой статье.

Ну, а второе


Это уже проблема ненужных отрисовок во вью, которые будут происходить каждый раз даже когда в стейте меняется всего одно из полей. И эта проблема решается несколькими иногда не самыми красивыми способами (порой тупой проверкой перед тем как сетать во вью новое значение, на то, что оно отличается от предыдущего). Но с выходом compose в stable версию эта проблема будет решена, вот тогда мой друг заживем с тобой в преображенном и счастливом мире!

Время для плюсов:


  1. Одна точка входа во вью
  2. Мы всегда под рукой имеем текущее состояние экрана
  3. Еще на стадии реализации приходится продумывать как один стейт будет перетекать
    в другой и какая между ними связь
  4. Unidirectional Data Flow

Любите андроид и никогда не теряйте свою мотивацию!

Список моих вдохновителей


  • www.youtube.com/watch?v=VsStyq4Lzxo&t=592s — Declarative UI Patterns (Google
    I/O'19)
  • www.youtube.com/watch?v=pXw6r2kAvq8&t=2s — Architectural journey by Zsolt
    Kocsi, Badoo EN
  • www.youtube.com/watch?v=hBkQkjWnAjg&t=318s —  Как приготовить хорошо
    прожаренный MVI под Android
  • www.youtube.com/watch?v=0IKHxjkgop4 — Managing State with RxJava by Jake
    Wharton
  • hannesdorfmann.com/android/model-view-intent — статья Ханнеса Доорфмана

Источник: https://habr.com/ru/post/517962/


Интересные статьи

Интересные статьи

Недавно на проекте интегрировал модуль CRM Битрикса c виртуальной АТС Ростелеком. Делал по стандартной инструкции, где пошагово показано, какие поля заполнять. Оказалось, следование ей не гаран...
Большинство из нас бесит то, что владелец сервера читает нашу почту. Конечно, делают это алгоритмы, а не живые люди, но от этого не легче: мутная контекстная реклама, собранная из о...
На работе я занимаюсь поддержкой пользователей и обслуживанием коробочной версии CRM Битрикс24, в том числе и написанием бизнес-процессов. Нужно отметить, что на самом деле я не «чист...
Осенью прошлого года в московском офисе Яндекса прошла первая Школа бэкенд-разработки. Мы сняли занятия на видео и сегодня рады поделиться на Хабре полным видеокурсом Школы. Он позволит вам научи...
Многие из вас знают про форматы видео как PAL, NTSC и, конечно же, SECAM. Скорее всего эти аббривеатуры вы слышали, когда речь шла о видеотехнике. Толком никто не знал в чем была между ними разни...