[Разработка мобильных приложений, Разработка под Android, Kotlin] View Model не обязательно наследоваться от ViewModel
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Рекомендованные практики от Google, как правило, включают использование ViewModel в качестве базового класса для View Models (тех, которые в MVVM). ViewModel — отличная штука для сохранения чего угодно в случае поворота экрана: будь то View Model, Presenter или Router. Но можно ли получить все преимущества выживания при повороте без необходимости наследоваться от ViewModel напрямую?Почему такой вопрос вообще может прийти кому-то в голову? Для этого может быть несколько причин:
- ViewModel — это абстрактный класс. Никто не любит наследоваться от чужих классов по всему коду без веской причины.
- ViewModel завязан на Android. А что если мы захотим использовать View Model слой в Kotlin Multiplatform?
- Иногда бывает сложно тестировать View Model, если нет контроля над его CoroutineScope, который создается и контролируется внутри библиотеки и не может быть легко заменен на тестовый.
Что хорошего делает ViewModel и чего нам это стоитПеред тем, как пытаться улучшить или заменить какой-то инструмент, стоит разобраться, что он делает и что он делает не так.Выживание при повороте экрана. Самое главное и горячо любимое свойство ViewModel. Также ViewModel выживает, когда Fragment, к которому она привязана, отправляется в стек. Чего нам это стоит: Приходится расширять абстрактный класс ViewModel. Даже если то, что мы пытаемся сохранить при повороте, не является View Model из MVVM. Презентер? Наследует ViewModel. Какой-то утилитарный класс, не имеющий ничего общего с MVVM? Все равно наследует ViewModel.Очистка ресурсов. ViewModel позволяет очистить используемые ресурсы, когда она больше не будет использоваться. Как правило, это происходит, когда экран, к которому привязана VM (активити или фрагмент), закрывается насовсем.Чего нам это стоит: ViewModel должна пройти через ViewModelProvider.get(), чтобы метод 'onCleared()' работал как положено. Иначе нужно не забыть о необходимости вызвать его вручную. Кажется, что кейс с неправильным созданием VM редкий, на грани с невозможным, но при некоторых комбинациях DI и универсальных самописных ViewModelProvider.Factory такое может выстрелить в ногу. Очень маловероятно, но все же.CoroutineScope. ViewModel обеспечивает нас CoroutineScope, если мы используем ktx библиотеку. Это логично следует из двух предыдущих пунктов и может быть сделано самостоятельно, если нам, например, очень хочется запускать корутины на другом диспатчере по умолчанию.Чего нам это стоит:Немного нарушает SRP и плохо тестируется, потому что ViewModel внутри решает, как создавать и прибивать CoroutineScope. Мы это совсем не контролируем. Было бы удобнее, если бы CoroutineScope предоставлялся снаружи ViewModel и закрывался так же снаружи. В конце концов, жизненный цикл у всех VM, привязанных к одному активити или фрагменту, будет один и тот же. Почему бы им и не разделить общий CoroutineScope? Это реализовано в Hilt, но не очень очевидно, и не решает проблему с торчащей наружу extension function при использовании KTX-библиотеки.CoroutineScope доступен снаружи ViewModel. Если мы не прячем View Model за интерфейсом (а мы обычно не прячем), то слой View имеет доступ к этому CoroutineScope: он может на нем что-то запустить и «потечь». Никто в здравом уме так делать не будет, но мы все иногда нанимаем стажеров и забываем посмотреть их коммиты очень внимательно.SavedStateHandle. SavedStateHandle позволяет сохранить данные в случае «смерти» процесса. Очень полезно, хоть и не всем нужно. SavedStateHandle принимает в качестве сохраняемых данных вообще что угодно. Но в действительности может сохранить далеко не все что угодно.Что делать, чтобы получать преимущества ViewModel без прилагающихся недостатковВсе просто — нужно перестать наследоваться от VM напрямую. Для начала давайте подумаем о том, какой API нужен для того, чтобы удобно делать объект выживающим при повороте экрана. Кажется, вот так будет удобно:
inline fun <reified T: Any> ViewModelStoreOwner.getOrCreatePersisted(create: () -> T): T
И как это реализовать? Очень просто — заставить ViewModel делать всю работу за нас:
class PersistentStorage : ViewModel() {
private val persisted = mutableMapOf<Class<*>, Any>()
fun <T: Any> getOrCreate(clazz: Class<T>, create: () -> T) =
persisted.getOrPut(clazz) { create.invoke() } as T
}
inline fun <reified T : Any> ViewModelStoreOwner.getOrCreatePersisted(noinline create: () -> T): T =
ViewModelProvider(this).get<PersistentStorage>().getOrCreate(T::class.java, create)
Вот так просто, всего в несколько строк. В результате мы можем сделать что угодно выживающим при повороте. Так же, как раньше выживала ViewModel:
val myVM = getOrCreatePersisted { MyMV(params) }
Очистка ресурсов. Это все прекрасно, но API у ViewModel такой корявый не спроста: нужно научиться очищать ресурсы в том случае, когда VM больше не жилец.Вместо того, чтобы вешать на живучие классы лишние интерфейсы (которые еще и вызываются только при определенных обстоятельствах), мы можем позволить пользовательскому классу явно зарегистрироваться в качестве владельца ресурсов для их очистки. Так как все ViewModel у одного владельца (фрагмента или активити) имеют один и тот же жизненный цикл, то и ресурсы им нужно вычищать одновременно. Можно попробовать так:
interface PersistentLifecycle {
fun addOnClearResourcesListener(listener: () -> Unit)
}
class PersistentLifecycleImpl : ViewModel(), PersistentLifecycle {
private val listeners = mutableListOf<() -> Unit>()
override fun addOnClearResourcesListener(listener: () -> Unit) {
listeners.add(listener)
}
override fun onCleared() {
super.onCleared()
for (listener in listeners) {
listener.invoke()
}
listeners.clear()
}
}
fun ViewModelStoreOwner.persistentLifecycle(): PersistentLifecycle =
ViewModelProvider(this).get<PersistentLifecycleImpl>()
Может быть, нейминг далек от идеала. Но это все равно лучше, чем называть базовый класс ViewModel, когда он совсем не про MVVM :)Теперь мы можем подписаться на прибивание ресурсов в живучих классах. А «убийцу» получать в конструкторе, требуя не игнорировать эту функциональность явно публичным API:
class MyViewModel(persistentLifecycle: PersistentLifecycle) {
init {
persistentLifecycle.addOnClearResourcesListener {
// clean resources
}
}
}
CoroutineScope. Cамое простое. Нужно просто предоставить CoroutineScope снаружи, чтобы проще тестировать. А еще, чтобы совпал жизненный цикл с циклом VM:
class MyViewModel(
private val coroutineScope: CoroutineScope
) {
fun onSomethingClick() {
coroutineScope.launch {
// do something
}
}
}
Можно просто использовать существующий в ViewModel:
class CoroutineScopeViewModel : ViewModel()
fun ViewModelStoreOwner.persistentCoroutineScope() =
ViewModelProvider(this).get<CoroutineScopeViewModel>().viewModelScope
SavedStateHandle. Самая сложная часть. Нужно придумать удобный API, не зависящий напрямую от Android-библиотек.Хотелось бы использовать делегаты так:
class MyViewModel(
stateHelper: SavedStateHelper
) {
private var screenId: String by stateHelper.savedState("screenId", default = "")
}
В этом случае интерфейс SavedStateHelper будет содержать один метод:
interface SavedStateHelper {
fun <T: Any, VM : Any> savedState(key: String, default: T): ReadWriteProperty<VM, T>
}
Остается только написать реализацию этого метода:
class SavedStateHelperImpl(
private val stateHandle: SavedStateHandle
) : ViewModel(), SavedStateHelper {
override fun <T: Any, VM : Any> savedState(key: String, default: T): ReadWriteProperty<VM, T> {
return PersistentStateDelegate(stateHandle, key, default)
}
}
fun Fragment.savedStateHelper(): SavedStateHelper =
ViewModelProvider(this, SavedStateViewModelFactory(requireActivity().application, this))
.get<SavedStateHelperImpl>()
private class PersistentStateDelegate<T: Any>(
private val holder: SavedStateHandle,
private val key: String,
private val default: T
) : ReadWriteProperty<Any, T> {
override fun getValue(thisRef: Any, property: KProperty<*>): T {
return holder.get<T>(getKey(thisRef))
?: default.also { setValue(thisRef, property, it) }
}
override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
holder.set(getKey(thisRef), value)
}
private fun getKey(thisRef: Any) = "${thisRef.javaClass.name}__$key"
}
Обратите внимание на "${thisRef.javaClass.name}__$key". Ключ должен привязываться к живучему классу, иначе ключи в разных местах в пределах одного фрагмента могут оказаться одинаковыми, и мы получим что-то страшное.Такая реализация будет поддерживать те же типы, что и SavedStateHandle. Но не стоит использовать Bundle или Parcelable в View Model, если мы хотим избавиться от зависимостей на Андроид.ЗаключениеТеперь у нас есть простой API для сохранения View Model, презентеров и вообще чего угодно при повороте экрана без необходимости напрямую зависеть в этих классах от чужих библиотек. Плюс у нас появился буфер между нашим кодом и чужими библиотеками.Теперь View Model можно создать так:
val viewModel = getOrCreatePersisted {
MyViewModel(savedStateHelper(), persistentCoroutineScope(), persistentLifecycle())
}
Да, в конструктор полезло всякое — это называется Constructor Injection. Можно облегчить жизнь одним из популярных DI-фреймворков, но это уже тема для другой статьи, а то и для целой серии.UPD: уже после написания статьи оказалось что я такой не первый, и даже есть библиотека с очень похожей реализацией. Тем не менее, прорекламировать идею и обсудить ее с сообществом все еще не лишнее.Пользуясь случаем, хочу рассказать, что мы в Wrike ищем Android-разработчика с релокацией в Прагу. У нас классная маленькая команда, один большой продукт и много задач на любой вкус и цвет. И нет, мы не используем Flutter в мобильной разработке. Думаю, я бы заметил :). Если хотите познакомиться и узнать больше про нашу команду, откликайтесь на вакансию.
===========
Источник:
habr.com
===========
Похожие новости:
- [Groovy & Grails, Разработка под Android, Kotlin, Gradle] Gradle Plugin: Что, зачем и как?
- [Kotlin] Detekt — пишем свои правила
- [Информационная безопасность, Разработка мобильных приложений, Разработка под Android] Google до конца года потребует от разработчиков приложений включить двухфакторную авторизацию
- [Анализ и проектирование систем, Разработка мобильных приложений, Usability, Дизайн мобильных приложений] Запихнуть многоквартирный дом в маленький телефон
- [Разработка веб-сайтов, JavaScript, Программирование, Управление разработкой] Круглый стол в Wrike: как перевести фронтенд на новый стек
- [Разработка под iOS, Разработка мобильных приложений, Swift] Tuist: Xcode-проект на стероидах
- [Open source, Разработка мобильных приложений, Flutter] Flutter плагин для импорта стикеров в телеграм
- [Информационная безопасность, Тестирование IT-систем, Разработка под Android] Уязвимости NFC позволяют взломать банкомат, просто взмахнув смартфоном (перевод)
- [Разработка мобильных приложений, Дизайн мобильных приложений, Дизайн игр] Wild Horizon. А вы точно издатель?
- [Разработка под Android, Kotlin] Пишем комикс-приключение на Kotlin
Теги для поиска: #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_razrabotka_pod_android (Разработка под Android), #_kotlin, #_viewmodel, #_android_development, #_mvvm, #_android_architecture, #_blog_kompanii_wrike (
Блог компании Wrike
), #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
), #_razrabotka_pod_android (
Разработка под Android
), #_kotlin
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 08:04
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Рекомендованные практики от Google, как правило, включают использование ViewModel в качестве базового класса для View Models (тех, которые в MVVM). ViewModel — отличная штука для сохранения чего угодно в случае поворота экрана: будь то View Model, Presenter или Router. Но можно ли получить все преимущества выживания при повороте без необходимости наследоваться от ViewModel напрямую?Почему такой вопрос вообще может прийти кому-то в голову? Для этого может быть несколько причин:
inline fun <reified T: Any> ViewModelStoreOwner.getOrCreatePersisted(create: () -> T): T
class PersistentStorage : ViewModel() {
private val persisted = mutableMapOf<Class<*>, Any>() fun <T: Any> getOrCreate(clazz: Class<T>, create: () -> T) = persisted.getOrPut(clazz) { create.invoke() } as T } inline fun <reified T : Any> ViewModelStoreOwner.getOrCreatePersisted(noinline create: () -> T): T = ViewModelProvider(this).get<PersistentStorage>().getOrCreate(T::class.java, create) val myVM = getOrCreatePersisted { MyMV(params) }
interface PersistentLifecycle {
fun addOnClearResourcesListener(listener: () -> Unit) } class PersistentLifecycleImpl : ViewModel(), PersistentLifecycle { private val listeners = mutableListOf<() -> Unit>() override fun addOnClearResourcesListener(listener: () -> Unit) { listeners.add(listener) } override fun onCleared() { super.onCleared() for (listener in listeners) { listener.invoke() } listeners.clear() } } fun ViewModelStoreOwner.persistentLifecycle(): PersistentLifecycle = ViewModelProvider(this).get<PersistentLifecycleImpl>() class MyViewModel(persistentLifecycle: PersistentLifecycle) {
init { persistentLifecycle.addOnClearResourcesListener { // clean resources } } } class MyViewModel(
private val coroutineScope: CoroutineScope ) { fun onSomethingClick() { coroutineScope.launch { // do something } } } class CoroutineScopeViewModel : ViewModel()
fun ViewModelStoreOwner.persistentCoroutineScope() = ViewModelProvider(this).get<CoroutineScopeViewModel>().viewModelScope class MyViewModel(
stateHelper: SavedStateHelper ) { private var screenId: String by stateHelper.savedState("screenId", default = "") } interface SavedStateHelper {
fun <T: Any, VM : Any> savedState(key: String, default: T): ReadWriteProperty<VM, T> } class SavedStateHelperImpl(
private val stateHandle: SavedStateHandle ) : ViewModel(), SavedStateHelper { override fun <T: Any, VM : Any> savedState(key: String, default: T): ReadWriteProperty<VM, T> { return PersistentStateDelegate(stateHandle, key, default) } } fun Fragment.savedStateHelper(): SavedStateHelper = ViewModelProvider(this, SavedStateViewModelFactory(requireActivity().application, this)) .get<SavedStateHelperImpl>() private class PersistentStateDelegate<T: Any>( private val holder: SavedStateHandle, private val key: String, private val default: T ) : ReadWriteProperty<Any, T> { override fun getValue(thisRef: Any, property: KProperty<*>): T { return holder.get<T>(getKey(thisRef)) ?: default.also { setValue(thisRef, property, it) } } override fun setValue(thisRef: Any, property: KProperty<*>, value: T) { holder.set(getKey(thisRef), value) } private fun getKey(thisRef: Any) = "${thisRef.javaClass.name}__$key" } val viewModel = getOrCreatePersisted {
MyViewModel(savedStateHelper(), persistentCoroutineScope(), persistentLifecycle()) } =========== Источник: habr.com =========== Похожие новости:
Блог компании Wrike ), #_razrabotka_mobilnyh_prilozhenij ( Разработка мобильных приложений ), #_razrabotka_pod_android ( Разработка под Android ), #_kotlin |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 08:04
Часовой пояс: UTC + 5