Site icon AppTractor

Осваиваем ViewModel в Android: «можно» и «нельзя» — Часть 4

Это будет четвертая статья из серии «Осваиваем ViewModel в Android». Мы обсудили советы по улучшению производительности и качества кода во ViewModel, которые в настоящее время являются основным компонентом приложений для Android.

В предыдущих частях мы уже обсуждали:

  1. Избегайте инициализации состояния в блоке init{}.
  2. Избегайте раскрытия мутабельных состояний.
  3. Используйте update{} при использовании MutableStateFlows.
  4. Старайтесь не импортировать зависимости Android во ViewModel
  5. Лениво внедряйте зависимости в конструктор.

В этой части мы обсудим 6-8 пункты из списка:

  1. Примите более реактивное и менее императивное программирование.
  2. Избегайте инициализации ViewModel из внешнего мира.
  3. Избегайте передачи параметров из внешнего мира.
  4. Избегайте жесткого прописывания диспетчеров корутинов.
  5. Проводите модульное тестирование своих ViewModel.
  6. Избегайте раскрытия suspended функций.
  7. Используйте обратный вызов onCleared() во ViewModel.
  8. Обрабатывайте смерть процесса и изменения конфигурации.
  9. Вставляйте UseCases, которые вызывают Репозитории, которые, в свою очередь, вызывают DataSource.
  10. Включайте в ViewModel только доменные объекты.
  11. Используйте операторы 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 }
    }
}

Преимущества реактивного подхода

Переход от императивного к реактивному подходу в 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. Проблемы жизненного цикла

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

2. Жесткая связь

3. Нарушение ответственности

4. Ошибки

5. Трудности тестирования

6. Расточительство ресурсов

7. Повышенное обслуживание

8. Проблемы с производительностью

Мы можем поговорить о других возможных минусах использования этого подхода, но давайте обсудим, что можно было бы сделать иначе.

Рекомендуемый подход

Чтобы избежать этих проблем, ViewModel должна обрабатывать свою собственную инициализацию внутри и не полагаться на инициализацию ViewModel снаружи. Мы можем сделать это с помощью комбинации использования первого пункта из первой статьи этой серии.

И используя SavedStateHandle из библиотеки жизненного цикла AndroidX, посмотрите на следующую статью от Google.

Таким образом, используя SavedStateHandle и получая необходимые аргументы из любой используемой нами навигационной библиотеки, инициализация ViewModel в случаях, когда нам нужен внешний параметр извне, будет гораздо эффективнее. А если нам не нужны никакие внешние параметры, используя первый пункт, рассмотренный в первой части, мы можем вообще избежать ручной инициализации!

Заключение

Освоение использования ViewModels в разработке под Android очень важно для создания надежных, эффективных и поддерживаемых приложений. В этой серии мы рассмотрели полный набор лучших практик, призванных улучшить качество кода и производительность приложения.

Источник

Exit mobile version