[Разработка под Android] Так для чего же нам все таки нужен MVI в мобильной разработке
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Много уже сказано про 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()
Опять же для того, чтобы у нас на экране ничего не скакало поверх остальных юайных элементов, пресловутая консистентность.
Вот и всплывает вопрос, кто ударит тебя по рукам, если сделаешь что-то не так? Ответ прост — НИКТО. В такой реализации у тебя нет абсолютно никакого контроля.
Возможно в будущем понадобится добавить еще какую-либо функциональность во вью, которая также будет связано с тем, что уже есть. Какие минусы мы из этого получаем?
- Лапша из зависимостей состояний юайных элементов
- Логика переходов из одного состояния отображения в другое будет размазана по
презентеру
- Довольно тяжело добавлять новое состояние экрана, так как велик риск того, что
забудешь что-то скрыть перед тем как отобразить новый баннер или диалог
И это мы с тобой разбирали кейс когда во вью всего 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
Вся суть
Суть в том, что мы имеем сущность, которая называется стейт. На основе этого стейта вьюшка будет рендерить свое отображение. Не буду углубляться, так моя задача пробудить в тебе интерес, так что сразу перейду к примерам. А в конце статьи будет список очень полезных источников, если тебя все же появится интерес.
Давай вспомним наше положение в начале статьи, у нас есть вью, на которой мы показываем диалоги, баннеры и волшебство. Опишем ка мы с тобой стейт вьюшки
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.
Однако не стоит рано радоваться, у всего в этом мире есть как плюсы так и минусы, а вот и они
- Нас ломает об ногу обычный показ toast
- При обновлении одного флажка весь стейт будет скопирован заново и отправлен во
вью, то есть произойдет ненужная перерисовка, если ничего с этим не сделать
Предположим, что мы хотим вывести обычный андроидный toast, по текущей логике мы заведем в нашем стейте флаг для вывода нашего тостика.
data class UIState(
val showToast: Boolean = false,
)
Первое
Берем и меняем стейт в презентере, ставим showToast = true и самое простое, что может произойти это поворот экрана. Все уничтожается взрывы и разрушения активити пересоздается, но так как ты крутой разработчик твой стейт все это дело переживает. А в стейте у нас волшебство флаг, который говорит отобразить toast. Результат — toast показывается дважды. Для решения данной проблемы есть несколько способов и все выглядят как костыли. Опять же об этом будет написано в источниках, приложенных к этой статье.
Ну, а второе
Это уже проблема ненужных отрисовок во вью, которые будут происходить каждый раз даже когда в стейте меняется всего одно из полей. И эта проблема решается несколькими иногда не самыми красивыми способами (порой тупой проверкой перед тем как сетать во вью новое значение, на то, что оно отличается от предыдущего). Но с выходом compose в stable версию эта проблема будет решена, вот тогда мой друг заживем с тобой в преображенном и счастливом мире!
Время для плюсов:
- Одна точка входа во вью
- Мы всегда под рукой имеем текущее состояние экрана
- Еще на стадии реализации приходится продумывать как один стейт будет перетекать
в другой и какая между ними связь
- 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 — статья Ханнеса Доорфмана
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка под Android] Загрузка и сборка AOSP
- [Разработка под Android, Тестирование мобильных приложений] На чем писать Android UI-тесты
- [Разработка под iOS, Разработка мобильных приложений, Разработка под Android] «Ну, покати!» или CI/CD мобильных приложений на основе контракта
- [Разработка под Android] Анализ сервисов приема SMS для Android против сайтов-сервисов и опыт разработки нового функционала под Android
- [Kotlin, Голосовые интерфейсы, Разработка мобильных приложений, Разработка под Android] Как встроить голосового помощника в любое мобильное приложение. Разбираем на примере Habitica
- [Google App Engine, Разработка мобильных приложений, Разработка под Android] Google начала публичный альфа-тест Jetpack Compose
- [Разработка мобильных приложений, Разработка под Android, Социальные сети и сообщества] Приложения для сети Fediverse могут удалить из Google Play из-за недопустимого контента
- [Разработка под Android, Смартфоны, IT-компании] Яндекс объявил о закрытии «Яндекс.Store»
- [Разработка под iOS, Разработка под Android, Тестирование мобильных приложений, Kotlin] Архитектурный шаблон MVI в Kotlin Multiplatform. Часть 3: тестирование
- [Разработка мобильных приложений, Разработка под Android, Развитие стартапа, Офисы IT-компаний, Криптовалюты] Top 10 Blockchain Development Companies To Partner With In 2020-21
Теги для поиска: #_razrabotka_pod_android (Разработка под Android), #_android_development, #_mvi, #_architecture, #_razrabotka_pod_android (
Разработка под Android
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 11:28
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Много уже сказано про 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() Опять же для того, чтобы у нас на экране ничего не скакало поверх остальных юайных элементов, пресловутая консистентность. Вот и всплывает вопрос, кто ударит тебя по рукам, если сделаешь что-то не так? Ответ прост — НИКТО. В такой реализации у тебя нет абсолютно никакого контроля. Возможно в будущем понадобится добавить еще какую-либо функциональность во вью, которая также будет связано с тем, что уже есть. Какие минусы мы из этого получаем?
И это мы с тобой разбирали кейс когда во вью всего 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 Вся суть Суть в том, что мы имеем сущность, которая называется стейт. На основе этого стейта вьюшка будет рендерить свое отображение. Не буду углубляться, так моя задача пробудить в тебе интерес, так что сразу перейду к примерам. А в конце статьи будет список очень полезных источников, если тебя все же появится интерес. Давай вспомним наше положение в начале статьи, у нас есть вью, на которой мы показываем диалоги, баннеры и волшебство. Опишем ка мы с тобой стейт вьюшки 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. Однако не стоит рано радоваться, у всего в этом мире есть как плюсы так и минусы, а вот и они
Предположим, что мы хотим вывести обычный андроидный toast, по текущей логике мы заведем в нашем стейте флаг для вывода нашего тостика. data class UIState(
val showToast: Boolean = false, ) Первое Берем и меняем стейт в презентере, ставим showToast = true и самое простое, что может произойти это поворот экрана. Все уничтожается взрывы и разрушения активити пересоздается, но так как ты крутой разработчик твой стейт все это дело переживает. А в стейте у нас волшебство флаг, который говорит отобразить toast. Результат — toast показывается дважды. Для решения данной проблемы есть несколько способов и все выглядят как костыли. Опять же об этом будет написано в источниках, приложенных к этой статье. Ну, а второе Это уже проблема ненужных отрисовок во вью, которые будут происходить каждый раз даже когда в стейте меняется всего одно из полей. И эта проблема решается несколькими иногда не самыми красивыми способами (порой тупой проверкой перед тем как сетать во вью новое значение, на то, что оно отличается от предыдущего). Но с выходом compose в stable версию эта проблема будет решена, вот тогда мой друг заживем с тобой в преображенном и счастливом мире! Время для плюсов:
Любите андроид и никогда не теряйте свою мотивацию! Список моих вдохновителей
=========== Источник: habr.com =========== Похожие новости:
Разработка под Android ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 11:28
Часовой пояс: UTC + 5