Хватит использовать ResourceProvider во ViewModel

Хватит использовать ResourceProvider во ViewModel

Alexey Bykov, Google Developer Expert for Android
Тут должен быть какой-то заумный текст, который отражает смысл выбора этой картинки, однако, его нет (или же есть, я ещё не решил)

Каждый по разному обрабатывает показ того или иного текста.

В большинстве своём, картина простая: создаётся ResourcesProvider/ResourceManager/etc с applicationContext внутри и ижектится в Presentation-слой, будь то Presenter или ViewModel.

Проблема

Смена конфигурации
Основная проблема с этим подходом — не 100 процентов кейсов смены конфигурации будут работать хорошо. Например, если вы устанавливаете текст в конструкторе ViewModel, при смене языка, пользователь увидит старый язык, т.к. ViewModel при правильной реализации переживает смену конфигурации.

class BojackHorsemanViewModel(provider: ResourceProvider): ViewModel(){
   private val liveData: LiveData<String> = MutableLiveData()

   init {
     liveData.value = provider.getString(R.string.dont_do_this_please)
   }
...


Лишняя сущность

У ResourcesProvider/ResourceManager/etc несколько задач — скрыть Android реализацию, запретить использование других методов у context и сделать Presentation слой тестируемым.
Эту проблему можно довольно просто решить и без её использования в конструкторе ViewModel, что упростит код.

Дизайн система
В больших проектах, для ускорения и стандартизации разработки, принято использовать дизайн систему. Иными словами, набор утверждённых, и главное, переиспользуемых компонентов.

Как мы видим на примере, каждый из компонентов может показывать текст. Однако, это может быть текст из ресурсов, текст, который пришёл с сервера или даже комбинация, где мы подставляем значения с сервера в уже существующую строку.
Без наличия общего решения, которое отвечает за обработку текста, сконфигурировать входящие данные и обработать результат для RowItem будет довольно-таки тяжело.

Решение

Мы можем сделать описание всех необходимых нам кейсов, связанных с текстом:

sealed class Text {
  data class Resource(@StringRes val resId: Int) : Text()
  data class ResourceParams(
       @StringRes val value: Int, 
       val args:List<Any>
  ) : Text()

  data class Simple(val text: String) : Text()
}

За обработку будет отвечать extension, который отображает текст в TextView. Крайне желательно протестировать его с помощью скриншот-тестов (например, с помощью Paparazzi):

fun TextView.setText(clause: Text) {
  when (clause) {
    is Text.Resource -> {
      setText(clause.resId)
    }
    is Text.ResourceParams -> {
      text = context.getString(clause.value, *clause.args.toTypedArray())
    }
    is Text.Simple -> {
      text = clause.text
    }
  }
}

Для JetpackCompose, решение так же легко адаптируется, однако придётся переименовать сам sealed класс во что-то другое, например, Clause.


Результат

  • ViewModel ничего не знает о ResourceProvider/ResourceManager/etc, смена конфигурации работает как надо
class BojackHorsemanViewModel(): ViewModel(){
   private val liveData: LiveData<Text> = MutableLiveData()

   init {
     liveData.value = Text.Resource(R.string.do_this)
   }
...
  • ViewModel легко тестируется
  • Компоненты дизайн-системы прозрачны и независимы
data class RowItem(val title: Text, ...)
  • Решение масштабируемо. Обработка тех же Spannable решается вводом дополнительной абстракции с описанием

Кстати, таким же образом можно обрабатывать и картинки, но об этом, как-нибудь в другой раз.

___________________________________
@invalidate_cache


09-07-2022 Немного про Manifest & Android 12

25-06-2022 Gradle Enterprise & AAB Tests

Report Page