Skip to content
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

Open
wants to merge 7 commits into
base: development
Choose a base branch
from

Conversation

SergeyKozhukhov
Copy link

No description provided.

* @param isShown признак отображения причины отсутствия данных
*/
data class Error(val message: String, val isShown: Boolean = false) : CatUiState()
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isShown нужен, чтобы при пересоздании activity заново не отображать Toast

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Идея понятная, но если ты хочешь ее решить совсем правильно то тебе нужно 2 лайвдаты/стейтфлоу. Один который с синглевентами/реплеями а другой без

Copy link
Author

@SergeyKozhukhov SergeyKozhukhov Aug 4, 2023

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
)
)
Copy link
Author

@SergeyKozhukhov SergeyKozhukhov Jul 16, 2023

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/"
Copy link
Author

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())
}
Copy link
Author

@SergeyKozhukhov SergeyKozhukhov Jul 16, 2023

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...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Или может делать в detachView
scope.coroutineContext.cancelChildren()?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Если хочешь чтобы не отменялся то целевой механизм это супервайзор

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Эксепшен лучше пробросить, либо написать catch на нужные типы исключений. Ну либо взять ексепшенхендлер

Copy link
Author

@SergeyKozhukhov SergeyKozhukhov Aug 4, 2023

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()
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Может слишком очевидный вопрос, но это ок, дергать Toast на этом контексте? Не надо ли applicationContext сделать?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тост можно показывать на любом контексте

Copy link
Author

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()
}
Copy link
Author

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) {
Copy link
Author

@SergeyKozhukhov SergeyKozhukhov Jul 16, 2023

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
}
Copy link
Author

@SergeyKozhukhov SergeyKozhukhov Jul 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Здесь бросаю, так как viewModelScope - supervisor. Но опять же, есть ли смысл?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Лучше просто сделай catch на один тип исключений

Copy link
Author

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) }
}
Copy link
Author

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()
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Хотелось бы, чтобы ViewModel переживала пересоздание, например, activity. Но как это сделать??? Подскажите, пожалуйста

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Так она же итак переживает, в этом ее особенность. Если ты ее правильно создаешь через фабрику то все ок

Copy link
Author

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
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Какие ошибки может выбрасывать Retrofit здесь?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Зависит от статус кода который придет, ну точно может выбросить Таймаут, в целом все равно все статус коды которые не входят в 200-299 будут расценены колладаптером как исключение

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Если нужно подробнее глянь реализацию колладаптеров

Copy link
Author

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())
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Правильно понимаю, что здесь нормально ставить LazyThreadSafetyMode.NONE? Или все-таки могут быть проблемы с синхронизацией?

Copy link
Collaborator

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) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Выполняю обновление через update, правильно понимаю, что это предпочтительный способ и его стоит везде использовать вместе
_uiState.value = ... ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Честно первый раз слышу про апдейт. Я знаю про emit и value. Что делает update?

Copy link
Author

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

Copy link
Collaborator

@antonkazakov antonkazakov left a 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
Copy link
Collaborator

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
Copy link
Collaborator

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())
}
Copy link
Collaborator

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()
}
Copy link
Collaborator

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())
}
Copy link
Collaborator

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())
}
Copy link
Collaborator

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()
}
Copy link
Collaborator

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) {
Copy link
Collaborator

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
}
Copy link
Collaborator

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()
Copy link
Collaborator

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)
Copy link
Author

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())
Copy link
Author

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) }
}
Copy link
Author

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```
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Добавил реализацию на MVI, но скорее в качестве эксперимента, там coroutine толком нет

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants