Разработка
Осваиваем ViewModel в Android: «можно» и «нельзя» — Часть 3
Если вы используете ViewModels, помните об этих вещах для повышения качества кода.
В третьей части этой серии статей мы продолжим обсуждать лучшие практики использования ViewModels в Android.
В предыдущих частях мы обсудили:
- Избегайте инициализации состояния в блоке init{}.
- Избегайте раскрытия мутабельных состояний.
- Используйте update{} при использовании MutableStateFlows.
В этой части мы обсудим 4 и 5 пункты из списка:
- Старайтесь не импортировать зависимости Android в ViewModel.
- Лениво внедряйте зависимости в конструктор.
- Примите более реактивное и менее императивное программирование.
- Избегайте инициализации ViewModel из внешнего мира.
- Избегайте передачи параметров из внешнего мира.
- Избегайте жесткого прописывания диспетчеров корутинов.
- Проводите модульное тестирование своих ViewModel.
- Избегайте раскрытия suspended функций.
- Используйте обратный вызов
onCleared()
во ViewModel. - Обрабатывайте смерть процесса и изменения конфигурации.
- Вставляйте UseCases, которые вызывают Репозитории, которые, в свою очередь, вызывают DataSource.
- Включайте в ViewModel только доменные объекты.
- Используйте операторы
shareIn()
иstateIn()
, чтобы избежать многократных обращений к восходящему потоку.
4. Старайтесь не импортировать зависимости Android во ViewModel
Рекомендация избегать импорта зависимостей Android во ViewModel, за особыми исключениями для таких классов, как LiveData и его трансформеров, основана на принципах чистой архитектуры и тестируемости. Ниже мы объясним, что это значит и почему это важно.
1. Разделение ответственности
- Зависимости Android, такие как
R
(ресурсы) и другие классы Android-фреймворка, напрямую связаны с операционной системой Android и ее контекстом. Эти классы отвечают за управление элементами пользовательского интерфейса, доступ к ресурсам Android (строки, ресурсы drawable и т. д.) и взаимодействие с системой Android (намерения, контекст и т. д.). - Не допуская зависимости во ViewModel, вы обеспечиваете четкое разделение задач. ViewModel не нужно знать о контексте Android или элементах пользовательского интерфейса, чтобы выполнять свою работу, которая заключается в управлении данными для пользовательского интерфейса.
Но что, если мне нужно выдать какую-то строку из вью-модели как часть состояния?
Для таких случаев идеальным вариантом будет использование изолированного (sealed) интерфейса Kotlin, подробнее об этом читайте в этой статье.
2. Тестируемость
- Зависимости Android могут усложнить модульное тестирование, поскольку часто требуют наличия работающего окружения Android (например, эмулятора или физического устройства). Это может замедлить тестирование и сделать его более хрупким.
- ViewModel без зависимостей от Android можно тестировать на JVM без использования среды Android, что приводит к более быстрым и надежным тестам. LiveData и ее трансформаторы являются исключением, поскольку они разработаны с учетом жизненного цикла и могут быть легко мокированы или наблюдаемы в тестах.
3. Переносимость
Код, который не зависит напрямую от фреймворка Android, более переносим и прост в сопровождении. Его можно повторно использовать в разных частях приложения или даже в других проектах с минимальными изменениями. И даже в будущем вью-модели можно будет легко заменить, например, на Circuit Presenters из Slack, чтобы перенести вашу кодовую базу на Kotlin MultiPlatform.
А как же LiveData и трансформеры
- LiveData и ее трансформеры (например,
Transformations.map
иTransformations.switchMap
) предназначены для использования во ViewModel. Они учитывают жизненный цикл, позволяя ViewModel обновлять пользовательский интерфейс безопасным способом, реагируя на события жизненных циклов Активити и Фрагментов. - Эти классы не привязывают вашу ViewModel к конкретным элементам или ресурсам пользовательского интерфейса Android. Вместо этого они предоставляют ViewModel механизм для изменения данных с учетом жизненного цикла, который может наблюдать UI-слой.
5. Лениво внедряйте зависимости в конструктор
Инжектирование зависимостей непосредственно в конструкторы ViewModel без ленивой инициализации может привести к:
- увеличению времени запуска
- повышенному использованию памяти
- излишней загрузке процессора
Ленивая инициализация откладывает создание зависимостей до тех пор, пока они действительно не понадобятся, оптимизируя производительность и эффективность приложения. Такой подход особенно полезен для больших, редко используемых или условно необходимых зависимостей. Баланс между немедленной и ленивой инициализацией в зависимости от случаев использования зависимостей имеет решающее значение для оптимальной производительности приложения.
Неиспользование ленивой инициализации для зависимостей в конструкторах ViewModel может повлиять на производительность и использование ресурсов Android-приложения, особенно во время запуска или при создании экземпляров этих ViewModel. Ниже обсудим, как неленивая инъекция сравнивается с ленивой и почему последняя часто предпочтительнее в определенных сценариях.
Когда следует использовать ленивую инициализацию
- Большие или редко используемые зависимости: Если ViewModel имеет большие зависимости или те, к которым редко обращаются, ленивая инициализация будет полезна, поскольку она позволяет избежать затрат на инициализацию этих ресурсов до тех пор, пока они действительно не понадобятся.
- Условные зависимости: Для зависимостей, которые нужны только при определенных условиях (например, на основе действий пользователя или определенных состояний приложения), ленивая инициализация предотвращает ненужную настройку.
Пример
@HiltViewModel class BookViewModel @Inject constructor( @IoDispatcher private val ioDispatcher: CoroutineDispatcher, private val bookmarkUseCase: dagger.Lazy<BookmarkUsecase>,
Представьте, что в вашей ViewModel есть действие с закладкой, которое сработает, если пользователь нажмет на кнопку закладки. Для этой зависимости, которая внедряется через конструктор BookViewModel
, мы можем использовать ленивую инъекцию, которая отложит создание Usecase до тех пор, пока он не понадобится.