Разработка
Осваиваем ViewModel в Android: «можно» и «нельзя» — Часть 4
В этой серии мы рассмотрели полный набор лучших практик, призванных улучшить качество кода и производительность приложения.
Это будет четвертая статья из серии «Осваиваем ViewModel в Android». Мы обсудили советы по улучшению производительности и качества кода во ViewModel, которые в настоящее время являются основным компонентом приложений для Android.
В предыдущих частях мы уже обсуждали:
- Избегайте инициализации состояния в блоке init{}.
- Избегайте раскрытия мутабельных состояний.
- Используйте update{} при использовании MutableStateFlows.
- Старайтесь не импортировать зависимости Android во ViewModel
- Лениво внедряйте зависимости в конструктор.
В этой части мы обсудим 6-8 пункты из списка:
- Примите более реактивное и менее императивное программирование.
- Избегайте инициализации ViewModel из внешнего мира.
- Избегайте передачи параметров из внешнего мира.
- Избегайте жесткого прописывания диспетчеров корутинов.
- Проводите модульное тестирование своих ViewModel.
- Избегайте раскрытия suspended функций.
- Используйте обратный вызов
onCleared()
во ViewModel. - Обрабатывайте смерть процесса и изменения конфигурации.
- Вставляйте UseCases, которые вызывают Репозитории, которые, в свою очередь, вызывают DataSource.
- Включайте в ViewModel только доменные объекты.
- Используйте операторы
shareIn()
иstateIn()
, чтобы избежать многократных обращений к восходящему потоку.
6. Примите более реактивное и менее императивное кодирование
Представьте, что вы работаете над функцией поиска в своем приложении для Android. Вы хотите передать запрос, который пользователь вводит с клавиатуры, в API и отобразить результаты. Один из способов сделать это — традиционный императивный подход, при котором вы вручную получаете и обновляете результаты поиска. Однако более современным и эффективным способом является реактивное программирование с помощью Kotlin Flow в вашей вью-модели Android. Для начала давайте рассмотрим императивный подход.
Императивный подход
При императивном подходе вы обычно вызываете методы для получения и обновления результатов поиска напрямую. Давайте рассмотрим пример:
class SearchViewModel @Inject constructor(private val searchRepo: SearchRepository) : ViewModel() { val searchResults: StateFlow<List<SearchResult>> field = MutableStateFlow<List<SearchResult>>() fun search(query: String) { viewModelScope.launch { val results = searchRepository.search(query) searchResults.update { results } } } }
Хотя такой подход работает, он менее эффективен, особенно при работе с частыми обновлениями, такими как поисковые запросы в реальном времени. Каждое нажатие клавиши запускает новый поиск, что может быть ресурсоемким и привести к неотзывчивости пользовательского интерфейса. Кроме того, его сложнее понять, и он требует большей когнитивной нагрузки, если мы работаем с реактивным подходом в разных частях проекта, что, скорее всего, и происходит, поскольку мы работаем с Flow и LiveData в настоящее время.
Теперь давайте рассмотрим реактивный подход.
Реактивный подход
class SearchViewModel @Inject constructor(private val searchRepo: SearchRepository): ViewModel() { private val _searchQuery = MutableStateFlow("") val searchResults: StateFlow<List<SearchResult>> = _searchQuery .debounce(300) // Add a debounce to limit requests .filter(String::isNotEmpty) // Ignore empty queries .flatMapLatest(searchRepository::search) .stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) fun setSearchQuery(query: String) { _searchQuery.update { query } } }
Преимущества реактивного подхода
- Эффективность: Debouncing и фильтрация сокращают количество ненужных вызовов API, повышая производительность и улучшая пользовательский опыт.
- Отзывчивость: Пользовательский интерфейс обновляется автоматически в ответ на изменения в поисковом запросе, обеспечивая плавный и интерактивный пользовательский опыт.
- Поддерживаемость: Декларативный характер реактивного подхода делает код чище и проще в сопровождении. Он разделяет проблемы управления состоянием и обновления пользовательского интерфейса.
Переход от императивного к реактивному подходу в Android ViewModel с помощью Kotlin Flow дает значительные преимущества, особенно для таких функций, как поиск, где очень важна оперативность реагирования в реальном времени. Используя Flow, вы сможете создавать более эффективные, отзывчивые и поддерживаемые приложения.
Воспользуйтесь возможностями реактивного программирования, чтобы создавать современные приложения для Android, обеспечивающие бесперебойный пользовательский опыт.
7. Избегайте инициализации ViewModel из внешнего мира
8. Избегайте передачи параметров из внешнего мира
Одним из распространенных антипаттернов, который я встречал во многих кодовых базах, является инициализация ViewModel из внешнего мира. Такая практика может привести к различным проблемам, включая неожиданные результаты и ненужные вызовы API из-за изменений жизненного цикла.
Например, в методе onViewCreated
фрагмента или в методе onCreate
активити. Иногда из-за того, что нам нужно передать параметр для инициирования viewModel, могут возникнуть проблемы.
Рассмотрим следующий сценарий. Вам нужно получить пользовательские данные в вашей ViewModel. Типичным, но проблематичным подходом является вызов метода инициализации типа fetchUserData()
непосредственно в методе onViewCreated
фрагмента или в методе onCreate()
активити:
// In a Fragment class UserFragment : Fragment() { private val userViewModel: UserViewModel by viewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val userId = "12345" // Get this from arguments or somewhere else userViewModel.fetchUserData(userId) // Called from Fragment } }
Так что же на самом деле не так с этим подходом? Много чего, это неправильно во многих аспектах.
1. Проблемы жизненного цикла
- Избыточные вызовы API: Каждый раз, когда фрагмент или активность создается заново (например, при изменении конфигурации, например, при повороте экрана), метод
fetchUserData
вызывается снова, что приводит к ненужным и избыточным вызовам API. Чтобы проверить это, вы можете добавить несколько записей в logcat и выполнить следующую команду, чтобы увидеть, что произойдет на самом деле:
adb shell content insert --uri content://settings/system --bind name:s:font_scale --bind value:f:1.15;adb shell content insert --uri content://settings/system --bind name:s:font_scale --bind value:f:1.0
- Непоследовательные состояния: Многократные вызовы API могут привести к несогласованному состоянию, если данные меняются между вызовами, и может быть неясно, какие данные являются последними.
- Пропущенная инициализация: Если метод инициализации не вызывается из-за события жизненного цикла (например, фрагмент пересоздается до вызова), ViewModel может оказаться в неинициализированном состоянии.
2. Жесткая связь
- Зависимость между фрагментом или активностью и ViewModel: ViewModel становится тесно связанной с фрагментом или активностью, что затрудняет повторное использование ViewModel в других частях приложения.
- Повышенная сложность: Фрагмент или активность должны управлять не только логикой пользовательского интерфейса, но и логикой инициализации ViewModel, что увеличивает сложность.
3. Нарушение ответственности
- Принцип единственной ответственности: Фрагмент должен управлять логикой, связанной с пользовательским интерфейсом, и только наблюдать за выдаваемыми данными, а ViewModel должна заниматься бизнес-логикой и управлением данными. Смешивание этих обязанностей нарушает этот принцип.
4. Ошибки
- Вызовы методов вручную: Зависимость от ручного вызова методов, таких как
fetchUserData
, повышает риск человеческой ошибки, например, вы можете забыть вызвать метод или передать неверные параметры. - Проблемы со сроками жизненного цикла: Вызов методов инициализации из фрагмента или активности повышает вероятность возникновения проблем со временем жизненного цикла, когда метод может быть вызван в неподходящий момент жизненного цикла.
5. Трудности тестирования
- Сложность модульного тестирования: Юнит-тестирование ViewModel становится более сложным, поскольку ее инициализация зависит от жизненного цикла и состояния фрагмента.
- Требуется мокинг: Для тестирования требуется корректное моделирование жизненного цикла фрагмента и его взаимодействия с ViewModel, что добавляет сложности.
6. Расточительство ресурсов
- Сетевые и серверные ресурсы: Избыточные вызовы API из-за изменений конфигурации расходуют пропускную способность сети и ресурсы сервера, что сказывается как на удобстве работы пользователей, так и на нагрузке на сервер.
7. Повышенное обслуживание
- Удобство обслуживания кода: Наличие логики инициализации, распределенной между фрагментом и ViewModel, делает код более сложным для обслуживания и понимания.
- Дублирование кода: Аналогичная логика инициализации может быть продублирована в нескольких фрагментах или активностях, что приводит к увеличению объема кода для сопровождения и повышению вероятности возникновения ошибок.
- Технический долг: Если мы решим отказаться от фрагментов и полностью перейти на Compose, останется технический долг, который нужно будет отдать.
8. Проблемы с производительностью
- Фризы пользовательского интерфейса: Если вызовы API выполняются синхронно или блокируют основной поток, это может привести к паузам в работе UI и ухудшению пользовательского опыта. Требуются дополнительные усилия для переноса работы в фоновый поток по сравнению с отсутствием инициализации работы извне ViewModel.
- Неэффективное использование ресурсов: Постоянная повторная выборка данных может привести к неэффективному использованию ресурсов устройства, таких как батарея и процессор.
Мы можем поговорить о других возможных минусах использования этого подхода, но давайте обсудим, что можно было бы сделать иначе.
Рекомендуемый подход
Чтобы избежать этих проблем, ViewModel должна обрабатывать свою собственную инициализацию внутри и не полагаться на инициализацию ViewModel снаружи. Мы можем сделать это с помощью комбинации использования первого пункта из первой статьи этой серии.
И используя SavedStateHandle из библиотеки жизненного цикла AndroidX, посмотрите на следующую статью от Google.
Таким образом, используя SavedStateHandle и получая необходимые аргументы из любой используемой нами навигационной библиотеки, инициализация ViewModel в случаях, когда нам нужен внешний параметр извне, будет гораздо эффективнее. А если нам не нужны никакие внешние параметры, используя первый пункт, рассмотренный в первой части, мы можем вообще избежать ручной инициализации!
Заключение
Освоение использования ViewModels в разработке под Android очень важно для создания надежных, эффективных и поддерживаемых приложений. В этой серии мы рассмотрели полный набор лучших практик, призванных улучшить качество кода и производительность приложения.