Все мы читали туториалы по MVI (Unidirectional Data Flow). Идея звучит безупречно: у вас есть один
Intent (событие от юзера), один Reducer и один State (дата-класс, описывающий весь экран).На экране авторизации это выглядит как поэзия. Но потом вы приходите на реальный прод.
Вам дают Главный Экран приложения: здесь лента постов, фильтры, баннеры, статус сети, профиль юзера в шапке и плеер в свернутом виде.
И тут начинается MVI-ад:
data class DashboardState(
val isLoading: Boolean = false,
val feed: List<Post> = emptyList(),
val searchQuery: String = "",
val activeFilters: Set<Filter> = emptySet(),
val userProfile: Profile? = null,
val unreadNotificationsCount: Int = 0,
val miniPlayerState: PlayerState = PlayerState.Idle,
// ... и еще 20 полей
)
В чем боль такого подхода?
1. Адские
copy(): Пользователь вводит текст в строку поиска. На каждый символ вы делаете _state.value = _state.value.copy(searchQuery = newText). Вы пересоздаете гигантский объект состояния ради одного символа.2. Конфликты Reducer'ов: Разные корутины грузят профиль, ленту и нотификации. Они начинают драться за актуальный
state.value, затирая copy друг друга, если не использовать update { } (а с ним код становится еще более громоздким).3. God-object ViewModel: Ваша ViewModel разрастается до 1500 строк, потому что она вынуждена имплементить интерфейсы плеера, пагинации, аналитики и поиска.
🛠 Как это чинят Сеньоры? Декомпозиция.
Прагматичный подход гласит: Не будьте догматиками. Один экран НЕ обязан иметь ровно один
StateFlow.Паттерн 1: Раздельные потоки (State Decomposition)
Вместо одного монолитного стейта, разбейте его на логические блоки внутри одной ViewModel:
class DashboardViewModel : ViewModel() {
val searchState: StateFlow<SearchState> = ...
val feedState: StateFlow<FeedState> = ...
val playerState: StateFlow<PlayerState> = ...
}
UI (Compose или Fragment) просто подписывается на нужные куски. Ввод текста в поиск больше не триггерит пересоздание списка постов.
Паттерн 2: Компонентный подход (Множественные ViewModel)
Кто сказал, что на Fragment/Activity может быть только одна ViewModel?
Если у вас на экране есть сложный независимый блок (например, Mini Player), дайте ему свою
PlayerViewModel. Пусть она живет в скоупе Activity и отвечает только за плеер. Главный экран будет чище.MVI это паттерн, а не религия. Умейте дробить стейт, когда он начинает пахнуть.
✍️ @kotlin_lib