Инициализация в ViewModel
Последний месяц в англоязычном сообществе в очередной раз перетирают тему как правильно триггерить инициализацию стейта в ViewModel. Сначала было обсуждение
в твиттере, потом
вот здесь по итогам появилась статья,
на реддите и в комментариях автору закидали всю панамку,
на ютубе ещё раз пережевали, и закончили всё тем, что автор
вторую статью написал с ответами (оверинжинеринг лютый) на популярные вопросы. И это всё прям интересно так, показательно.
Задача такая: есть экран, у него есть какое-то состояние, на старте триггерится какой-то процесс, например запрос в интернет, который поменяет это состояние. Вопрос звучит так: где это всё делать? Сразу оговоримся, что речь идёт только про Compose, поэтому зумеры пытаются придумать решение проблеме, которую раньше даже не приходилось решать.
Первое решение, которым, по опросу от автора, пользуется примерно треть: LaunchedEffect(Unit) { viewModel.load() }. То есть Composable функция при первой рекомпозиции сама триггерит вызов какого-то метода. Плюсы: бизнес логика отвязана от создания объекта. Минусы: можно забыть; вьюшка командует вьюмоделью; при пересоздании вьюшки всё перезагрузится.
Второй вариант более популярный - использовать init секцию ViewModel. Плюсы: вызвать инит секцию ты никогда не забудешь. Минусы: бизнес логика (запуск корутины) формально является частью создания объекта и вытекающие из этого последствия.
На это всё пришёл чувак из гугла и говорит: вообще-то и то и это антипаттерны, у нас
в документации даны
все рекомендации Рекомендации заключаются в том, что состояние экрана должно быть холодным флоу, который запускает всю логику инициализации уже когда вьюшка на него подпишется. Ну и чтобы каждый раз флоу не пересоздавать, то обмазать StateFlow какой-то стратегией типа SharingStarted.WhileSubscribed(5_000). Так, говорят, ещё у наших дедов в LiveData было.
Плюсы: бизнес логика отвязана от создания объекта. Вручную дополнительно делать ничего не нужно. Переживёт смену конфигурации. Минусы: вообще нафиг всё остальное, но мы все вам об этом не расскажем. 🏥
Мне кажется эта строчка с 5 секундами это какой-то мем, потому что на полном серьёзе это в рекомендациях писать не стали бы. Идея заключается в том, что пересоздание вьюшки типа поворотов экрана стейт переживёт, а в случае если на него "долго" никто не подписан, то флоу можно прибить. Надёжная как швейцарские часы. Перешёл на другой экран, через 10 секунд вернулся - всё перезагружается. Аргументируют это тем, что импакт от перезагрузки обычно незначительный. Без комментариев. Даже если весь мир закэшировать, то это всё равно лишняя работа по восстановлению состояния убитого ни за что.
Стейт экрана это что-то более комплексное, чем единожды созданный дата класс. Для загрузки могут понадобиться параметры пришедшие из аргументов экрана или от UI. Возможно мы загрузку захотим зарефрешить каким-нибудь свайпом. Обычно ViewModel апдейтит его из кучи разных методов и это нормально. Возвращаться в 2017 комбинируя все эти потоки данных в одну огромную реактивную цепочку я не очень хочу, я уже привык, что можно вместо этого писать простой код. Как по мне это решение становится хуже и хуже с каждым маленьким усложнением.
Справедливости ради, если мы хотим что-то подобное без этих недостатков, то кроме WhileSubscribed есть стратегии SharingStarted.Lazily или Eagerly. К ним у меня вопросов уже сильно меньше, вот
хорошая статья с похожими мыслями, они хотя бы UX не ломают. Да даже и в документации гугла по ссылкам выше это сказано, но почему-то на виду примеры кода только с первым. 😱
А вы как подобное пишете? Мне init + MutableStateFlow проще и понятнее вообще всегда. Стреляло в ногу один раз, но это уже отдельная история.