From e92e6aef02cc973e6e70f7bc6acddbffd498de92 Mon Sep 17 00:00:00 2001 From: DatLag Date: Fri, 3 May 2024 19:50:47 +0200 Subject: [PATCH] change medium state machine to repository --- .../dev/datlag/aniflow/anilist/Cache.kt | 48 -------- .../aniflow/anilist/CharacterRepository.kt | 6 +- .../aniflow/anilist/MediumRepository.kt | 79 ++++++++++++ .../aniflow/anilist/MediumStateMachine.kt | 115 ------------------ .../datlag/aniflow/module/NetworkModule.kt | 6 + .../screen/medium/MediumComponent.kt | 4 +- .../navigation/screen/medium/MediumScreen.kt | 2 +- .../screen/medium/MediumScreenComponent.kt | 65 +++++----- .../medium/component/CollapsingToolbar.kt | 8 +- 9 files changed, 121 insertions(+), 212 deletions(-) delete mode 100644 anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/Cache.kt create mode 100644 anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/MediumRepository.kt delete mode 100644 anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/MediumStateMachine.kt diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/Cache.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/Cache.kt deleted file mode 100644 index 79f2850..0000000 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/Cache.kt +++ /dev/null @@ -1,48 +0,0 @@ -package dev.datlag.aniflow.anilist - -import com.mayakapps.kache.InMemoryKache -import com.mayakapps.kache.KacheStrategy -import dev.datlag.aniflow.anilist.model.Character -import dev.datlag.aniflow.anilist.model.Medium -import dev.datlag.tooling.async.suspendCatching -import kotlin.time.Duration.Companion.hours - -internal object Cache { - private val medium = InMemoryKache( - maxSize = 10L * 1024 * 1024 - ) { - strategy = KacheStrategy.LRU - expireAfterWriteDuration = 2.hours - } - - private val character = InMemoryKache( - maxSize = 5L * 1024 * 1024 - ) { - strategy = KacheStrategy.LRU - expireAfterWriteDuration = 2.hours - } - - suspend fun getMedium(key: MediumQuery): Medium? { - return suspendCatching { - medium.getIfAvailable(key) - }.getOrNull() - } - - suspend fun setMedium(key: MediumQuery, data: Medium): Medium { - return suspendCatching { - medium.put(key, data) - }.getOrNull() ?: data - } - - suspend fun getCharacter(key: CharacterQuery) : Character? { - return suspendCatching { - character.getIfAvailable(key) - }.getOrNull() - } - - suspend fun setCharacter(key: CharacterQuery, data: Character): Character { - return suspendCatching { - character.put(key, data) - }.getOrNull() ?: data - } -} \ No newline at end of file diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/CharacterRepository.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/CharacterRepository.kt index 3f29f7b..930c190 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/CharacterRepository.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/CharacterRepository.kt @@ -69,11 +69,7 @@ class CharacterRepository( companion object { fun fromGraphQL(query: CharacterQuery.Data?): State { - val char = query?.Character?.let { Character(it) } - - if (char == null) { - return Error - } + val char = query?.Character?.let { Character(it) } ?: return Error return Success(char) } diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/MediumRepository.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/MediumRepository.kt new file mode 100644 index 0000000..1141f2c --- /dev/null +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/MediumRepository.kt @@ -0,0 +1,79 @@ +package dev.datlag.aniflow.anilist + +import com.apollographql.apollo3.ApolloClient +import com.apollographql.apollo3.api.Optional +import dev.datlag.aniflow.anilist.model.Medium +import kotlinx.coroutines.flow.* + +class MediumRepository( + private val client: ApolloClient, + private val fallbackClient: ApolloClient +) { + + private val id = MutableStateFlow(null) + private val query = id.filterNotNull().map { + Query(it) + } + private val fallbackQuery = query.transform { + return@transform emitAll(fallbackClient.query(it.toGraphQL()).toFlow()) + }.mapNotNull { + val data = it.data + if (data == null) { + if (it.hasErrors()) { + State.fromGraphQL(data) + } else { + null + } + } else { + State.fromGraphQL(data) + } + } + + val medium = query.transform { + return@transform emitAll(client.query(it.toGraphQL()).toFlow()) + }.mapNotNull { + val data = it.data + if (data == null) { + if (it.hasErrors()) { + State.fromGraphQL(data) + } else { + null + } + } else { + State.fromGraphQL(data) + } + }.transform { + return@transform if (it is State.Error) { + emitAll(fallbackQuery) + } else { + emit(it) + } + } + + fun clear() = id.update { null } + + fun load(id: Int) = this.id.update { id } + + private data class Query( + val id: Int, + ) { + fun toGraphQL() = MediumQuery( + id = Optional.present(id), + statusVersion = Optional.present(2), + html = Optional.present(true) + ) + } + + sealed interface State { + data class Success(val medium: Medium) : State + data object Error : State + + companion object { + fun fromGraphQL(query: MediumQuery.Data?): State { + val medium = query?.Media?.let(::Medium) ?: return Error + + return Success(medium) + } + } + } +} \ No newline at end of file diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/MediumStateMachine.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/MediumStateMachine.kt deleted file mode 100644 index 0fa3981..0000000 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/MediumStateMachine.kt +++ /dev/null @@ -1,115 +0,0 @@ -package dev.datlag.aniflow.anilist - -import com.apollographql.apollo3.ApolloClient -import com.apollographql.apollo3.api.Optional -import com.freeletics.flowredux.dsl.FlowReduxStateMachine -import dev.datlag.aniflow.anilist.model.Medium -import dev.datlag.aniflow.firebase.FirebaseFactory -import dev.datlag.aniflow.model.CatchResult -import dev.datlag.aniflow.model.mapError -import dev.datlag.aniflow.model.safeFirstOrNull -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlin.time.Duration.Companion.seconds - -@OptIn(ExperimentalCoroutinesApi::class) -class MediumStateMachine( - private val client: ApolloClient, - private val fallbackClient: ApolloClient, - private val crashlytics: FirebaseFactory.Crashlytics?, - private val id: Int, -) : FlowReduxStateMachine( - initialState = State.Loading(id) -) { - - var currentState: State = State.Loading(id) - private set - - init { - spec { - inState { - onEnterEffect { - currentState = it - } - onEnter { state -> - val response = CatchResult.repeat(times = 2, timeoutDuration = 30.seconds) { - val query = client.query(state.snapshot.query) - - query.execute().data ?: query.toFlow().safeFirstOrNull()?.dataOrThrow() - }.mapError { - val query = fallbackClient.query(state.snapshot.query) - - query.execute().data ?: query.toFlow().safeFirstOrNull()?.data - }.mapSuccess { - it.Media?.let { data -> - State.Success(state.snapshot.query, Medium(data)) - } - } - - val cached = Cache.getMedium(state.snapshot.query) - - state.override { - response.asSuccess { - crashlytics?.log(it) - - if (cached != null) { - State.Success(query, cached) - } else { - State.Error(query) - } - } - } - } - } - inState { - onEnterEffect { - currentState = it - Cache.setMedium(it.query, it.data) - } - } - inState { - onEnterEffect { - currentState = it - } - on { _, state -> - state.override { - State.Loading(state.snapshot.query) - } - } - } - } - } - - sealed interface State { - - val isLoading: Boolean - get() = this is Loading - - val isSuccess: Boolean - get() = this is Success - - data class Loading( - internal val query: MediumQuery - ) : State { - constructor(id: Int) : this( - MediumQuery( - id = Optional.present(id), - statusVersion = Optional.present(2), - html = Optional.present(true) - ) - ) - } - - data class Success( - internal val query: MediumQuery, - val data: Medium - ) : State - - data class Error( - internal val query: MediumQuery, - ) : State - } - - sealed interface Action { - data object Retry : Action - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt index 0c048ff..11d9249 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt @@ -161,5 +161,11 @@ data object NetworkModule { fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT), ) } + bindSingleton { + MediumRepository( + client = instance(Constants.AniList.APOLLO_CLIENT), + fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT) + ) + } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumComponent.kt index 3040140..4317ca7 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumComponent.kt @@ -2,7 +2,7 @@ package dev.datlag.aniflow.ui.navigation.screen.medium import com.arkivanov.decompose.router.slot.ChildSlot import com.arkivanov.decompose.value.Value -import dev.datlag.aniflow.anilist.MediumStateMachine +import dev.datlag.aniflow.anilist.MediumRepository import dev.datlag.aniflow.anilist.model.Character import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.type.MediaFormat @@ -21,7 +21,7 @@ interface MediumComponent : ContentHolderComponent { val titleLanguage: Flow val charLanguage: Flow - val mediumState: StateFlow + val mediumState: Flow val isAdult: Flow val isAdultAllowed: Flow diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt index 8eed995..1c486d5 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt @@ -70,7 +70,7 @@ fun MediumScreen(component: MediumComponent) { scrollBehavior = scrollState, initialMedium = component.initialMedium, titleLanguageFlow = component.titleLanguage, - mediumStateFlow = component.mediumState, + mediumFlow = component.mediumState, bannerImageFlow = component.bannerImage, coverImage = coverImage, titleFlow = component.title, diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt index 8ab2a79..8fb315f 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreenComponent.kt @@ -50,100 +50,87 @@ class MediumScreenComponent( ) : MediumComponent, ComponentContext by componentContext { private val aniListClient by di.instance(Constants.AniList.APOLLO_CLIENT) - private val aniListFallbackClient by di.instance(Constants.AniList.FALLBACK_APOLLO_CLIENT) private val appSettings by di.instance() private val userHelper by di.instance() override val titleLanguage: Flow = appSettings.titleLanguage.flowOn(ioDispatcher()) override val charLanguage: Flow = appSettings.charLanguage.flowOn(ioDispatcher()) - private val mediumStateMachine = MediumStateMachine( - client = aniListClient, - fallbackClient = aniListFallbackClient, - crashlytics = di.nullableFirebaseInstance()?.crashlytics, - id = initialMedium.id - ) - - override val mediumState = mediumStateMachine.state.flowOn( - context = ioDispatcher() - ).stateIn( - scope = ioScope(), - started = SharingStarted.WhileSubscribed(), - initialValue = mediumStateMachine.currentState - ) + private val mediumRepository by di.instance() + override val mediumState = mediumRepository.medium private val mediumSuccessState = mediumState.mapNotNull { - it.safeCast() + it.safeCast() } override val isAdult: Flow = mediumSuccessState.map { - it.data.isAdult + it.medium.isAdult } override val isAdultAllowed: Flow = appSettings.adultContent private val type: Flow = mediumSuccessState.map { - it.data.type + it.medium.type } override val bannerImage: Flow = mediumSuccessState.map { - it.data.bannerImage + it.medium.bannerImage } override val coverImage: Flow = mediumSuccessState.map { - it.data.coverImage + it.medium.coverImage } override val title: Flow = mediumSuccessState.map { - it.data.title + it.medium.title } override val description: Flow = mediumSuccessState.map { - it.data.description?.ifBlank { null } + it.medium.description?.ifBlank { null } } override val translatedDescription: MutableStateFlow = MutableStateFlow(null) override val genres: Flow> = mediumSuccessState.map { - it.data.genres + it.medium.genres }.mapNotEmpty() override val format: Flow = mediumSuccessState.map { - it.data.format + it.medium.format } override val episodes: Flow = mediumSuccessState.map { - it.data.episodes + it.medium.episodes }.distinctUntilChanged() override val duration: Flow = mediumSuccessState.map { - it.data.avgEpisodeDurationInMin + it.medium.avgEpisodeDurationInMin }.distinctUntilChanged() override val status: Flow = mediumSuccessState.map { - it.data.status + it.medium.status }.distinctUntilChanged() override val rated: Flow = mediumSuccessState.mapNotNull { - it.data.rated() + it.medium.rated() }.distinctUntilChanged() override val popular: Flow = mediumSuccessState.mapNotNull { - it.data.popular() + it.medium.popular() }.distinctUntilChanged() override val score: Flow = mediumSuccessState.mapNotNull { - it.data.averageScore.asNullIf(-1) + it.medium.averageScore.asNullIf(-1) }.distinctUntilChanged() override val characters: Flow> = mediumSuccessState.map { - it.data.characters + it.medium.characters }.mapNotEmpty() private val changedRating: MutableStateFlow = MutableStateFlow(initialMedium.entry?.score?.toInt() ?: -1) override val rating: Flow = combine( mediumSuccessState.map { - it.data.entry?.score?.toInt() + it.medium.entry?.score?.toInt() }, changedRating ) { t1, t2 -> @@ -155,23 +142,23 @@ class MediumScreenComponent( } override val trailer: Flow = mediumSuccessState.map { - it.data.trailer + it.medium.trailer } override val alreadyAdded: Flow = mediumSuccessState.map { - it.data.entry != null + it.medium.entry != null } override val isFavorite: Flow = mediumSuccessState.map { - it.data.isFavorite + it.medium.isFavorite } override val isFavoriteBlocked: Flow = mediumSuccessState.map { - it.data.isFavoriteBlocked + it.medium.isFavoriteBlocked } override val siteUrl: Flow = mediumSuccessState.map { - it.data.siteUrl + it.medium.siteUrl } private val burningSeriesResolver by di.instance() @@ -194,6 +181,10 @@ class MediumScreenComponent( } } + init { + mediumRepository.load(initialMedium.id) + } + @Composable override fun render() { val state = HazeState() diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CollapsingToolbar.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CollapsingToolbar.kt index 2efe627..7614082 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CollapsingToolbar.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CollapsingToolbar.kt @@ -29,7 +29,7 @@ import dev.chrisbanes.haze.hazeChild import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.HazeMaterials import dev.datlag.aniflow.LocalHaze -import dev.datlag.aniflow.anilist.MediumStateMachine +import dev.datlag.aniflow.anilist.MediumRepository import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.common.notPreferred import dev.datlag.aniflow.common.preferred @@ -51,7 +51,7 @@ fun CollapsingToolbar( scrollBehavior: TopAppBarScrollBehavior, initialMedium: Medium, titleLanguageFlow: Flow, - mediumStateFlow: StateFlow, + mediumFlow: Flow, bannerImageFlow: Flow, coverImage: Medium.CoverImage, titleFlow: Flow, @@ -168,12 +168,12 @@ fun CollapsingToolbar( horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, ) { - val mediumState by mediumStateFlow.collectAsStateWithLifecycle() + val mediumState by mediumFlow.collectAsStateWithLifecycle(null) val siteUrl by siteUrlFlow.collectAsStateWithLifecycle(initialMedium.siteUrl) val shareHandler = shareHandler() AnimatedVisibility( - visible = mediumState.isSuccess, + visible = mediumState is MediumRepository.State.Success, enter = fadeIn(), exit = fadeOut() ) {