-
Notifications
You must be signed in to change notification settings - Fork 222
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Задача по Coroutines #160
base: development
Are you sure you want to change the base?
Задача по Coroutines #160
Conversation
- add custom view with custom ViewModelStoreOwner and LifecycleOwner - refactoring
* @param isShown признак отображения причины отсутствия данных | ||
*/ | ||
data class Error(val message: String, val isShown: Boolean = false) : CatUiState() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isShown нужен, чтобы при пересоздании activity заново не отображать Toast
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Идея понятная, но если ты хочешь ее решить совсем правильно то тебе нужно 2 лайвдаты/стейтфлоу. Один который с синглевентами/реплеями а другой без
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Сделал, для одного варианта с ViewModel использования StateFlow и Channel.
Но почему для правильности надо разбивать?? Насколько понимаю, наоборот подход через StateFlow корректный. В том числе на занятии по MVI его рассматривали.
Вот статья про подход
https://scribe.rip/androiddevelopers/viewmodel-one-off-event-antipatterns-16a1da869b95
Intent( | ||
this, otus.homework.coroutines.presentation.mvvm.owners.CatsActivity::class.java | ||
) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Activity ведет на другие три activity с тремя вариантами решения: с презентером, ViewModel и ViewModel с кастомными owаner-ами
|
||
private companion object { | ||
const val FACT_BASE_URL = "https://catfact.ninja/" | ||
const val IMAGES_BASE_URL = "https://api.thecatapi.com/v1/images/" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Сайт, указанный в ДЗ не грузится. Взял другое API, которое нашел
} else { | ||
CrashMonitor.trackWarning(e) | ||
_catsView?.warn(message = e.messageOrDefault()) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Вот тут непонимание. Не пробрасываю дальше CancelationException, так как PresenterScope не supervisor. Если здесь придет ошибка, то не хотелось бы, чтобы скоп отменился. Хотелось бы, чтобы дальше могли делать запросы. Поэтому не пробрасываю CancelationException. Но верно ли так??? Все-таки ниже отменяю при detachView job. Но, наверное, тогда и отмена не произойдет, так как не пробрасывается CancelationException. С другой стороны detachView вызывается в onStop, а по возвращению бы все-таки иметь живой скоп, на котором запускать launch. Или все-таки что-то путаю. При пересоздана activity все-равно будет здесь создан новый скоп, можно было бы и скоп весь отменить. Но все равно, надо ли тут пробрасывать Cancelation...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Или может делать в detachView
scope.coroutineContext.cancelChildren()?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Если хочешь чтобы не отменялся то целевой механизм это супервайзор
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Эксепшен лучше пробросить, либо написать catch на нужные типы исключений. Ну либо взять ексепшенхендлер
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
В том числе и поэтому спрашивал про ошибки ретрофита)
Пробросил здесь CancellationException.
Прочитал в этой статье про распространенную ошибку, что не пробрасывается дальше CancellationException.
https://www.lukaslechner.com/7-common-mistakes-you-might-be-making-when-using-kotlin-coroutines/
То есть всегда-всегда нужно пробрасывать дальше CancellationException, даже если отмена корутины не нужна?
Все-таки не совсем понимаю, почему в целом будет плохо отловить CancellationException, а не пробросить его. Ну то есть у нас есть, допустим, случай, что нам не нужно отменять Крутину, почему просто не словить CancellationException. Какие могут быть последствия? То, что, например, ViewModel отменят скоп по уничтожению, он отменяет всех своих детей, но эта Крутина продолжает работать, так как перехватила CancellationException?
|
||
override fun warn(message: String) { | ||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Может слишком очевидный вопрос, но это ок, дергать Toast на этом контексте? Не надо ли applicationContext сделать?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Тост можно показывать на любом контексте
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Имел ввиду, что если правильно понимаю, внутри View будет context activity получаться через context, так как она в нем лежит. Но не будет ли утечек никаких, если этот контент использовать для Toast-а внутри View, а не в самой Activity. Предполагаю, что все нормально будет
} else { | ||
toast.show() | ||
viewModel.onErrorShown() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Чтобы повторно не показывать Toast при пересоздании
* Принудительное выполнение загрузки - `true`. Ленивое выполнение загрузки - `false` | ||
* (загрузка будет выполняться только в случае, если она еще не проводилась) | ||
*/ | ||
fun getRandomCat(force: Boolean) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
force чтобы при attach не дергать доп запросы
onError(e) | ||
if (e is CancellationException) { | ||
throw e | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Здесь бросаю, так как viewModelScope - supervisor. Но опять же, есть ли смысл?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Лучше просто сделай catch на один тип исключений
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Сделал
lifecycleScope.launch { | ||
repeatOnLifecycle(Lifecycle.State.CREATED) { | ||
viewModel.uiState.collect { state -> update(state) } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lifecycle.State.CREATED - так как здесь только он и есть.
Стоит ли дополнять другими состояния - вопрос.
override fun onDetachedFromWindow() { | ||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) | ||
viewModelStore.clear() | ||
super.onDetachedFromWindow() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Хотелось бы, чтобы ViewModel переживала пересоздание, например, activity. Но как это сделать??? Подскажите, пожалуйста
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Так она же итак переживает, в этом ее особенность. Если ты ее правильно создаешь через фабрику то все ок
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
В этой CatsView реализую самостоятельно ViewModelStore, LifecycleRegistry и наследую View от ViewModelStoreOwner, LifecycleOwner. Через lifecycleRegistry вызываю Event-ы на ON_CREATE и ON_DESTROY.
То есть ViewModel закрепляется за самой View, а не за Activity или Fragment-ом. Получаю ViewModel через фабрику.
Но хотелось бы, чтобы при, например, повороте экрана запрос из ViewModel продолжил бы выполняться. В целом, чтобы ViewModel осталась жива. Но как это сделать - не придумал. ViewModelStore очищается при повороте экрана и ViewModel создается заново. Не знаю, насколько нормально придумывать что-то с передачей через Bundle. Какие-то практики может подскажите?)
|
||
/** Получить случайных факт о кошке */ | ||
@GET("fact") | ||
suspend fun getCatFact(): Fact |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Какие ошибки может выбрасывать Retrofit здесь?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Зависит от статус кода который придет, ну точно может выбросить Таймаут, в целом все равно все статус коды которые не входят в 200-299 будут расценены колладаптером как исключение
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Если нужно подробнее глянь реализацию колладаптеров
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Спасибо! Пока кажется, что с ретрофитом мы не можем четко прописать бросаемые исключения, так как они внутри него самого прописаны и их могут менять
/** Репозиторий информации о кошке */ | ||
val catRepository: CatRepository by lazy(LazyThreadSafetyMode.NONE) { | ||
CatRepositoryImpl(provideFactService(), provideImagesService(), CatConverter()) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Правильно понимаю, что здесь нормально ставить LazyThreadSafetyMode.NONE? Или все-таки могут быть проблемы с синхронизацией?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Все верно, так гораздо лучше
try { | ||
val catInfo = repository.getCatInfo() | ||
_uiState.update { CatUiState.Success(catInfo) } | ||
} catch (e: Exception) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Выполняю обновление через update, правильно понимаю, что это предпочтительный способ и его стоит везде использовать вместе
_uiState.value = ... ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Честно первый раз слышу про апдейт. Я знаю про emit и value. Что делает update?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Вот здесь хорошо описано
https://scribe.rip/geekculture/atomic-updates-with-mutablestateflow-dc0331724405
В примерах у гугла, кажется, его видел еще. Но не нашел что-то сейчас.
update синхронизировано обновляет значение.
В целом кажется, что здесь может быть излишне, здесь не использую IO и Default
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Привет. Ответил на вопросы, замечаний вроде нет
|
||
/** Получить случайных факт о кошке */ | ||
@GET("fact") | ||
suspend fun getCatFact(): Fact |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Зависит от статус кода который придет, ну точно может выбросить Таймаут, в целом все равно все статус коды которые не входят в 200-299 будут расценены колладаптером как исключение
|
||
/** Получить случайных факт о кошке */ | ||
@GET("fact") | ||
suspend fun getCatFact(): Fact |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Если нужно подробнее глянь реализацию колладаптеров
/** Репозиторий информации о кошке */ | ||
val catRepository: CatRepository by lazy(LazyThreadSafetyMode.NONE) { | ||
CatRepositoryImpl(provideFactService(), provideImagesService(), CatConverter()) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Все верно, так гораздо лучше
* @param isShown признак отображения причины отсутствия данных | ||
*/ | ||
data class Error(val message: String, val isShown: Boolean = false) : CatUiState() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Идея понятная, но если ты хочешь ее решить совсем правильно то тебе нужно 2 лайвдаты/стейтфлоу. Один который с синглевентами/реплеями а другой без
} else { | ||
CrashMonitor.trackWarning(e) | ||
_catsView?.warn(message = e.messageOrDefault()) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Если хочешь чтобы не отменялся то целевой механизм это супервайзор
} else { | ||
CrashMonitor.trackWarning(e) | ||
_catsView?.warn(message = e.messageOrDefault()) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Эксепшен лучше пробросить, либо написать catch на нужные типы исключений. Ну либо взять ексепшенхендлер
|
||
override fun warn(message: String) { | ||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Тост можно показывать на любом контексте
try { | ||
val catInfo = repository.getCatInfo() | ||
_uiState.update { CatUiState.Success(catInfo) } | ||
} catch (e: Exception) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Честно первый раз слышу про апдейт. Я знаю про emit и value. Что делает update?
onError(e) | ||
if (e is CancellationException) { | ||
throw e | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Лучше просто сделай catch на один тип исключений
override fun onDetachedFromWindow() { | ||
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) | ||
viewModelStore.clear() | ||
super.onDetachedFromWindow() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Так она же итак переживает, в этом ее особенность. Если ты ее правильно создаешь через фабрику то все ок
|
||
/** Событие ошибки */ | ||
val errorEvents get() = _errorEvent.receiveAsFlow() | ||
private val _errorEvent = Channel<String>(CONFLATED) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Сделал CONFLATED, но может есть лучше практики
CrashMonitor.trackWarning(e) | ||
viewModelScope.launch(dispatcher.mainImmediate) { | ||
_errorEvent.send( | ||
stringProvider.getString(R.string.unexpected_request_error, e.messageOrDefault()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Здесь делаю launch на main.immeduate по вот этому описанию
Kotlin/kotlinx.coroutines#2886 (comment)
} | ||
launch(Dispatchers.Main.immediate) { | ||
viewModel.errorEvents.collect { message -> onError(message) } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Здесь также для Channel-а использую Dispatchers.Main.immediate
по той статье
1. ```/mvp``` - реализация *custom view* на основе паттеран `MVP` | ||
2. ```/mvvm/parent``` - реализация *custom view* на основе паттерна ```MVVM``` и `findViewTreeViewModelStoreOwner` | ||
3. ```/mvvm/owners``` - реализация *custom view* на основе паттерна ```MVVM``` и *custom* ```ViewModelStoreOwner, LifecycleOwner``` | ||
4. ```/mvi``` - реализация *custom view* на основе паттерна ```MVI``` и библиотеки ```MVICore``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Добавил реализацию на MVI, но скорее в качестве эксперимента, там coroutine толком нет
No description provided.