From 5ba299a520e90b0d6756bace644ec1211e4bef14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20Grebe-L=C3=BCth?= Date: Fri, 22 Aug 2025 17:03:18 +0200 Subject: [PATCH 1/2] refactor: removed superfluous class WorkResult.kt and use Result.kt instead. Renamed all "Get...UseCase" to "Fetch....UseCase". Moved ui state loading logic into ViewModels. Updated unit tests. --- .../entikore/composedex/domain/WorkResult.kt | 60 --------- ...esUseCase.kt => FetchFavouritesUseCase.kt} | 22 ++-- ...onUseCase.kt => FetchGenerationUseCase.kt} | 22 ++-- ...sUseCase.kt => FetchGenerationsUseCase.kt} | 21 ++-- ....kt => FetchPokemonOfGenerationUseCase.kt} | 23 ++-- ...seCase.kt => FetchPokemonOfTypeUseCase.kt} | 22 ++-- ...kemonUseCase.kt => FetchPokemonUseCase.kt} | 39 +++--- ...{GetTypeUseCase.kt => FetchTypeUseCase.kt} | 22 ++-- ...etTypesUseCase.kt => FetchTypesUseCase.kt} | 21 ++-- .../domain/usecase/base/BaseFetchUseCase.kt | 51 ++++++++ .../domain/usecase/base/ParamsUseCase.kt | 26 ---- .../composedex/domain/util/Constants.kt | 2 +- .../composedex/domain/util/FlowExtension.kt | 38 ++++++ .../ui/screen/favourite/FavouriteViewModel.kt | 30 +++-- .../screen/generation/GenerationViewModel.kt | 98 ++++++++------- .../ui/screen/pokemon/PokemonViewModel.kt | 90 +++++++------- .../ui/screen/setting/SettingsViewModel.kt | 4 +- .../ui/screen/shared/PokemonUiState.kt | 4 +- .../ui/screen/type/TypeViewModel.kt | 117 ++++++++++-------- ...eTest.kt => FetchGenerationUseCaseTest.kt} | 43 +++---- ...Test.kt => FetchGenerationsUseCaseTest.kt} | 39 +++--- ...=> FetchPokemonOfGenerationUseCaseTest.kt} | 33 +++-- ...st.kt => FetchPokemonOfTypeUseCaseTest.kt} | 47 +++---- ...CaseTest.kt => FetchPokemonUseCaseTest.kt} | 39 +++--- ...UseCaseTest.kt => FetchTypeUseCaseTest.kt} | 39 +++--- ...seCaseTest.kt => FetchTypesUseCaseTest.kt} | 39 +++--- .../konsist/architecture/ArchitectureCheck.kt | 15 --- .../favourite/FavouriteViewModelTest.kt | 98 +++++++-------- .../generation/GenerationViewModelTest.kt | 50 ++++---- .../ui/screen/pokemon/PokemonViewModelTest.kt | 20 ++- .../ui/screen/type/TypeViewModelTest.kt | 74 +++++------ 31 files changed, 602 insertions(+), 646 deletions(-) delete mode 100644 app/src/main/kotlin/de/entikore/composedex/domain/WorkResult.kt rename app/src/main/kotlin/de/entikore/composedex/domain/usecase/{GetFavouritesUseCase.kt => FetchFavouritesUseCase.kt} (60%) rename app/src/main/kotlin/de/entikore/composedex/domain/usecase/{GetGenerationUseCase.kt => FetchGenerationUseCase.kt} (69%) rename app/src/main/kotlin/de/entikore/composedex/domain/usecase/{GetGenerationsUseCase.kt => FetchGenerationsUseCase.kt} (61%) rename app/src/main/kotlin/de/entikore/composedex/domain/usecase/{GetPokemonOfGenerationUseCase.kt => FetchPokemonOfGenerationUseCase.kt} (69%) rename app/src/main/kotlin/de/entikore/composedex/domain/usecase/{GetPokemonOfTypeUseCase.kt => FetchPokemonOfTypeUseCase.kt} (67%) rename app/src/main/kotlin/de/entikore/composedex/domain/usecase/{GetPokemonUseCase.kt => FetchPokemonUseCase.kt} (67%) rename app/src/main/kotlin/de/entikore/composedex/domain/usecase/{GetTypeUseCase.kt => FetchTypeUseCase.kt} (66%) rename app/src/main/kotlin/de/entikore/composedex/domain/usecase/{GetTypesUseCase.kt => FetchTypesUseCase.kt} (68%) create mode 100644 app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/BaseFetchUseCase.kt delete mode 100644 app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/ParamsUseCase.kt create mode 100644 app/src/main/kotlin/de/entikore/composedex/domain/util/FlowExtension.kt rename app/src/test/kotlin/de/entikore/composedex/domain/usecase/{GetGenerationUseCaseTest.kt => FetchGenerationUseCaseTest.kt} (74%) rename app/src/test/kotlin/de/entikore/composedex/domain/usecase/{GetGenerationsUseCaseTest.kt => FetchGenerationsUseCaseTest.kt} (63%) rename app/src/test/kotlin/de/entikore/composedex/domain/usecase/{GetPokemonOfGenerationUseCaseTest.kt => FetchPokemonOfGenerationUseCaseTest.kt} (79%) rename app/src/test/kotlin/de/entikore/composedex/domain/usecase/{GetPokemonOfTypeUseCaseTest.kt => FetchPokemonOfTypeUseCaseTest.kt} (70%) rename app/src/test/kotlin/de/entikore/composedex/domain/usecase/{GetPokemonUseCaseTest.kt => FetchPokemonUseCaseTest.kt} (78%) rename app/src/test/kotlin/de/entikore/composedex/domain/usecase/{GetTypeUseCaseTest.kt => FetchTypeUseCaseTest.kt} (67%) rename app/src/test/kotlin/de/entikore/composedex/domain/usecase/{GetTypesUseCaseTest.kt => FetchTypesUseCaseTest.kt} (63%) diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/WorkResult.kt b/app/src/main/kotlin/de/entikore/composedex/domain/WorkResult.kt deleted file mode 100644 index ef87c58..0000000 --- a/app/src/main/kotlin/de/entikore/composedex/domain/WorkResult.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2024 Entikore - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package de.entikore.composedex.domain - -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.retryWhen -import java.io.IOException - -private const val RETRY_TIME_IN_MILLIS = 3_000L - -/** - * Represents the result of an operation, typically asynchronous work. - * - * This sealed interface encapsulates the three possible states of an operation: - * - [Success]: The operation completed successfully and returned data. - * - [Error]: The operation failed, possibly with an exception. - * - [Loading]: The operation is still in progress. - */ -sealed interface WorkResult { - data class Success(val data: T) : WorkResult - data class Error(val exception: Throwable? = null) : WorkResult - data object Loading : WorkResult -} - -fun Flow.asWorkResult(): Flow> { - return this - .map> { - WorkResult.Success(it) - } - .onStart { emit(WorkResult.Loading) } - .retryWhen { cause, _ -> - if (cause is IOException) { - emit(WorkResult.Error(cause)) - delay(RETRY_TIME_IN_MILLIS) - true - } else { - false - } - } - .catch { - emit(WorkResult.Error(it)) - } -} diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetFavouritesUseCase.kt b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchFavouritesUseCase.kt similarity index 60% rename from app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetFavouritesUseCase.kt rename to app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchFavouritesUseCase.kt index a49b34e..ef7af51 100644 --- a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetFavouritesUseCase.kt +++ b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchFavouritesUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,21 +15,23 @@ */ package de.entikore.composedex.domain.usecase -import de.entikore.composedex.domain.WorkResult -import de.entikore.composedex.domain.asWorkResult import de.entikore.composedex.domain.model.pokemon.Pokemon import de.entikore.composedex.domain.repository.FavouriteRepository -import de.entikore.composedex.domain.usecase.base.UseCase -import kotlinx.coroutines.flow.Flow +import de.entikore.composedex.domain.usecase.base.BaseFetchUseCase +import de.entikore.composedex.domain.util.asResult +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.distinctUntilChanged import javax.inject.Inject /** * This use case returns the latest as favourites marked Pokemon. */ -class GetFavouritesUseCase @Inject constructor(private val repository: FavouriteRepository) : - UseCase>>>() { - - override operator fun invoke() = repository.getFavourites().distinctUntilChanged() - .asWorkResult() +@Suppress("TooGenericExceptionCaught") +class FetchFavouritesUseCase @Inject constructor( + private val repository: FavouriteRepository, + dispatcher: CoroutineDispatcher = Dispatchers.IO +) : + BaseFetchUseCase>(dispatcher) { + override fun execute(params: Unit) = repository.getFavourites().distinctUntilChanged().asResult() } diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetGenerationUseCase.kt b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchGenerationUseCase.kt similarity index 69% rename from app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetGenerationUseCase.kt rename to app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchGenerationUseCase.kt index 8bd0a11..ec42bc7 100644 --- a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetGenerationUseCase.kt +++ b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchGenerationUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,11 +15,12 @@ */ package de.entikore.composedex.domain.usecase -import de.entikore.composedex.domain.WorkResult -import de.entikore.composedex.domain.asWorkResult import de.entikore.composedex.domain.model.generation.Generation import de.entikore.composedex.domain.repository.GenerationRepository -import de.entikore.composedex.domain.usecase.base.ParamsUseCase +import de.entikore.composedex.domain.usecase.base.BaseFetchUseCase +import de.entikore.composedex.domain.util.asResult +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import javax.inject.Inject @@ -27,16 +28,19 @@ import javax.inject.Inject /** * This use case returns the latest [Generation] of the provided name or id. */ -class GetGenerationUseCase @Inject constructor(private val repository: GenerationRepository) : - ParamsUseCase>>() { +class FetchGenerationUseCase @Inject constructor( + private val repository: GenerationRepository, + dispatcher: CoroutineDispatcher = Dispatchers.IO +) : + BaseFetchUseCase(dispatcher) { - override operator fun invoke(params: String): Flow> { + override fun execute(params: String): Flow> { val id = params.trim().toIntOrNull() return if (id != null) { - repository.getGenerationById(id).distinctUntilChanged().asWorkResult() + repository.getGenerationById(id).distinctUntilChanged().asResult() } else { val normalizedParams = params.lowercase().trim() - repository.getGenerationByName(normalizedParams).distinctUntilChanged().asWorkResult() + repository.getGenerationByName(normalizedParams).distinctUntilChanged().asResult() } } } diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetGenerationsUseCase.kt b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchGenerationsUseCase.kt similarity index 61% rename from app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetGenerationsUseCase.kt rename to app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchGenerationsUseCase.kt index ae5b02e..a56115f 100644 --- a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetGenerationsUseCase.kt +++ b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchGenerationsUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,20 +15,23 @@ */ package de.entikore.composedex.domain.usecase -import de.entikore.composedex.domain.WorkResult -import de.entikore.composedex.domain.asWorkResult import de.entikore.composedex.domain.model.generation.Generation import de.entikore.composedex.domain.repository.GenerationRepository -import de.entikore.composedex.domain.usecase.base.UseCase -import kotlinx.coroutines.flow.Flow +import de.entikore.composedex.domain.usecase.base.BaseFetchUseCase +import de.entikore.composedex.domain.util.asResult +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.distinctUntilChanged import javax.inject.Inject /** * This use case returns the latest list of all [Generation]. */ -class GetGenerationsUseCase @Inject constructor(private val repository: GenerationRepository) : - UseCase>>>() { - override operator fun invoke() = - repository.getGenerations().distinctUntilChanged().asWorkResult() +class FetchGenerationsUseCase @Inject constructor( + private val repository: GenerationRepository, + dispatcher: CoroutineDispatcher = Dispatchers.IO +) : + BaseFetchUseCase>(dispatcher) { + override fun execute(params: Unit) = + repository.getGenerations().distinctUntilChanged().asResult() } diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetPokemonOfGenerationUseCase.kt b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchPokemonOfGenerationUseCase.kt similarity index 69% rename from app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetPokemonOfGenerationUseCase.kt rename to app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchPokemonOfGenerationUseCase.kt index 71b3f2c..fc80b41 100644 --- a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetPokemonOfGenerationUseCase.kt +++ b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchPokemonOfGenerationUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,12 +15,13 @@ */ package de.entikore.composedex.domain.usecase -import de.entikore.composedex.domain.WorkResult -import de.entikore.composedex.domain.asWorkResult import de.entikore.composedex.domain.model.generation.Generation import de.entikore.composedex.domain.model.pokemon.Pokemon import de.entikore.composedex.domain.repository.GenerationRepository -import de.entikore.composedex.domain.usecase.base.ParamsUseCase +import de.entikore.composedex.domain.usecase.base.BaseFetchUseCase +import de.entikore.composedex.domain.util.asResult +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import javax.inject.Inject @@ -29,17 +30,17 @@ import javax.inject.Inject * This use case returns the latest list of [Pokemon] belonging to the [Generation] * of the provided name or id. */ -class GetPokemonOfGenerationUseCase @Inject constructor( - private val repository: GenerationRepository -) : ParamsUseCase>>>() { - override operator fun invoke(params: String): Flow>> { +class FetchPokemonOfGenerationUseCase @Inject constructor( + private val repository: GenerationRepository, + dispatcher: CoroutineDispatcher = Dispatchers.IO +) : BaseFetchUseCase>(dispatcher) { + override fun execute(params: String): Flow>> { val id = params.trim().toIntOrNull() return if (id != null) { - repository.getPokemonOfGenerationById(id).distinctUntilChanged().asWorkResult() + repository.getPokemonOfGenerationById(id).distinctUntilChanged().asResult() } else { val normalizedParams = params.lowercase().trim() - repository.getPokemonOfGenerationByName(normalizedParams).distinctUntilChanged() - .asWorkResult() + repository.getPokemonOfGenerationByName(normalizedParams).distinctUntilChanged().asResult() } } } diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetPokemonOfTypeUseCase.kt b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchPokemonOfTypeUseCase.kt similarity index 67% rename from app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetPokemonOfTypeUseCase.kt rename to app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchPokemonOfTypeUseCase.kt index 73b102c..0780540 100644 --- a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetPokemonOfTypeUseCase.kt +++ b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchPokemonOfTypeUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,13 +15,13 @@ */ package de.entikore.composedex.domain.usecase -import de.entikore.composedex.domain.WorkResult -import de.entikore.composedex.domain.asWorkResult import de.entikore.composedex.domain.model.pokemon.Pokemon import de.entikore.composedex.domain.model.type.Type import de.entikore.composedex.domain.repository.TypeRepository -import de.entikore.composedex.domain.usecase.base.ParamsUseCase -import kotlinx.coroutines.flow.Flow +import de.entikore.composedex.domain.usecase.base.BaseFetchUseCase +import de.entikore.composedex.domain.util.asResult +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.distinctUntilChanged import javax.inject.Inject @@ -29,9 +29,11 @@ import javax.inject.Inject * This use case returns the latest list of [Pokemon] belonging to the [Type] * of the provided name. */ -class GetPokemonOfTypeUseCase @Inject constructor(private val repository: TypeRepository) : - ParamsUseCase>>>() { - - override operator fun invoke(params: String): Flow>> = - repository.getPokemonOfType(params).distinctUntilChanged().asWorkResult() +class FetchPokemonOfTypeUseCase @Inject constructor( + private val repository: TypeRepository, + dispatcher: CoroutineDispatcher = Dispatchers.IO +) : + BaseFetchUseCase>(dispatcher) { + override fun execute(params: String) = + repository.getPokemonOfType(params).distinctUntilChanged().asResult() } diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetPokemonUseCase.kt b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchPokemonUseCase.kt similarity index 67% rename from app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetPokemonUseCase.kt rename to app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchPokemonUseCase.kt index 7e40f53..29d3ad2 100644 --- a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetPokemonUseCase.kt +++ b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchPokemonUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,13 +15,14 @@ */ package de.entikore.composedex.domain.usecase -import de.entikore.composedex.domain.WorkResult -import de.entikore.composedex.domain.asWorkResult import de.entikore.composedex.domain.model.pokemon.Pokemon import de.entikore.composedex.domain.model.pokemon.wasPokemonUpdated import de.entikore.composedex.domain.repository.PokemonRepository -import de.entikore.composedex.domain.usecase.base.ParamsUseCase +import de.entikore.composedex.domain.usecase.base.BaseFetchUseCase +import de.entikore.composedex.domain.util.asResult import de.entikore.composedex.domain.util.whitespacePattern +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -32,36 +33,30 @@ import javax.inject.Inject * [Pokemon] was retrieved successfully the flavor text entries are formatted and duplicates are * filtered. */ -class GetPokemonUseCase @Inject constructor(private val repository: PokemonRepository) : - ParamsUseCase>>() { - - override operator fun invoke(params: String): Flow> { +class FetchPokemonUseCase @Inject constructor( + private val repository: PokemonRepository, + dispatcher: CoroutineDispatcher = Dispatchers.IO +) : + BaseFetchUseCase(dispatcher) { + override fun execute(params: String): Flow> { val id = params.trim().toIntOrNull() return if (id != null) { - repository.getPokemonById(id).distinctUntilChanged(Pokemon::wasPokemonUpdated).asWorkResult().map { + repository.getPokemonById(id).distinctUntilChanged(Pokemon::wasPokemonUpdated).map { processSuccessResult(it) - } + }.asResult() } else { val normalizedParams = params.lowercase().trim() repository.getPokemonByName( normalizedParams - ).distinctUntilChanged(Pokemon::wasPokemonUpdated).asWorkResult() + ).distinctUntilChanged(Pokemon::wasPokemonUpdated) .map { processSuccessResult(it) - } + }.asResult() } } - private fun processSuccessResult(result: WorkResult): WorkResult { - return when (result) { - is WorkResult.Success -> { - val pokemon = - result.data.copy(textEntries = processFlavorTextEntries(result.data.textEntries)) - WorkResult.Success(pokemon) - } - - else -> result - } + private fun processSuccessResult(result: Pokemon): Pokemon { + return result.copy(textEntries = processFlavorTextEntries(result.textEntries)) } /** diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetTypeUseCase.kt b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchTypeUseCase.kt similarity index 66% rename from app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetTypeUseCase.kt rename to app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchTypeUseCase.kt index f62a3f5..84d675e 100644 --- a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetTypeUseCase.kt +++ b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchTypeUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,20 +15,24 @@ */ package de.entikore.composedex.domain.usecase -import de.entikore.composedex.domain.WorkResult -import de.entikore.composedex.domain.asWorkResult import de.entikore.composedex.domain.model.type.Type import de.entikore.composedex.domain.repository.TypeRepository -import de.entikore.composedex.domain.usecase.base.ParamsUseCase -import kotlinx.coroutines.flow.Flow +import de.entikore.composedex.domain.usecase.base.BaseFetchUseCase +import de.entikore.composedex.domain.util.asResult +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.distinctUntilChanged import javax.inject.Inject /** * This use case returns the latest [Type] of the provided name. */ -class GetTypeUseCase @Inject constructor(private val repository: TypeRepository) : - ParamsUseCase>>() { - override operator fun invoke(params: String): Flow> = - repository.getTypeByName(params).distinctUntilChanged().asWorkResult() +class FetchTypeUseCase @Inject constructor( + private val repository: TypeRepository, + dispatcher: CoroutineDispatcher = Dispatchers.IO +) : + BaseFetchUseCase(dispatcher) { + + override fun execute(params: String) = + repository.getTypeByName(params).distinctUntilChanged().asResult() } diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetTypesUseCase.kt b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchTypesUseCase.kt similarity index 68% rename from app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetTypesUseCase.kt rename to app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchTypesUseCase.kt index 0df92ec..572ab8c 100644 --- a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetTypesUseCase.kt +++ b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/FetchTypesUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,12 +15,12 @@ */ package de.entikore.composedex.domain.usecase -import de.entikore.composedex.domain.WorkResult -import de.entikore.composedex.domain.asWorkResult import de.entikore.composedex.domain.model.type.Type import de.entikore.composedex.domain.repository.TypeRepository -import de.entikore.composedex.domain.usecase.base.UseCase -import kotlinx.coroutines.flow.Flow +import de.entikore.composedex.domain.usecase.base.BaseFetchUseCase +import de.entikore.composedex.domain.util.asResult +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import javax.inject.Inject @@ -28,10 +28,13 @@ import javax.inject.Inject /** * This use case returns the latest list of all [Type]. */ -class GetTypesUseCase @Inject constructor(private val repository: TypeRepository) : - UseCase>>>() { - override operator fun invoke(): Flow>> = +class FetchTypesUseCase @Inject constructor( + private val repository: TypeRepository, + dispatcher: CoroutineDispatcher = Dispatchers.IO +) : + BaseFetchUseCase>(dispatcher) { + override fun execute(params: Unit) = repository.getTypes().distinctUntilChanged().map { it.filter { processedType -> !Type.isUnsupportedType(processedType.name) } - }.asWorkResult() + }.asResult() } diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/BaseFetchUseCase.kt b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/BaseFetchUseCase.kt new file mode 100644 index 0000000..4261ac7 --- /dev/null +++ b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/BaseFetchUseCase.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2025 Entikore + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.entikore.composedex.domain.usecase.base + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn + +/** + * Base class for Use Cases that fetch domain data. + * It standardizes use case execution by automatically: + * - Running the core logic on a specified [CoroutineDispatcher]. + * - Providing a top-level error catch mechanism that emits [Result]. + * + * This class is designed to be implemented by concrete use cases. + */ +abstract class BaseFetchUseCase( + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + + protected abstract fun execute(params: P): Flow> + + operator fun invoke(params: P): Flow> { + return flow { + emitAll(execute(params)) + }.flowOn( + dispatcher + ) + } + + operator fun invoke(): Flow> { + @Suppress("UNCHECKED_CAST") + return invoke(Unit as P) + } +} diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/ParamsUseCase.kt b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/ParamsUseCase.kt deleted file mode 100644 index cd0fc9d..0000000 --- a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/ParamsUseCase.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2024 Entikore - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package de.entikore.composedex.domain.usecase.base - -/** - * Represents a generic use case that accepts parameters. - * - * @param P The type of parameters accepted by the use case. - * @param T The type of result returned by the use case. - */ -abstract class ParamsUseCase { - abstract operator fun invoke(params: P): T -} diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/util/Constants.kt b/app/src/main/kotlin/de/entikore/composedex/domain/util/Constants.kt index d435361..635d897 100644 --- a/app/src/main/kotlin/de/entikore/composedex/domain/util/Constants.kt +++ b/app/src/main/kotlin/de/entikore/composedex/domain/util/Constants.kt @@ -17,5 +17,5 @@ package de.entikore.composedex.domain.util const val IDLE_CONNECTION_COUNT = 2 const val KEEP_ALIVE_DURATION = 5L - +const val RETRY_TIME_IN_MILLIS = 3_000L val whitespacePattern = "\\s+".toRegex() diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/util/FlowExtension.kt b/app/src/main/kotlin/de/entikore/composedex/domain/util/FlowExtension.kt new file mode 100644 index 0000000..d9f40e0 --- /dev/null +++ b/app/src/main/kotlin/de/entikore/composedex/domain/util/FlowExtension.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2025 Entikore + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.entikore.composedex.domain.util + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.retryWhen +import java.io.IOException + +fun Flow.asResult() = + this + .map { Result.success(it) } + .retryWhen { cause, _ -> + if (cause is IOException) { + emit(Result.failure(cause)) + delay(RETRY_TIME_IN_MILLIS) + true + } else { + false + } + }.catch { e -> + emit(Result.failure(e)) + } diff --git a/app/src/main/kotlin/de/entikore/composedex/ui/screen/favourite/FavouriteViewModel.kt b/app/src/main/kotlin/de/entikore/composedex/ui/screen/favourite/FavouriteViewModel.kt index ef6a71a..0b96216 100644 --- a/app/src/main/kotlin/de/entikore/composedex/ui/screen/favourite/FavouriteViewModel.kt +++ b/app/src/main/kotlin/de/entikore/composedex/ui/screen/favourite/FavouriteViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,14 +17,14 @@ package de.entikore.composedex.ui.screen.favourite import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import de.entikore.composedex.domain.WorkResult import de.entikore.composedex.domain.model.pokemon.Pokemon -import de.entikore.composedex.domain.usecase.GetFavouritesUseCase +import de.entikore.composedex.domain.usecase.FetchFavouritesUseCase import de.entikore.composedex.domain.usecase.SetFavouriteData import de.entikore.composedex.domain.usecase.base.ParamsSuspendUseCase import de.entikore.composedex.ui.screen.shared.PokemonFilterOptions import de.entikore.composedex.ui.screen.shared.PokemonFilterViewModel import de.entikore.composedex.ui.screen.shared.PokemonUiState +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn @@ -37,27 +37,31 @@ import javax.inject.Inject */ @HiltViewModel class FavouriteViewModel @Inject constructor( - getFavourites: GetFavouritesUseCase, + getFavourites: FetchFavouritesUseCase, private val setAsFavouriteUseCase: @JvmSuppressWildcards ParamsSuspendUseCase ) : PokemonFilterViewModel() { + private val _isUpdatingFavourite = MutableStateFlow(false) + val screenState = - getFavourites().combine( - filterOptions - ) { favourites: WorkResult>, filterSettings: PokemonFilterOptions -> - when (favourites) { - is WorkResult.Error -> PokemonUiState.Error - WorkResult.Loading -> PokemonUiState.Loading - is WorkResult.Success -> PokemonUiState.Success( + combine( + getFavourites(), + filterOptions, + _isUpdatingFavourite + ) { favourites: Result>, filterSettings: PokemonFilterOptions, isUpdating: Boolean -> + if (favourites.isSuccess) { + PokemonUiState.Success( filterSettings.getFilteredList( - favourites.data + favourites.getOrDefault(emptyList()) ) ) + } else { + PokemonUiState.Error } }.stateIn( viewModelScope, SharingStarted.Eagerly, - PokemonUiState.Success(emptyList()) + PokemonUiState.Loading ) fun updateFavourite(id: Int, isFavourite: Boolean) { diff --git a/app/src/main/kotlin/de/entikore/composedex/ui/screen/generation/GenerationViewModel.kt b/app/src/main/kotlin/de/entikore/composedex/ui/screen/generation/GenerationViewModel.kt index ea99fb6..8b62e2f 100644 --- a/app/src/main/kotlin/de/entikore/composedex/ui/screen/generation/GenerationViewModel.kt +++ b/app/src/main/kotlin/de/entikore/composedex/ui/screen/generation/GenerationViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +17,11 @@ package de.entikore.composedex.ui.screen.generation import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import de.entikore.composedex.domain.WorkResult import de.entikore.composedex.domain.model.generation.Generation import de.entikore.composedex.domain.model.pokemon.Pokemon -import de.entikore.composedex.domain.usecase.GetGenerationUseCase -import de.entikore.composedex.domain.usecase.GetGenerationsUseCase -import de.entikore.composedex.domain.usecase.GetPokemonOfGenerationUseCase +import de.entikore.composedex.domain.usecase.FetchGenerationUseCase +import de.entikore.composedex.domain.usecase.FetchGenerationsUseCase +import de.entikore.composedex.domain.usecase.FetchPokemonOfGenerationUseCase import de.entikore.composedex.domain.usecase.SaveImageData import de.entikore.composedex.domain.usecase.SetFavouriteData import de.entikore.composedex.domain.usecase.base.ParamsSuspendUseCase @@ -48,9 +47,9 @@ import javax.inject.Inject */ @HiltViewModel class GenerationViewModel @Inject constructor( - getGenerationsUseCase: GetGenerationsUseCase, - getGenerationUseCase: GetGenerationUseCase, - getPokemonOfGenerationUseCase: GetPokemonOfGenerationUseCase, + getGenerationsUseCase: FetchGenerationsUseCase, + getGenerationUseCase: FetchGenerationUseCase, + getPokemonOfGenerationUseCase: FetchPokemonOfGenerationUseCase, private val saveRemoteImageUseCase: @JvmSuppressWildcards ParamsSuspendUseCase, private val setAsFavouriteUseCase: @JvmSuppressWildcards ParamsSuspendUseCase ) : PokemonFilterViewModel() { @@ -77,8 +76,8 @@ class GenerationViewModel @Inject constructor( ) }.stateIn( viewModelScope, - SharingStarted.WhileSubscribed(5_000), - GenerationScreenUiState.Success() + SharingStarted.WhileSubscribed(5_000L), + GenerationScreenUiState.Loading ) fun searchForGeneration(generationId: String) { @@ -92,61 +91,72 @@ class GenerationViewModel @Inject constructor( } private fun buildSelectedGenerationUiState( - generation: WorkResult, - pokemon: WorkResult> + generation: Result, + pokemon: Result> ): SelectedGenerationUiState { - return when (generation) { - WorkResult.Loading -> SelectedGenerationUiState.Loading - is WorkResult.Error -> SelectedGenerationUiState.Error - is WorkResult.Success -> { - val pokemonUiState = when (pokemon) { - is WorkResult.Error -> PokemonUiState.Error - WorkResult.Loading -> PokemonUiState.Loading - is WorkResult.Success -> { + return when { + generation.isFailure -> SelectedGenerationUiState.Error + generation.isSuccess -> { + val pokemonUiState = when { + pokemon.isFailure -> PokemonUiState.Error + pokemon.isSuccess -> { PokemonUiState.Success( - pokemon.data.sortedBy { it.id }.also { pokemonList -> - viewModelScope.launch { - pokemonList.forEach { - retrieveAsset( - it.id, - buildString { - append(it.name) - append(SUFFIX_SPRITE) - }, - it.sprite, - it.remoteSprite, - saveAssetUseCase = { id, url, fileName -> - saveRemoteImageUseCase(SaveImageData(id, url, fileName, true)) - } - ) + pokemon.getOrDefault(emptyList()).sortedBy { it.id } + .also { pokemonList -> + viewModelScope.launch { + pokemonList.forEach { + retrieveAsset( + it.id, + buildString { + append(it.name) + append(SUFFIX_SPRITE) + }, + it.sprite, + it.remoteSprite, + saveAssetUseCase = { id, url, fileName -> + saveRemoteImageUseCase( + SaveImageData( + id, + url, + fileName, + true + ) + ) + } + ) + } } } - } ) } + + else -> PokemonUiState.Loading } SelectedGenerationUiState.Success( - selectedGeneration = generation.data, + selectedGeneration = generation.getOrThrow(), pokemonState = pokemonUiState, - showLoadingItem = pokemonUiState.stillLoading(generation.data.numberOfPokemon) + showLoadingItem = pokemonUiState.stillLoading(generation.getOrThrow().numberOfPokemon) ) } + + else -> SelectedGenerationUiState.Loading } } private fun buildGenerationScreenUiState( - generations: WorkResult>, + generations: Result>, selectedGenerationUiState: SelectedGenerationUiState ): GenerationScreenUiState { - return when (generations) { - is WorkResult.Error -> GenerationScreenUiState.Error - WorkResult.Loading -> GenerationScreenUiState.Loading - is WorkResult.Success -> { + return when { + generations.isFailure -> GenerationScreenUiState.Error + generations.isSuccess -> { GenerationScreenUiState.Success( - generations = generations.data, + generations = generations.getOrDefault(emptyList()), selectedGeneration = selectedGenerationUiState ) } + + else -> GenerationScreenUiState.Loading } } } diff --git a/app/src/main/kotlin/de/entikore/composedex/ui/screen/pokemon/PokemonViewModel.kt b/app/src/main/kotlin/de/entikore/composedex/ui/screen/pokemon/PokemonViewModel.kt index 91ba341..d030433 100644 --- a/app/src/main/kotlin/de/entikore/composedex/ui/screen/pokemon/PokemonViewModel.kt +++ b/app/src/main/kotlin/de/entikore/composedex/ui/screen/pokemon/PokemonViewModel.kt @@ -21,11 +21,10 @@ import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.exoplayer.ExoPlayer import dagger.hilt.android.lifecycle.HiltViewModel -import de.entikore.composedex.domain.WorkResult import de.entikore.composedex.domain.model.pokemon.ChainLink import de.entikore.composedex.domain.model.pokemon.Pokemon import de.entikore.composedex.domain.model.pokemon.Variety -import de.entikore.composedex.domain.usecase.GetPokemonUseCase +import de.entikore.composedex.domain.usecase.FetchPokemonUseCase import de.entikore.composedex.domain.usecase.SaveImageData import de.entikore.composedex.domain.usecase.SaveSoundData import de.entikore.composedex.domain.usecase.SetFavouriteData @@ -54,7 +53,7 @@ import javax.inject.Inject */ @HiltViewModel class PokemonViewModel @Inject constructor( - private val getPokemonUseCase: GetPokemonUseCase, + private val getPokemonUseCase: FetchPokemonUseCase, private val saveRemoteImageUseCase: @JvmSuppressWildcards ParamsSuspendUseCase, private val saveRemoteCryUseCase: @JvmSuppressWildcards ParamsSuspendUseCase, private val setAsFavouriteUseCase: @JvmSuppressWildcards ParamsSuspendUseCase, @@ -74,16 +73,14 @@ class PokemonViewModel @Inject constructor( .flatMapLatest { pokemonParameter -> pokemonParameter?.let { getPokemonUseCase(pokemonParameter).map { - when (it) { - is WorkResult.Error -> { + when { + it.isFailure -> { PokemonScreenState.Error( errorMessage = "$ERROR_LOADING_POKEMON ${_selectedPokemonFlow.value}" ) } - - WorkResult.Loading -> PokemonScreenState.Loading - is WorkResult.Success -> { - val pokemon = it.data + it.isSuccess -> { + val pokemon = it.getOrThrow() viewModelScope.launch { retrieveAsset( @@ -127,6 +124,7 @@ class PokemonViewModel @Inject constructor( varieties = listOf(pokemon) ) } + else -> PokemonScreenState.Loading } } } ?: flowOf(PokemonScreenState.NoPokemonSelected) @@ -314,16 +312,10 @@ class PokemonViewModel @Inject constructor( getPokemonUseCase(name).map { val evolutionText = "Evolves from ${name.replaceFirstChar { char -> char.uppercaseChar() }}" - when (it) { - is WorkResult.Error -> null - WorkResult.Loading -> PokemonPreview( - name = name, - isLoading = true, - evolutionText = evolutionText - ) - - is WorkResult.Success -> { - it.data.also { pokemon -> + when { + it.isFailure -> null + it.isSuccess -> { + it.getOrThrow().also { pokemon -> retrieveAsset( pokemon.id, buildString { @@ -337,25 +329,30 @@ class PokemonViewModel @Inject constructor( } ) } - if (it.data.sprite == null) { + if (it.getOrThrow().sprite == null) { PokemonPreview( name = name, - url = it.data.remoteSprite, - types = it.data.types, + url = it.getOrThrow().remoteSprite, + types = it.getOrThrow().types, evolutionText = evolutionText, isLoading = true ) } else { PokemonPreview( name = name, - url = it.data.remoteSprite, - types = it.data.types, - sprite = it.data.sprite, + url = it.getOrThrow().remoteSprite, + types = it.getOrThrow().types, + sprite = it.getOrThrow().sprite!!, evolutionText = evolutionText, isLoading = false ) } } + else -> PokemonPreview( + name = name, + isLoading = true, + evolutionText = evolutionText + ) } } } @@ -374,19 +371,13 @@ class PokemonViewModel @Inject constructor( val evolutionText = "Evolves to ${evolvesToList[it].name.replaceFirstChar { char -> char.uppercaseChar() }}" getPokemonUseCase(evolvesToList[it].url).map { result -> - when (result) { - is WorkResult.Error -> { + when { + result.isFailure -> { Timber.d("Error loading Pokemon ${evolvesToList[it].name}") null } - WorkResult.Loading -> PokemonPreview( - name = evolvesToList[it].name, - evolutionText = evolutionText, - isLoading = true - ) - - is WorkResult.Success -> { - result.data.also { pokemon -> + result.isSuccess -> { + result.getOrThrow().also { pokemon -> retrieveAsset( pokemon.id, buildString { @@ -401,24 +392,29 @@ class PokemonViewModel @Inject constructor( ) } - if (result.data.sprite == null) { + if (result.getOrThrow().sprite == null) { PokemonPreview( - name = result.data.name, - url = result.data.remoteSprite, - types = result.data.types, + name = result.getOrThrow().name, + url = result.getOrThrow().remoteSprite, + types = result.getOrThrow().types, evolutionText = evolutionText, isLoading = true ) } else { PokemonPreview( - name = result.data.name, - types = result.data.types, - sprite = result.data.sprite, + name = result.getOrThrow().name, + types = result.getOrThrow().types, + sprite = result.getOrThrow().sprite!!, evolutionText = evolutionText, isLoading = false ) } } + else -> PokemonPreview( + name = evolvesToList[it].name, + evolutionText = evolutionText, + isLoading = true + ) } } } @@ -432,9 +428,9 @@ class PokemonViewModel @Inject constructor( } return Array(size = varieties.size) { getPokemonUseCase(varieties[it].varietyName).map { result -> - when (result) { - is WorkResult.Success -> { - result.data.also { pokemon -> + when { + result.isSuccess -> { + result.getOrThrow().also { pokemon -> viewModelScope.launch { retrieveAsset( pokemon.id, @@ -470,8 +466,8 @@ class PokemonViewModel @Inject constructor( ) } } - if (result.data.artwork != null) { - result.data + if (result.getOrThrow().artwork != null) { + result.getOrThrow() } else { Timber.d("Artwork for variety ${varieties[it].varietyName} is null") null diff --git a/app/src/main/kotlin/de/entikore/composedex/ui/screen/setting/SettingsViewModel.kt b/app/src/main/kotlin/de/entikore/composedex/ui/screen/setting/SettingsViewModel.kt index 679ccc6..bb7093c 100644 --- a/app/src/main/kotlin/de/entikore/composedex/ui/screen/setting/SettingsViewModel.kt +++ b/app/src/main/kotlin/de/entikore/composedex/ui/screen/setting/SettingsViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,6 +55,6 @@ class SettingsViewModel @Inject constructor( } fun switchTheme(theme: AppThemeConfig) { - viewModelScope.launch { changeLightDarkThemeUseCase.invoke(theme) } + viewModelScope.launch { changeLightDarkThemeUseCase(theme) } } } diff --git a/app/src/main/kotlin/de/entikore/composedex/ui/screen/shared/PokemonUiState.kt b/app/src/main/kotlin/de/entikore/composedex/ui/screen/shared/PokemonUiState.kt index 7d2ebe8..5efd411 100644 --- a/app/src/main/kotlin/de/entikore/composedex/ui/screen/shared/PokemonUiState.kt +++ b/app/src/main/kotlin/de/entikore/composedex/ui/screen/shared/PokemonUiState.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import de.entikore.composedex.domain.model.pokemon.Pokemon * Models state of a list of Pokemon. */ sealed interface PokemonUiState { - data class Success(val pokemon: List) : PokemonUiState + data class Success(val pokemon: List = emptyList()) : PokemonUiState data object Error : PokemonUiState data object Loading : PokemonUiState } diff --git a/app/src/main/kotlin/de/entikore/composedex/ui/screen/type/TypeViewModel.kt b/app/src/main/kotlin/de/entikore/composedex/ui/screen/type/TypeViewModel.kt index 8b16d7f..91bf27c 100644 --- a/app/src/main/kotlin/de/entikore/composedex/ui/screen/type/TypeViewModel.kt +++ b/app/src/main/kotlin/de/entikore/composedex/ui/screen/type/TypeViewModel.kt @@ -17,12 +17,11 @@ package de.entikore.composedex.ui.screen.type import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import de.entikore.composedex.domain.WorkResult import de.entikore.composedex.domain.model.pokemon.Pokemon import de.entikore.composedex.domain.model.type.Type -import de.entikore.composedex.domain.usecase.GetPokemonOfTypeUseCase -import de.entikore.composedex.domain.usecase.GetTypeUseCase -import de.entikore.composedex.domain.usecase.GetTypesUseCase +import de.entikore.composedex.domain.usecase.FetchPokemonOfTypeUseCase +import de.entikore.composedex.domain.usecase.FetchTypeUseCase +import de.entikore.composedex.domain.usecase.FetchTypesUseCase import de.entikore.composedex.domain.usecase.SaveImageData import de.entikore.composedex.domain.usecase.SetFavouriteData import de.entikore.composedex.domain.usecase.base.ParamsSuspendUseCase @@ -34,13 +33,14 @@ import de.entikore.composedex.ui.screen.type.TypeScreenUiState.Success import de.entikore.composedex.ui.screen.util.SUFFIX_SPRITE import de.entikore.composedex.ui.screen.util.retrieveAsset import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import timber.log.Timber @@ -51,44 +51,45 @@ import javax.inject.Inject */ @HiltViewModel class TypeViewModel @Inject constructor( - getTypesUseCase: GetTypesUseCase, - getTypeUseCase: GetTypeUseCase, - getPokemonOfTypeUseCase: GetPokemonOfTypeUseCase, + getTypesUseCase: FetchTypesUseCase, + private val getTypeUseCase: FetchTypeUseCase, + private val getPokemonOfTypeUseCase: FetchPokemonOfTypeUseCase, private val saveRemoteImageUseCase: @JvmSuppressWildcards ParamsSuspendUseCase, private val setAsFavouriteUseCase: @JvmSuppressWildcards ParamsSuspendUseCase ) : PokemonFilterViewModel() { - private val _selectedTypeFlow = MutableStateFlow(null) - val selectedType: StateFlow = _selectedTypeFlow.map { it ?: "" } - .stateIn( - viewModelScope, - SharingStarted.Eagerly, - "" + private val _selectedTypeFlow = MutableStateFlow("") + val selectedType: StateFlow = + _selectedTypeFlow.asStateFlow() + + @OptIn(ExperimentalCoroutinesApi::class) + private fun fetchSelectedTypeDetailsFlow(type: String): Flow { + return getTypeUseCase(type).combine( + getPokemonOfTypeUseCase(type), + ::buildSelectedTypeUiState ) + } @OptIn(ExperimentalCoroutinesApi::class) - val screenState = + val screenState: StateFlow = combine( getTypesUseCase(), _selectedTypeFlow.flatMapLatest { selectedType -> - selectedType?.let { - getTypeUseCase(it).combine( - getPokemonOfTypeUseCase(it), - ::buildSelectedTypeUiState - ) - } ?: flowOf(SelectedTypeUiState.NoTypeSelected) + if (selectedType.isNotEmpty()) { + fetchSelectedTypeDetailsFlow(selectedType) + } else { + flowOf(SelectedTypeUiState.NoTypeSelected) + } }, ::buildTypeScreenUiState ).combine(filterOptions) { uiState: TypeScreenUiState, filterSettings: PokemonFilterOptions -> - uiState.withFilteredPokemonList( - uiState.getPokemonList()?.let { - filterSettings.getFilteredList(it) - } - ) + val currentPokemonList = uiState.getPokemonList() + val filteredList = currentPokemonList?.let { filterSettings.getFilteredList(it) } + uiState.withFilteredPokemonList(filteredList) }.stateIn( - viewModelScope, - SharingStarted.Eagerly, - Success() + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000L), + initialValue = TypeScreenUiState.Loading ) fun fetchType(typeName: String) { @@ -102,19 +103,17 @@ class TypeViewModel @Inject constructor( } private fun buildSelectedTypeUiState( - type: WorkResult, - pokemon: WorkResult> + type: Result, + pokemon: Result> ): SelectedTypeUiState { - return when (type) { - WorkResult.Loading -> SelectedTypeUiState.Loading - is WorkResult.Error -> SelectedTypeUiState.Error - is WorkResult.Success -> { - val pokemonUiState = when (pokemon) { - is WorkResult.Error -> PokemonUiState.Error - WorkResult.Loading -> PokemonUiState.Loading - is WorkResult.Success -> { + return when { + type.isFailure -> SelectedTypeUiState.Error + type.isSuccess -> { + val pokemonUiState = when { + pokemon.isFailure -> PokemonUiState.Error + pokemon.isSuccess -> { PokemonUiState.Success( - pokemon.data.sortedBy { it.id }.also { pokemonList -> + pokemon.getOrThrow().sortedBy { it.id }.also { pokemonList -> viewModelScope.launch { pokemonList.forEach { retrieveAsset( @@ -126,7 +125,14 @@ class TypeViewModel @Inject constructor( it.sprite, it.remoteSprite, saveAssetUseCase = { id, url, fileName -> - saveRemoteImageUseCase(SaveImageData(id, url, fileName, true)) + saveRemoteImageUseCase( + SaveImageData( + id, + url, + fileName, + true + ) + ) } ) } @@ -134,28 +140,39 @@ class TypeViewModel @Inject constructor( } ) } + + else -> PokemonUiState.Loading } SelectedTypeUiState.Success( - selectedType = type.data, + selectedType = type.getOrThrow(), pokemonState = pokemonUiState, - showLoadingItem = pokemonUiState.stillLoading(type.data.pokemonOfType.size) + showLoadingItem = pokemonUiState.stillLoading(type.getOrThrow().pokemonOfType.size) ) } + + else -> SelectedTypeUiState.Loading } } private fun buildTypeScreenUiState( - types: WorkResult>, + types: Result>, selectedTypeUiState: SelectedTypeUiState ): TypeScreenUiState { - return when (types) { - is WorkResult.Error -> TypeScreenUiState.Error - WorkResult.Loading -> TypeScreenUiState.Loading - is WorkResult.Success -> { - Success( - types = types.data, + return when { + types.isFailure -> { + TypeScreenUiState.Error + } + + types.isSuccess -> { + val success = Success( + types = types.getOrThrow(), selectedType = selectedTypeUiState ) + success + } + + else -> { + TypeScreenUiState.Loading } } } diff --git a/app/src/test/kotlin/de/entikore/composedex/domain/usecase/GetGenerationUseCaseTest.kt b/app/src/test/kotlin/de/entikore/composedex/domain/usecase/FetchGenerationUseCaseTest.kt similarity index 74% rename from app/src/test/kotlin/de/entikore/composedex/domain/usecase/GetGenerationUseCaseTest.kt rename to app/src/test/kotlin/de/entikore/composedex/domain/usecase/FetchGenerationUseCaseTest.kt index 1d2413a..7b33eb0 100644 --- a/app/src/test/kotlin/de/entikore/composedex/domain/usecase/GetGenerationUseCaseTest.kt +++ b/app/src/test/kotlin/de/entikore/composedex/domain/usecase/FetchGenerationUseCaseTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import com.google.common.truth.Truth.assertThat import de.entikore.composedex.MainCoroutineRule import de.entikore.composedex.data.local.entity.generation.asExternalModel import de.entikore.composedex.data.remote.model.generation.toEntity -import de.entikore.composedex.domain.WorkResult import de.entikore.composedex.domain.model.generation.Generation import de.entikore.composedex.fake.repository.FailableFakeRepository.Companion.EXPECTED_TEST_EXCEPTION import de.entikore.composedex.fake.repository.FakeGenerationRepository @@ -37,18 +36,18 @@ import org.junit.jupiter.api.extension.ExtendWith @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainCoroutineRule::class) -class GetGenerationUseCaseTest { +class FetchGenerationUseCaseTest { private lateinit var repository: FakeGenerationRepository - private lateinit var getGenerationUseCase: GetGenerationUseCase + private lateinit var getGenerationUseCase: FetchGenerationUseCase @BeforeEach fun setUp() { repository = FakeGenerationRepository() - getGenerationUseCase = GetGenerationUseCase(repository) + getGenerationUseCase = FetchGenerationUseCase(repository) } @Test - fun `get generation by name results in WorkResult Success with searched generation`() = + fun `get generation by name results in successful Result Success with searched generation`() = runTest { val generationI = getGenerationRemote(GEN_I_FILE).toEntity().asExternalModel() val generationII = getGenerationRemote(GEN_II_FILE).toEntity().asExternalModel() @@ -63,7 +62,7 @@ class GetGenerationUseCaseTest { } @Test - fun `get generation by id results in WorkResult Success with searched generation`() = + fun `get generation by id results in successful Result with searched generation`() = runTest { val generationI = getGenerationRemote(GEN_I_FILE).toEntity().asExternalModel() val generationII = getGenerationRemote(GEN_II_FILE).toEntity().asExternalModel() @@ -85,28 +84,24 @@ class GetGenerationUseCaseTest { repository.addGenerations(*testData) getGenerationUseCase.invoke(useCaseParam).test { - var actualGeneration = awaitItem() - assertThat(actualGeneration).isInstanceOf(WorkResult.Loading::class.java) - actualGeneration = awaitItem() - assertThat(actualGeneration).isInstanceOf(WorkResult.Success::class.java) - assertThat((actualGeneration as WorkResult.Success).data).isEqualTo(expectedGeneration) + val actualGeneration = awaitItem() + assertThat(actualGeneration.isSuccess).isTrue() + assertThat(actualGeneration.getOrThrow()).isEqualTo(expectedGeneration) awaitComplete() } } @Test - fun `get generation by unknown name results in WorkResult Error`() = runTest { + fun `get generation by unknown name results in error Result`() = runTest { val generationI = getGenerationRemote(GEN_I_FILE).toEntity().asExternalModel() val generationII = getGenerationRemote(GEN_II_FILE).toEntity().asExternalModel() val generationVI = getGenerationRemote(GEN_VI_FILE).toEntity().asExternalModel() repository.addGenerations(generationI, generationII, generationVI) getGenerationUseCase.invoke("unknown").test { - var actualGeneration = awaitItem() - assertThat(actualGeneration).isInstanceOf(WorkResult.Loading::class.java) - actualGeneration = awaitItem() - assertThat(actualGeneration).isInstanceOf(WorkResult.Error::class.java) - assertThat((actualGeneration as WorkResult.Error).exception!!.message).isEqualTo( + val actualGeneration = awaitItem() + assertThat(actualGeneration.isFailure).isTrue() + assertThat(actualGeneration.exceptionOrNull()?.message).isEqualTo( GENERATION_WITH_NAME_NOT_FOUND ) awaitComplete() @@ -114,7 +109,7 @@ class GetGenerationUseCaseTest { } @Test - fun `get generation by name results in WorkResult Error on any exception`() = runTest { + fun `get generation by name results in error Result on any exception`() = runTest { val generationI = getGenerationRemote(GEN_I_FILE).toEntity().asExternalModel() val generationII = getGenerationRemote(GEN_II_FILE).toEntity().asExternalModel() val generationVI = getGenerationRemote(GEN_VI_FILE).toEntity().asExternalModel() @@ -128,7 +123,7 @@ class GetGenerationUseCaseTest { } @Test - fun `get generation by id results in WorkResult Error on any exception`() = runTest { + fun `get generation by id results in error Result on any exception`() = runTest { val generationI = getGenerationRemote(GEN_I_FILE).toEntity().asExternalModel() val generationII = getGenerationRemote(GEN_II_FILE).toEntity().asExternalModel() val generationVI = getGenerationRemote(GEN_VI_FILE).toEntity().asExternalModel() @@ -149,11 +144,9 @@ class GetGenerationUseCaseTest { repository.setReturnError(true) getGenerationUseCase.invoke(useCaseParam).test { - var actualGeneration = awaitItem() - assertThat(actualGeneration).isInstanceOf(WorkResult.Loading::class.java) - actualGeneration = awaitItem() - assertThat(actualGeneration).isInstanceOf(WorkResult.Error::class.java) - assertThat((actualGeneration as WorkResult.Error).exception!!.message).isEqualTo( + val actualGeneration = awaitItem() + assertThat(actualGeneration.isFailure).isTrue() + assertThat(actualGeneration.exceptionOrNull()?.message).isEqualTo( EXPECTED_TEST_EXCEPTION ) awaitComplete() diff --git a/app/src/test/kotlin/de/entikore/composedex/domain/usecase/GetGenerationsUseCaseTest.kt b/app/src/test/kotlin/de/entikore/composedex/domain/usecase/FetchGenerationsUseCaseTest.kt similarity index 63% rename from app/src/test/kotlin/de/entikore/composedex/domain/usecase/GetGenerationsUseCaseTest.kt rename to app/src/test/kotlin/de/entikore/composedex/domain/usecase/FetchGenerationsUseCaseTest.kt index 0f025a9..6fd02ad 100644 --- a/app/src/test/kotlin/de/entikore/composedex/domain/usecase/GetGenerationsUseCaseTest.kt +++ b/app/src/test/kotlin/de/entikore/composedex/domain/usecase/FetchGenerationsUseCaseTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import com.google.common.truth.Truth.assertThat import de.entikore.composedex.MainCoroutineRule import de.entikore.composedex.data.local.entity.generation.asExternalModel import de.entikore.composedex.data.remote.model.generation.toEntity -import de.entikore.composedex.domain.WorkResult import de.entikore.composedex.fake.repository.FailableFakeRepository.Companion.EXPECTED_TEST_EXCEPTION import de.entikore.composedex.fake.repository.FakeGenerationRepository import de.entikore.sharedtestcode.GEN_II_FILE @@ -35,50 +34,46 @@ import org.junit.jupiter.api.extension.ExtendWith @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainCoroutineRule::class) -class GetGenerationsUseCaseTest { +class FetchGenerationsUseCaseTest { private lateinit var repository: FakeGenerationRepository - private lateinit var getGenerationsUseCase: GetGenerationsUseCase + private lateinit var getGenerationsUseCase: FetchGenerationsUseCase @BeforeEach fun setUp() { repository = FakeGenerationRepository() - getGenerationsUseCase = GetGenerationsUseCase(repository) + getGenerationsUseCase = FetchGenerationsUseCase(repository) } @Test - fun `get generations results in WorkResult Success with all generations`() = runTest { + fun `get generations results in successful Result with all generations`() = runTest { val generationI = getGenerationRemote(GEN_I_FILE).toEntity().asExternalModel() val generationII = getGenerationRemote(GEN_II_FILE).toEntity().asExternalModel() val generationVI = getGenerationRemote(GEN_VI_FILE).toEntity().asExternalModel() repository.addGenerations(generationI, generationII, generationVI) getGenerationsUseCase().test { - var allGenerationsResult = awaitItem() - assertThat(allGenerationsResult).isInstanceOf(WorkResult.Loading::class.java) - allGenerationsResult = awaitItem() - assertThat(allGenerationsResult).isInstanceOf(WorkResult.Success::class.java) + val allGenerationsResult = awaitItem() + assertThat(allGenerationsResult.isSuccess).isTrue() assertThat( - (allGenerationsResult as WorkResult.Success).data + (allGenerationsResult.getOrThrow()) ).containsExactly(generationI, generationII, generationVI) awaitComplete() } } @Test - fun `get generations results in WorkResult Success with empty list when no generations are present`() = runTest { + fun `successful Result with empty list when no generations are present`() = runTest { getGenerationsUseCase().test { - var allGenerationsResult = awaitItem() - assertThat(allGenerationsResult).isInstanceOf(WorkResult.Loading::class.java) - allGenerationsResult = awaitItem() - assertThat(allGenerationsResult).isInstanceOf(WorkResult.Success::class.java) - assertThat((allGenerationsResult as WorkResult.Success).data).isEmpty() + val allGenerationsResult = awaitItem() + assertThat(allGenerationsResult.isSuccess).isTrue() + assertThat((allGenerationsResult.getOrThrow())).isEmpty() awaitComplete() } } @Test - fun `get generations results in WorkResult Error on any exception`() = runTest { + fun `get generations results in error Result on any exception`() = runTest { val generationI = getGenerationRemote(GEN_I_FILE).toEntity().asExternalModel() val generationII = getGenerationRemote(GEN_II_FILE).toEntity().asExternalModel() val generationVI = getGenerationRemote(GEN_VI_FILE).toEntity().asExternalModel() @@ -86,11 +81,9 @@ class GetGenerationsUseCaseTest { repository.setReturnError(true) getGenerationsUseCase().test { - var allGenerationsResult = awaitItem() - assertThat(allGenerationsResult).isInstanceOf(WorkResult.Loading::class.java) - allGenerationsResult = awaitItem() - assertThat(allGenerationsResult).isInstanceOf(WorkResult.Error::class.java) - assertThat((allGenerationsResult as WorkResult.Error).exception!!.message).isEqualTo( + val allGenerationsResult = awaitItem() + assertThat(allGenerationsResult.isFailure).isTrue() + assertThat((allGenerationsResult.exceptionOrNull())?.message).isEqualTo( EXPECTED_TEST_EXCEPTION ) awaitComplete() diff --git a/app/src/test/kotlin/de/entikore/composedex/domain/usecase/GetPokemonOfGenerationUseCaseTest.kt b/app/src/test/kotlin/de/entikore/composedex/domain/usecase/FetchPokemonOfGenerationUseCaseTest.kt similarity index 79% rename from app/src/test/kotlin/de/entikore/composedex/domain/usecase/GetPokemonOfGenerationUseCaseTest.kt rename to app/src/test/kotlin/de/entikore/composedex/domain/usecase/FetchPokemonOfGenerationUseCaseTest.kt index e456638..dbd3553 100644 --- a/app/src/test/kotlin/de/entikore/composedex/domain/usecase/GetPokemonOfGenerationUseCaseTest.kt +++ b/app/src/test/kotlin/de/entikore/composedex/domain/usecase/FetchPokemonOfGenerationUseCaseTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,6 @@ import de.entikore.composedex.data.local.entity.generation.asExternalModel import de.entikore.composedex.data.local.entity.pokemon.relation.asExternalModel import de.entikore.composedex.data.remote.model.generation.toEntity import de.entikore.composedex.data.remote.model.toEntity -import de.entikore.composedex.domain.WorkResult import de.entikore.composedex.domain.model.pokemon.Pokemon import de.entikore.composedex.fake.repository.FailableFakeRepository.Companion.EXPECTED_TEST_EXCEPTION import de.entikore.composedex.fake.repository.FakeGenerationRepository @@ -42,18 +41,18 @@ import org.junit.jupiter.api.extension.ExtendWith @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainCoroutineRule::class) -class GetPokemonOfGenerationUseCaseTest { +class FetchPokemonOfGenerationUseCaseTest { private lateinit var repository: FakeGenerationRepository - private lateinit var getPokemonOfGenerationUseCase: GetPokemonOfGenerationUseCase + private lateinit var getPokemonOfGenerationUseCase: FetchPokemonOfGenerationUseCase @BeforeEach fun setUp() { repository = FakeGenerationRepository() - getPokemonOfGenerationUseCase = GetPokemonOfGenerationUseCase(repository) + getPokemonOfGenerationUseCase = FetchPokemonOfGenerationUseCase(repository) } @Test - fun `get pokemon of generation by name results in WorkResult Success with all expected pokemon`() = runTest { + fun `get pokemon of generation by name results in successful Result with all expected pokemon`() = runTest { val generationI = getGenerationRemote(GEN_I_FILE).toEntity().asExternalModel() val generationII = getGenerationRemote(GEN_II_FILE).toEntity().asExternalModel() @@ -72,7 +71,7 @@ class GetPokemonOfGenerationUseCaseTest { } @Test - fun `get pokemon of generation by id results in WorkResult Success with all expected pokemon`() = runTest { + fun `get pokemon of generation by id results in successful Result with all expected pokemon`() = runTest { val generationI = getGenerationRemote(GEN_I_FILE).toEntity().asExternalModel() val generationII = getGenerationRemote(GEN_II_FILE).toEntity().asExternalModel() @@ -91,7 +90,7 @@ class GetPokemonOfGenerationUseCaseTest { } @Test - fun `get pokemon of generation results in WorkResult Success with empty list if no pokemon of generation`() = runTest { + fun `get pokemon of generation results in successful Result with empty list if no pokemon of generation`() = runTest { val generationI = getGenerationRemote(GEN_I_FILE).toEntity().asExternalModel() val generationII = getGenerationRemote(GEN_II_FILE).toEntity().asExternalModel() val lapras = getPokemonInfoRemote(getTestModel(POKEMON_LAPRAS_NAME)).toEntity().asExternalModel() @@ -105,7 +104,7 @@ class GetPokemonOfGenerationUseCaseTest { } @Test - fun `get pokemon of generation results in WorkResult Error on any exception`() = runTest { + fun `get pokemon of generation results in error Result on any exception`() = runTest { val generationI = getGenerationRemote(GEN_I_FILE).toEntity().asExternalModel() val lapras = getPokemonInfoRemote(getTestModel(POKEMON_LAPRAS_NAME)).toEntity().asExternalModel() val ditto = getPokemonInfoRemote(getTestModel(POKEMON_DITTO_NAME)).toEntity().asExternalModel() @@ -114,11 +113,9 @@ class GetPokemonOfGenerationUseCaseTest { repository.addPokemon(lapras, ditto) repository.setReturnError(true) getPokemonOfGenerationUseCase(generationI.name).test { - var actualPokemon = awaitItem() - assertThat(actualPokemon).isInstanceOf(WorkResult.Loading::class.java) - actualPokemon = awaitItem() - assertThat(actualPokemon).isInstanceOf(WorkResult.Error::class.java) - assertThat((actualPokemon as WorkResult.Error).exception!!.message).isEqualTo( + val actualPokemon = awaitItem() + assertThat(actualPokemon.isFailure).isTrue() + assertThat(actualPokemon.exceptionOrNull()?.message).isEqualTo( EXPECTED_TEST_EXCEPTION ) awaitComplete() @@ -126,11 +123,9 @@ class GetPokemonOfGenerationUseCaseTest { } private suspend fun getPokemonOfGenerationSuccessful(useCaseParam: String, expectedResult: List) { getPokemonOfGenerationUseCase(useCaseParam).test { - var actualPokemon = awaitItem() - assertThat(actualPokemon).isInstanceOf(WorkResult.Loading::class.java) - actualPokemon = awaitItem() - assertThat(actualPokemon).isInstanceOf(WorkResult.Success::class.java) - assertThat((actualPokemon as WorkResult.Success).data).isEqualTo(expectedResult) + val actualPokemon = awaitItem() + assertThat(actualPokemon.isSuccess).isTrue() + assertThat(actualPokemon.getOrThrow()).isEqualTo(expectedResult) awaitComplete() } } diff --git a/app/src/test/kotlin/de/entikore/composedex/domain/usecase/GetPokemonOfTypeUseCaseTest.kt b/app/src/test/kotlin/de/entikore/composedex/domain/usecase/FetchPokemonOfTypeUseCaseTest.kt similarity index 70% rename from app/src/test/kotlin/de/entikore/composedex/domain/usecase/GetPokemonOfTypeUseCaseTest.kt rename to app/src/test/kotlin/de/entikore/composedex/domain/usecase/FetchPokemonOfTypeUseCaseTest.kt index 65692d1..f8a4380 100644 --- a/app/src/test/kotlin/de/entikore/composedex/domain/usecase/GetPokemonOfTypeUseCaseTest.kt +++ b/app/src/test/kotlin/de/entikore/composedex/domain/usecase/FetchPokemonOfTypeUseCaseTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,6 @@ import de.entikore.composedex.data.local.entity.pokemon.relation.asExternalModel import de.entikore.composedex.data.local.entity.type.asExternalModel import de.entikore.composedex.data.remote.model.toEntity import de.entikore.composedex.data.remote.model.type.toEntity -import de.entikore.composedex.domain.WorkResult import de.entikore.composedex.fake.repository.FailableFakeRepository.Companion.EXPECTED_TEST_EXCEPTION import de.entikore.composedex.fake.repository.FakeTypeRepository import de.entikore.sharedtestcode.POKEMON_BELLOSSOM_NAME @@ -43,18 +42,18 @@ import org.junit.jupiter.api.extension.ExtendWith @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainCoroutineRule::class) -class GetPokemonOfTypeUseCaseTest { +class FetchPokemonOfTypeUseCaseTest { private lateinit var repository: FakeTypeRepository - private lateinit var getPokemonOfTypesUseCase: GetPokemonOfTypeUseCase + private lateinit var getPokemonOfTypesUseCase: FetchPokemonOfTypeUseCase @BeforeEach fun setUp() { repository = FakeTypeRepository() - getPokemonOfTypesUseCase = GetPokemonOfTypeUseCase(repository) + getPokemonOfTypesUseCase = FetchPokemonOfTypeUseCase(repository) } @Test - fun `get pokemon of types results in WorkResult Success with all expected pokemon`() = runTest { + fun `get pokemon of types results in successful Result with all expected pokemon`() = runTest { val poisonType = getTypeRemote(TYPE_POISON_FILE).toEntity().asExternalModel() val grassType = getTypeRemote(TYPE_GRASS_FILE).toEntity().asExternalModel() @@ -70,25 +69,21 @@ class GetPokemonOfTypeUseCaseTest { val expectedGrassPokemon = expectedPoisonPokemon.plus(bellossom) getPokemonOfTypesUseCase(poisonType.name).test { - var actualPokemon = awaitItem() - assertThat(actualPokemon).isInstanceOf(WorkResult.Loading::class.java) - actualPokemon = awaitItem() - assertThat(actualPokemon).isInstanceOf(WorkResult.Success::class.java) - assertThat((actualPokemon as WorkResult.Success).data).isEqualTo(expectedPoisonPokemon) + val actualPokemon = awaitItem() + assertThat(actualPokemon.isSuccess).isTrue() + assertThat(actualPokemon.getOrThrow()).isEqualTo(expectedPoisonPokemon) awaitComplete() } getPokemonOfTypesUseCase(grassType.name).test { - var actualPokemon = awaitItem() - assertThat(actualPokemon).isInstanceOf(WorkResult.Loading::class.java) - actualPokemon = awaitItem() - assertThat(actualPokemon).isInstanceOf(WorkResult.Success::class.java) - assertThat((actualPokemon as WorkResult.Success).data).isEqualTo(expectedGrassPokemon) + val actualPokemon = awaitItem() + assertThat(actualPokemon.isSuccess).isTrue() + assertThat(actualPokemon.getOrThrow()).isEqualTo(expectedGrassPokemon) awaitComplete() } } @Test - fun `get pokemon of types results in WorkResult Success with empty list if no pokemon of type`() = runTest { + fun `get pokemon of types results in successful Result with empty list if no pokemon of type`() = runTest { val iceType = getTypeRemote(TYPE_ICE_FILE).toEntity().asExternalModel() val grassType = getTypeRemote(TYPE_GRASS_FILE).toEntity().asExternalModel() @@ -101,27 +96,23 @@ class GetPokemonOfTypeUseCaseTest { repository.addPokemon(oddish, gloom, vileplume, bellossom) getPokemonOfTypesUseCase(iceType.name).test { - var actualPokemon = awaitItem() - assertThat(actualPokemon).isInstanceOf(WorkResult.Loading::class.java) - actualPokemon = awaitItem() - assertThat(actualPokemon).isInstanceOf(WorkResult.Success::class.java) - assertThat((actualPokemon as WorkResult.Success).data).isEmpty() + val actualPokemon = awaitItem() + assertThat(actualPokemon.isSuccess).isTrue() + assertThat(actualPokemon.getOrThrow()).isEmpty() awaitComplete() } } @Test - fun `get pokemon of types results in WorkResult Error on any exception`() = runTest { + fun `get pokemon of types results in error Result on any exception`() = runTest { val grassType = getTypeRemote(TYPE_GRASS_FILE).toEntity().asExternalModel() repository.addTypes(getTypeRemote(TYPE_GRASS_FILE).toEntity().asExternalModel()) repository.addPokemon(getPokemonInfoRemote(getTestModel(POKEMON_ODDISH_NAME)).toEntity().asExternalModel()) repository.setReturnError(true) getPokemonOfTypesUseCase(grassType.name).test { - var actualPokemon = awaitItem() - assertThat(actualPokemon).isInstanceOf(WorkResult.Loading::class.java) - actualPokemon = awaitItem() - assertThat(actualPokemon).isInstanceOf(WorkResult.Error::class.java) - assertThat((actualPokemon as WorkResult.Error).exception!!.message).isEqualTo( + val actualPokemon = awaitItem() + assertThat(actualPokemon.isFailure).isTrue() + assertThat(actualPokemon.exceptionOrNull()?.message).isEqualTo( EXPECTED_TEST_EXCEPTION ) awaitComplete() diff --git a/app/src/test/kotlin/de/entikore/composedex/domain/usecase/GetPokemonUseCaseTest.kt b/app/src/test/kotlin/de/entikore/composedex/domain/usecase/FetchPokemonUseCaseTest.kt similarity index 78% rename from app/src/test/kotlin/de/entikore/composedex/domain/usecase/GetPokemonUseCaseTest.kt rename to app/src/test/kotlin/de/entikore/composedex/domain/usecase/FetchPokemonUseCaseTest.kt index 6ef3079..38209ee 100644 --- a/app/src/test/kotlin/de/entikore/composedex/domain/usecase/GetPokemonUseCaseTest.kt +++ b/app/src/test/kotlin/de/entikore/composedex/domain/usecase/FetchPokemonUseCaseTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import com.google.common.truth.Truth.assertThat import de.entikore.composedex.MainCoroutineRule import de.entikore.composedex.data.local.entity.pokemon.relation.asExternalModel import de.entikore.composedex.data.remote.model.toEntity -import de.entikore.composedex.domain.WorkResult import de.entikore.composedex.domain.model.pokemon.Pokemon import de.entikore.composedex.fake.repository.FailableFakeRepository.Companion.EXPECTED_TEST_EXCEPTION import de.entikore.composedex.fake.repository.FakePokemonRepository @@ -41,18 +40,18 @@ import org.junit.jupiter.api.extension.ExtendWith @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainCoroutineRule::class) -class GetPokemonUseCaseTest { +class FetchPokemonUseCaseTest { private lateinit var repository: FakePokemonRepository - private lateinit var getPokemonUseCase: GetPokemonUseCase + private lateinit var getPokemonUseCase: FetchPokemonUseCase @BeforeEach fun setUp() { repository = FakePokemonRepository() - getPokemonUseCase = GetPokemonUseCase(repository) + getPokemonUseCase = FetchPokemonUseCase(repository) } @Test - fun `get pokemon by name results in WorkResult Success with expected Pokemon`() = runTest { + fun `get pokemon by name results in successful Result with expected Pokemon`() = runTest { val oddish = getPokemonInfoRemote(getTestModel(POKEMON_ODDISH_NAME)).toEntity().asExternalModel() val gloom = @@ -73,7 +72,7 @@ class GetPokemonUseCaseTest { } @Test - fun `get pokemon by id results in WorkResult Success with expected Pokemon`() = runTest { + fun `get pokemon by id results in successful Result with expected Pokemon`() = runTest { val oddish = getPokemonInfoRemote(getTestModel(POKEMON_ODDISH_NAME)).toEntity().asExternalModel() val gloom = @@ -94,7 +93,7 @@ class GetPokemonUseCaseTest { } @Test - fun `get pokemon by unknown name results in WorkResult Error`() = runTest { + fun `get pokemon by unknown name results in error Result`() = runTest { val oddish = getPokemonInfoRemote(getTestModel(POKEMON_ODDISH_NAME)).toEntity().asExternalModel() @@ -107,7 +106,7 @@ class GetPokemonUseCaseTest { } @Test - fun `get pokemon by unknown id results in WorkResult Error`() = runTest { + fun `get pokemon by unknown id results in error Result`() = runTest { val oddish = getPokemonInfoRemote(getTestModel(POKEMON_ODDISH_NAME)).toEntity().asExternalModel() @@ -120,7 +119,7 @@ class GetPokemonUseCaseTest { } @Test - fun `get pokemon by name results in WorkResult Error on any exception`() = runTest { + fun `get pokemon by name results in error Result on any exception`() = runTest { val oddish = getPokemonInfoRemote(getTestModel(POKEMON_ODDISH_NAME)).toEntity().asExternalModel() @@ -133,7 +132,7 @@ class GetPokemonUseCaseTest { } @Test - fun `get pokemon by id results in WorkResult Error on any exception`() = runTest { + fun `get pokemon by id results in error Result on any exception`() = runTest { val oddish = getPokemonInfoRemote(getTestModel(POKEMON_ODDISH_NAME)).toEntity().asExternalModel() @@ -153,12 +152,10 @@ class GetPokemonUseCaseTest { repository.addPokemon(*testData) getPokemonUseCase.invoke(useCaseParam).test { - var actualPokemon = awaitItem() - assertThat(actualPokemon).isInstanceOf(WorkResult.Loading::class.java) - actualPokemon = awaitItem() - assertThat(actualPokemon).isInstanceOf(WorkResult.Success::class.java) - assertThat((actualPokemon as WorkResult.Success).data.name).isEqualTo(expectedPokemon.name) - assertThat((actualPokemon).data.id).isEqualTo(expectedPokemon.id) + val actualPokemon = awaitItem() + assertThat(actualPokemon.isSuccess).isTrue() + assertThat(actualPokemon.getOrThrow().name).isEqualTo(expectedPokemon.name) + assertThat(actualPokemon.getOrThrow().id).isEqualTo(expectedPokemon.id) awaitComplete() } } @@ -173,11 +170,9 @@ class GetPokemonUseCaseTest { repository.setReturnError(shouldReturnError) getPokemonUseCase.invoke(useCaseParam).test { - var actualPokemon = awaitItem() - assertThat(actualPokemon).isInstanceOf(WorkResult.Loading::class.java) - actualPokemon = awaitItem() - assertThat(actualPokemon).isInstanceOf(WorkResult.Error::class.java) - assertThat((actualPokemon as WorkResult.Error).exception!!.message).isEqualTo( + val actualPokemon = awaitItem() + assertThat(actualPokemon.isFailure).isTrue() + assertThat(actualPokemon.exceptionOrNull()?.message).isEqualTo( expectedException ) awaitComplete() diff --git a/app/src/test/kotlin/de/entikore/composedex/domain/usecase/GetTypeUseCaseTest.kt b/app/src/test/kotlin/de/entikore/composedex/domain/usecase/FetchTypeUseCaseTest.kt similarity index 67% rename from app/src/test/kotlin/de/entikore/composedex/domain/usecase/GetTypeUseCaseTest.kt rename to app/src/test/kotlin/de/entikore/composedex/domain/usecase/FetchTypeUseCaseTest.kt index 6cb11be..807bbe1 100644 --- a/app/src/test/kotlin/de/entikore/composedex/domain/usecase/GetTypeUseCaseTest.kt +++ b/app/src/test/kotlin/de/entikore/composedex/domain/usecase/FetchTypeUseCaseTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import com.google.common.truth.Truth.assertThat import de.entikore.composedex.MainCoroutineRule import de.entikore.composedex.data.local.entity.type.asExternalModel import de.entikore.composedex.data.remote.model.type.toEntity -import de.entikore.composedex.domain.WorkResult import de.entikore.composedex.fake.repository.FailableFakeRepository.Companion.EXPECTED_TEST_EXCEPTION import de.entikore.composedex.fake.repository.FakeTypeRepository import de.entikore.composedex.fake.repository.FakeTypeRepository.Companion.TYPE_WITH_NAME_NOT_FOUND @@ -36,47 +35,43 @@ import org.junit.jupiter.api.extension.ExtendWith @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainCoroutineRule::class) -class GetTypeUseCaseTest { +class FetchTypeUseCaseTest { private lateinit var repository: FakeTypeRepository - private lateinit var getTypeUseCase: GetTypeUseCase + private lateinit var getTypeUseCase: FetchTypeUseCase @BeforeEach fun setUp() { repository = FakeTypeRepository() - getTypeUseCase = GetTypeUseCase(repository) + getTypeUseCase = FetchTypeUseCase(repository) } @Test - fun `get type results in WorkResult Success with searched type`() = runTest { + fun `get type results in successful Result with searched type`() = runTest { val poisonType = getTypeRemote(TYPE_POISON_FILE).toEntity().asExternalModel() val grassType = getTypeRemote(TYPE_GRASS_FILE).toEntity().asExternalModel() val normalType = getTypeRemote(TYPE_NORMAL_FILE).toEntity().asExternalModel() repository.addTypes(poisonType, grassType, normalType) getTypeUseCase.invoke(poisonType.name).test { - var actualType = awaitItem() - assertThat(actualType).isInstanceOf(WorkResult.Loading::class.java) - actualType = awaitItem() - assertThat(actualType).isInstanceOf(WorkResult.Success::class.java) - assertThat((actualType as WorkResult.Success).data).isEqualTo(poisonType) + val actualType = awaitItem() + assertThat(actualType.isSuccess).isTrue() + assertThat(actualType.getOrThrow()).isEqualTo(poisonType) awaitComplete() } } @Test - fun `get type by unknown name results in WorkResult Error`() = runTest { + fun `get type by unknown name results in error Result`() = runTest { val poisonType = getTypeRemote(TYPE_POISON_FILE).toEntity().asExternalModel() val grassType = getTypeRemote(TYPE_GRASS_FILE).toEntity().asExternalModel() val normalType = getTypeRemote(TYPE_NORMAL_FILE).toEntity().asExternalModel() repository.addTypes(poisonType, grassType, normalType) getTypeUseCase.invoke("unknown").test { - var actualType = awaitItem() - assertThat(actualType).isInstanceOf(WorkResult.Loading::class.java) - actualType = awaitItem() - assertThat(actualType).isInstanceOf(WorkResult.Error::class.java) - assertThat((actualType as WorkResult.Error).exception!!.message).isEqualTo( + val actualType = awaitItem() + assertThat(actualType.isFailure).isTrue() + assertThat(actualType.exceptionOrNull()?.message).isEqualTo( TYPE_WITH_NAME_NOT_FOUND ) awaitComplete() @@ -84,17 +79,15 @@ class GetTypeUseCaseTest { } @Test - fun `get type results in WorkResult Error on any exception`() = runTest { + fun `get type results in error Result on any exception`() = runTest { val poisonType = getTypeRemote(TYPE_POISON_FILE).toEntity().asExternalModel() repository.addTypes(poisonType) repository.setReturnError(true) getTypeUseCase.invoke(poisonType.name).test { - var actualType = awaitItem() - assertThat(actualType).isInstanceOf(WorkResult.Loading::class.java) - actualType = awaitItem() - assertThat(actualType).isInstanceOf(WorkResult.Error::class.java) - assertThat((actualType as WorkResult.Error).exception!!.message).isEqualTo(EXPECTED_TEST_EXCEPTION) + val actualType = awaitItem() + assertThat(actualType.isFailure).isTrue() + assertThat(actualType.exceptionOrNull()?.message).isEqualTo(EXPECTED_TEST_EXCEPTION) awaitComplete() } } diff --git a/app/src/test/kotlin/de/entikore/composedex/domain/usecase/GetTypesUseCaseTest.kt b/app/src/test/kotlin/de/entikore/composedex/domain/usecase/FetchTypesUseCaseTest.kt similarity index 63% rename from app/src/test/kotlin/de/entikore/composedex/domain/usecase/GetTypesUseCaseTest.kt rename to app/src/test/kotlin/de/entikore/composedex/domain/usecase/FetchTypesUseCaseTest.kt index 3bcdb6c..a39cf66 100644 --- a/app/src/test/kotlin/de/entikore/composedex/domain/usecase/GetTypesUseCaseTest.kt +++ b/app/src/test/kotlin/de/entikore/composedex/domain/usecase/FetchTypesUseCaseTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import com.google.common.truth.Truth.assertThat import de.entikore.composedex.MainCoroutineRule import de.entikore.composedex.data.local.entity.type.asExternalModel import de.entikore.composedex.data.remote.model.type.toEntity -import de.entikore.composedex.domain.WorkResult import de.entikore.composedex.fake.repository.FailableFakeRepository.Companion.EXPECTED_TEST_EXCEPTION import de.entikore.composedex.fake.repository.FakeTypeRepository import de.entikore.sharedtestcode.TYPE_GRASS_FILE @@ -35,48 +34,44 @@ import org.junit.jupiter.api.extension.ExtendWith @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainCoroutineRule::class) -class GetTypesUseCaseTest { +class FetchTypesUseCaseTest { private lateinit var repository: FakeTypeRepository - private lateinit var getTypesUseCase: GetTypesUseCase + private lateinit var getTypesUseCase: FetchTypesUseCase @BeforeEach fun setUp() { repository = FakeTypeRepository() - getTypesUseCase = GetTypesUseCase(repository) + getTypesUseCase = FetchTypesUseCase(repository) } @Test - fun `get types results in WorkResult Success with all types`() = runTest { + fun `get types results in successful Result with all types`() = runTest { val poisonType = getTypeRemote(TYPE_POISON_FILE).toEntity().asExternalModel() val grassType = getTypeRemote(TYPE_GRASS_FILE).toEntity().asExternalModel() val normalType = getTypeRemote(TYPE_NORMAL_FILE).toEntity().asExternalModel() repository.addTypes(poisonType, grassType, normalType) getTypesUseCase().test { - var allTypesResult = awaitItem() - assertThat(allTypesResult).isInstanceOf(WorkResult.Loading::class.java) - allTypesResult = awaitItem() - assertThat(allTypesResult).isInstanceOf(WorkResult.Success::class.java) - assertThat((allTypesResult as WorkResult.Success).data).containsExactly(poisonType, grassType, normalType) + val allTypesResult = awaitItem() + assertThat(allTypesResult.isSuccess).isTrue() + assertThat(allTypesResult.getOrThrow()).containsExactly(poisonType, grassType, normalType) awaitComplete() } } @Test - fun `get types results in WorkResult Success with empty list when no types are present`() = runTest { + fun `get types results in successful Result with empty list when no types are present`() = runTest { getTypesUseCase().test { - var allTypesResult = awaitItem() - assertThat(allTypesResult).isInstanceOf(WorkResult.Loading::class.java) - allTypesResult = awaitItem() - assertThat(allTypesResult).isInstanceOf(WorkResult.Success::class.java) - assertThat((allTypesResult as WorkResult.Success).data).isEmpty() + val allTypesResult = awaitItem() + assertThat(allTypesResult.isSuccess).isTrue() + assertThat(allTypesResult.getOrThrow()).isEmpty() awaitComplete() } } @Test - fun `get types results in WorkResult Error on any exception`() = runTest { + fun `get types results in error Result on any exception`() = runTest { val poisonType = getTypeRemote(TYPE_POISON_FILE).toEntity().asExternalModel() val grassType = getTypeRemote(TYPE_GRASS_FILE).toEntity().asExternalModel() val normalType = getTypeRemote(TYPE_NORMAL_FILE).toEntity().asExternalModel() @@ -84,11 +79,9 @@ class GetTypesUseCaseTest { repository.setReturnError(true) getTypesUseCase().test { - var allTypesResult = awaitItem() - assertThat(allTypesResult).isInstanceOf(WorkResult.Loading::class.java) - allTypesResult = awaitItem() - assertThat(allTypesResult).isInstanceOf(WorkResult.Error::class.java) - assertThat((allTypesResult as WorkResult.Error).exception!!.message).isEqualTo(EXPECTED_TEST_EXCEPTION) + val allTypesResult = awaitItem() + assertThat(allTypesResult.isFailure).isTrue() + assertThat(allTypesResult.exceptionOrNull()?.message).isEqualTo(EXPECTED_TEST_EXCEPTION) awaitComplete() } } diff --git a/app/src/test/kotlin/de/entikore/composedex/konsist/architecture/ArchitectureCheck.kt b/app/src/test/kotlin/de/entikore/composedex/konsist/architecture/ArchitectureCheck.kt index a47a1db..f353892 100644 --- a/app/src/test/kotlin/de/entikore/composedex/konsist/architecture/ArchitectureCheck.kt +++ b/app/src/test/kotlin/de/entikore/composedex/konsist/architecture/ArchitectureCheck.kt @@ -44,21 +44,6 @@ class ArchitectureCheck { } } - @Test - fun `Classes with 'UseCases' suffix should expose one public function with 'operator' modifier named 'invoke'`() { - Konsist - .scopeFromProduction() - .classes() - .withNameEndingWith(USE_CASE) - .assertTrue { - val hasSingleInvokeOperatorMethod = it.hasFunction { function -> - function.name == "invoke" && function.hasPublicOrDefaultModifier && function.hasOperatorModifier - } - - hasSingleInvokeOperatorMethod && it.countFunctions { item -> item.hasPublicOrDefaultModifier } == 1 - } - } - @Test fun `Classes with 'UseCase' suffix should reside in 'domain' and 'usecase' package`() { Konsist diff --git a/app/src/test/kotlin/de/entikore/composedex/ui/screen/favourite/FavouriteViewModelTest.kt b/app/src/test/kotlin/de/entikore/composedex/ui/screen/favourite/FavouriteViewModelTest.kt index 7c29f9d..ce04c2b 100644 --- a/app/src/test/kotlin/de/entikore/composedex/ui/screen/favourite/FavouriteViewModelTest.kt +++ b/app/src/test/kotlin/de/entikore/composedex/ui/screen/favourite/FavouriteViewModelTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import com.google.common.truth.Truth.assertThat import de.entikore.composedex.MainCoroutineRule import de.entikore.composedex.data.local.entity.pokemon.relation.asExternalModel import de.entikore.composedex.data.remote.model.toEntity -import de.entikore.composedex.domain.usecase.GetFavouritesUseCase +import de.entikore.composedex.domain.usecase.FetchFavouritesUseCase import de.entikore.composedex.domain.usecase.SetAsFavouriteUseCase import de.entikore.composedex.fake.repository.FakeFavouriteRepository import de.entikore.composedex.ui.screen.shared.PokemonUiState @@ -41,64 +41,64 @@ class FavouriteViewModelTest { private lateinit var viewModel: FavouriteViewModel - private lateinit var getFavouritesUseCase: GetFavouritesUseCase + private lateinit var getFavouritesUseCase: FetchFavouritesUseCase private lateinit var setAsFavouriteUseCase: SetAsFavouriteUseCase private lateinit var fakeFavouriteRepository: FakeFavouriteRepository @BeforeEach fun setup() { fakeFavouriteRepository = FakeFavouriteRepository() - getFavouritesUseCase = GetFavouritesUseCase(fakeFavouriteRepository) + getFavouritesUseCase = FetchFavouritesUseCase(fakeFavouriteRepository) setAsFavouriteUseCase = SetAsFavouriteUseCase(fakeFavouriteRepository) } @Test - fun `creating FavouriteViewModel without favourite Pokemon exposes empty PokemonUiState`() = + fun `creating FavouriteViewModel without favourite Pokemon exposes PokemonUiState loading`() = runTest { viewModel = FavouriteViewModel(getFavouritesUseCase, setAsFavouriteUseCase) - val expectedState = PokemonUiState.Success(emptyList()) - assertThat(viewModel.screenState.value).isEqualTo(expectedState) + assertThat(viewModel.screenState.value).isEqualTo(PokemonUiState.Loading) } @Test - fun `favourites are loaded correctly`() = runTest { - val lapras = - getPokemonInfoRemote(getTestModel(POKEMON_LAPRAS_NAME)).toEntity().asExternalModel() - .copy(isFavourite = true) - val ditto = - getPokemonInfoRemote(getTestModel(POKEMON_DITTO_NAME)).toEntity().asExternalModel() - .copy(isFavourite = true) - viewModel = FavouriteViewModel(getFavouritesUseCase, setAsFavouriteUseCase) - - viewModel.screenState.test { - var stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(PokemonUiState.Success::class.java) - assertThat((stateResult as PokemonUiState.Success).pokemon).isEmpty() - - fakeFavouriteRepository.addPokemon( - lapras, - ditto - ) - - stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(PokemonUiState.Loading::class.java) - - stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(PokemonUiState.Success::class.java) - assertThat((stateResult as PokemonUiState.Success).pokemon).isEqualTo( - listOf(lapras, ditto) - ) - - viewModel.updateFavourite(POKEMON_LAPRAS_ID, false) + fun `FavouriteViewModel exposes success state when getting favourites are loaded correctly`() = + runTest { + val lapras = + getPokemonInfoRemote(getTestModel(POKEMON_LAPRAS_NAME)).toEntity().asExternalModel() + .copy(isFavourite = true) + val ditto = + getPokemonInfoRemote(getTestModel(POKEMON_DITTO_NAME)).toEntity().asExternalModel() + .copy(isFavourite = true) + viewModel = FavouriteViewModel(getFavouritesUseCase, setAsFavouriteUseCase) - stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(PokemonUiState.Success::class.java) - assertThat((stateResult as PokemonUiState.Success).pokemon).isEqualTo( - listOf(ditto) - ) + viewModel.screenState.test { + var stateResult = awaitItem() + assertThat(stateResult).isInstanceOf(PokemonUiState.Loading::class.java) + + stateResult = awaitItem() + assertThat(stateResult).isInstanceOf(PokemonUiState.Success::class.java) + assertThat((stateResult as PokemonUiState.Success).pokemon).isEmpty() + + fakeFavouriteRepository.addPokemon( + lapras, + ditto + ) + + stateResult = awaitItem() + assertThat(stateResult).isInstanceOf(PokemonUiState.Success::class.java) + assertThat((stateResult as PokemonUiState.Success).pokemon).isEqualTo( + listOf(lapras, ditto) + ) + + viewModel.updateFavourite(POKEMON_LAPRAS_ID, false) + + stateResult = awaitItem() + assertThat(stateResult).isInstanceOf(PokemonUiState.Success::class.java) + assertThat((stateResult as PokemonUiState.Success).pokemon).isEqualTo( + listOf(ditto) + ) + } } - } @Test fun `FavouriteViewModel exposes error state when getting favourites fails`() = runTest { @@ -110,19 +110,15 @@ class FavouriteViewModelTest { .copy(isFavourite = true) fakeFavouriteRepository.shouldReturnError = true + fakeFavouriteRepository.addPokemon( + lapras, + ditto + ) + viewModel = FavouriteViewModel(getFavouritesUseCase, setAsFavouriteUseCase) viewModel.screenState.test { var stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(PokemonUiState.Success::class.java) - assertThat((stateResult as PokemonUiState.Success).pokemon).isEmpty() - - fakeFavouriteRepository.addPokemon( - lapras, - ditto - ) - - stateResult = awaitItem() assertThat(stateResult).isInstanceOf(PokemonUiState.Loading::class.java) stateResult = awaitItem() diff --git a/app/src/test/kotlin/de/entikore/composedex/ui/screen/generation/GenerationViewModelTest.kt b/app/src/test/kotlin/de/entikore/composedex/ui/screen/generation/GenerationViewModelTest.kt index c9da60a..b828d21 100644 --- a/app/src/test/kotlin/de/entikore/composedex/ui/screen/generation/GenerationViewModelTest.kt +++ b/app/src/test/kotlin/de/entikore/composedex/ui/screen/generation/GenerationViewModelTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,9 +22,9 @@ import de.entikore.composedex.data.local.entity.generation.asExternalModel import de.entikore.composedex.data.local.entity.pokemon.relation.asExternalModel import de.entikore.composedex.data.remote.model.generation.toEntity import de.entikore.composedex.data.remote.model.toEntity -import de.entikore.composedex.domain.usecase.GetGenerationUseCase -import de.entikore.composedex.domain.usecase.GetGenerationsUseCase -import de.entikore.composedex.domain.usecase.GetPokemonOfGenerationUseCase +import de.entikore.composedex.domain.usecase.FetchGenerationUseCase +import de.entikore.composedex.domain.usecase.FetchGenerationsUseCase +import de.entikore.composedex.domain.usecase.FetchPokemonOfGenerationUseCase import de.entikore.composedex.domain.usecase.SetAsFavouriteUseCase import de.entikore.composedex.fake.repository.FakeGenerationRepository import de.entikore.composedex.fake.usecase.FakeSaveRemoteImageUseCase @@ -49,9 +49,9 @@ import org.mockito.Mockito.mock @ExtendWith(MainCoroutineRule::class) class GenerationViewModelTest { - private lateinit var getGenerationsUseCase: GetGenerationsUseCase - private lateinit var getGenerationUseCase: GetGenerationUseCase - private lateinit var getPokemonOfGenerationUseCase: GetPokemonOfGenerationUseCase + private lateinit var getGenerationsUseCase: FetchGenerationsUseCase + private lateinit var getGenerationUseCase: FetchGenerationUseCase + private lateinit var getPokemonOfGenerationUseCase: FetchPokemonOfGenerationUseCase private lateinit var saveRemoteImageUseCase: FakeSaveRemoteImageUseCase private lateinit var setAsFavouriteUseCase: SetAsFavouriteUseCase @@ -60,9 +60,9 @@ class GenerationViewModelTest { @BeforeEach fun setUp() { - getGenerationsUseCase = GetGenerationsUseCase(fakeGenerationRepository) - getGenerationUseCase = GetGenerationUseCase(fakeGenerationRepository) - getPokemonOfGenerationUseCase = GetPokemonOfGenerationUseCase(fakeGenerationRepository) + getGenerationsUseCase = FetchGenerationsUseCase(fakeGenerationRepository) + getGenerationUseCase = FetchGenerationUseCase(fakeGenerationRepository) + getPokemonOfGenerationUseCase = FetchPokemonOfGenerationUseCase(fakeGenerationRepository) saveRemoteImageUseCase = FakeSaveRemoteImageUseCase() setAsFavouriteUseCase = mock() } @@ -79,8 +79,13 @@ class GenerationViewModelTest { val expectedState = GenerationScreenUiState.Success() - assertThat(viewModel.screenState.value).isInstanceOf(GenerationScreenUiState.Success::class.java) - assertThat(viewModel.screenState.value).isEqualTo(expectedState) + viewModel.screenState.test { + var stateResult = awaitItem() + assertThat(stateResult).isInstanceOf(GenerationScreenUiState.Loading::class.java) + stateResult = awaitItem() + assertThat(stateResult).isInstanceOf(GenerationScreenUiState.Success::class.java) + assertThat(stateResult).isEqualTo(expectedState) + } } @Test @@ -105,8 +110,7 @@ class GenerationViewModelTest { viewModel.screenState.test { var stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(GenerationScreenUiState.Success::class.java) - assertThat(stateResult).isEqualTo(GenerationScreenUiState.Success()) + assertThat(stateResult).isInstanceOf(GenerationScreenUiState.Loading::class.java) stateResult = awaitItem() assertThat(stateResult).isInstanceOf(GenerationScreenUiState.Success::class.java) @@ -141,16 +145,15 @@ class GenerationViewModelTest { viewModel.screenState.test { var stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(GenerationScreenUiState.Success::class.java) - assertThat(stateResult).isEqualTo(GenerationScreenUiState.Success()) - - viewModel.searchForGeneration(generationI.id.toString()) + assertThat(stateResult).isInstanceOf(GenerationScreenUiState.Loading::class.java) stateResult = awaitItem() assertThat(stateResult).isInstanceOf(GenerationScreenUiState.Success::class.java) assertThat( stateResult - ).isEqualTo(expectedState.copy(selectedGeneration = SelectedGenerationUiState.Loading)) + ).isEqualTo(expectedState.copy(selectedGeneration = SelectedGenerationUiState.NoGenerationSelected)) + + viewModel.searchForGeneration(generationI.id.toString()) stateResult = awaitItem() assertThat(stateResult).isInstanceOf(GenerationScreenUiState.Success::class.java) @@ -178,16 +181,15 @@ class GenerationViewModelTest { viewModel.screenState.test { var stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(GenerationScreenUiState.Success::class.java) - assertThat(stateResult).isEqualTo(GenerationScreenUiState.Success()) - - viewModel.searchForGeneration(GEN_II_NAME) + assertThat(stateResult).isInstanceOf(GenerationScreenUiState.Loading::class.java) stateResult = awaitItem() assertThat(stateResult).isInstanceOf(GenerationScreenUiState.Success::class.java) assertThat( stateResult - ).isEqualTo(expectedState.copy(selectedGeneration = SelectedGenerationUiState.Loading)) + ).isEqualTo(expectedState.copy(selectedGeneration = SelectedGenerationUiState.NoGenerationSelected)) + + viewModel.searchForGeneration(GEN_II_NAME) stateResult = awaitItem() assertThat(stateResult).isInstanceOf(GenerationScreenUiState.Success::class.java) diff --git a/app/src/test/kotlin/de/entikore/composedex/ui/screen/pokemon/PokemonViewModelTest.kt b/app/src/test/kotlin/de/entikore/composedex/ui/screen/pokemon/PokemonViewModelTest.kt index 4fa65af..9a3477a 100644 --- a/app/src/test/kotlin/de/entikore/composedex/ui/screen/pokemon/PokemonViewModelTest.kt +++ b/app/src/test/kotlin/de/entikore/composedex/ui/screen/pokemon/PokemonViewModelTest.kt @@ -24,7 +24,7 @@ import de.entikore.composedex.MainCoroutineRule import de.entikore.composedex.data.local.entity.pokemon.relation.asExternalModel import de.entikore.composedex.data.remote.model.toEntity import de.entikore.composedex.domain.model.pokemon.Pokemon -import de.entikore.composedex.domain.usecase.GetPokemonUseCase +import de.entikore.composedex.domain.usecase.FetchPokemonUseCase import de.entikore.composedex.domain.usecase.SetAsFavouriteUseCase import de.entikore.composedex.domain.util.whitespacePattern import de.entikore.composedex.fake.repository.FakePokemonRepository @@ -68,7 +68,7 @@ class PokemonViewModelTest { @Mock private lateinit var mockTTS: ComposeDexTTS - private lateinit var pokemonUseCase: GetPokemonUseCase + private lateinit var pokemonUseCase: FetchPokemonUseCase private lateinit var saveRemoteImageUseCase: FakeSaveRemoteImageUseCase private lateinit var saveRemoteCryUseCase: FakeSaveRemoteCryUseCase private lateinit var setAsFavouriteUseCase: SetAsFavouriteUseCase @@ -82,7 +82,7 @@ class PokemonViewModelTest { mockSavedStateHandle = mock(SavedStateHandle::class.java) mockPlayer = mock(ExoPlayer::class.java) mockTTS = mock(ComposeDexTTS::class.java) - pokemonUseCase = GetPokemonUseCase(fakePokemonRepository) + pokemonUseCase = FetchPokemonUseCase(fakePokemonRepository) saveRemoteImageUseCase = FakeSaveRemoteImageUseCase() saveRemoteCryUseCase = FakeSaveRemoteCryUseCase() setAsFavouriteUseCase = mock() @@ -93,7 +93,7 @@ class PokemonViewModelTest { fun `creating PokemonDetailViewModel exposes an empty Success PokemonDetailScreenUiState`() = runTest { viewModel = PokemonViewModel( - GetPokemonUseCase(fakePokemonRepository), + FetchPokemonUseCase(fakePokemonRepository), saveRemoteImageUseCase, saveRemoteCryUseCase, setAsFavouriteUseCase, @@ -116,7 +116,7 @@ class PokemonViewModelTest { fakePokemonRepository.addPokemon(*testData) viewModel = PokemonViewModel( - GetPokemonUseCase(fakePokemonRepository), + FetchPokemonUseCase(fakePokemonRepository), saveRemoteImageUseCase, saveRemoteCryUseCase, setAsFavouriteUseCase, @@ -131,9 +131,6 @@ class PokemonViewModelTest { viewModel.lookUpPokemon(searchQuery) - stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(PokemonScreenState.Loading::class.java) - stateResult = awaitItem() assertThat(stateResult).isInstanceOf(PokemonScreenState.Success::class.java) assertThat(stateResult).isEqualTo( @@ -146,7 +143,7 @@ class PokemonViewModelTest { fun `search for unknown pokemon exposes Error PokemonDetailScreenUiState`() = runTest { viewModel = PokemonViewModel( - GetPokemonUseCase(fakePokemonRepository), + FetchPokemonUseCase(fakePokemonRepository), saveRemoteImageUseCase, saveRemoteCryUseCase, setAsFavouriteUseCase, @@ -165,9 +162,6 @@ class PokemonViewModelTest { viewModel.lookUpPokemon(POKEMON_GLOOM_NAME) - stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(PokemonScreenState.Loading::class.java) - stateResult = awaitItem() assertThat(stateResult).isInstanceOf(PokemonScreenState.Error::class.java) assertThat(stateResult).isEqualTo( @@ -216,7 +210,7 @@ class PokemonViewModelTest { } /** - * Copy from [GetPokemonUseCase.processFlavorTextEntries]. + * Copy from [FetchPokemonUseCase.processFlavorTextEntries]. */ private fun Pokemon.processFlavourTextEntries(): Pokemon = this.copy( diff --git a/app/src/test/kotlin/de/entikore/composedex/ui/screen/type/TypeViewModelTest.kt b/app/src/test/kotlin/de/entikore/composedex/ui/screen/type/TypeViewModelTest.kt index e6a4715..b875e9b 100644 --- a/app/src/test/kotlin/de/entikore/composedex/ui/screen/type/TypeViewModelTest.kt +++ b/app/src/test/kotlin/de/entikore/composedex/ui/screen/type/TypeViewModelTest.kt @@ -20,9 +20,9 @@ import com.google.common.truth.Truth.assertThat import de.entikore.composedex.MainCoroutineRule import de.entikore.composedex.data.local.entity.type.asExternalModel import de.entikore.composedex.data.remote.model.type.toEntity -import de.entikore.composedex.domain.usecase.GetPokemonOfTypeUseCase -import de.entikore.composedex.domain.usecase.GetTypeUseCase -import de.entikore.composedex.domain.usecase.GetTypesUseCase +import de.entikore.composedex.domain.usecase.FetchPokemonOfTypeUseCase +import de.entikore.composedex.domain.usecase.FetchTypeUseCase +import de.entikore.composedex.domain.usecase.FetchTypesUseCase import de.entikore.composedex.domain.usecase.SetAsFavouriteUseCase import de.entikore.composedex.fake.repository.FakeTypeRepository import de.entikore.composedex.fake.usecase.FakeSaveRemoteImageUseCase @@ -45,9 +45,9 @@ import org.mockito.Mockito.mock @ExtendWith(MainCoroutineRule::class) class TypeViewModelTest { - private lateinit var typesUseCase: GetTypesUseCase - private lateinit var typeUseCase: GetTypeUseCase - private lateinit var getPokemonOfTypeUseCase: GetPokemonOfTypeUseCase + private lateinit var typesUseCase: FetchTypesUseCase + private lateinit var typeUseCase: FetchTypeUseCase + private lateinit var getPokemonOfTypeUseCase: FetchPokemonOfTypeUseCase private lateinit var saveRemoteImageUseCase: FakeSaveRemoteImageUseCase private lateinit var setAsFavouriteUseCase: SetAsFavouriteUseCase @@ -56,9 +56,9 @@ class TypeViewModelTest { @BeforeEach fun setUp() { - typesUseCase = GetTypesUseCase(fakeTypeRepository) - typeUseCase = GetTypeUseCase(fakeTypeRepository) - getPokemonOfTypeUseCase = GetPokemonOfTypeUseCase(fakeTypeRepository) + typesUseCase = FetchTypesUseCase(fakeTypeRepository) + typeUseCase = FetchTypeUseCase(fakeTypeRepository) + getPokemonOfTypeUseCase = FetchPokemonOfTypeUseCase(fakeTypeRepository) saveRemoteImageUseCase = FakeSaveRemoteImageUseCase() setAsFavouriteUseCase = mock() } @@ -75,8 +75,13 @@ class TypeViewModelTest { val expectedState = TypeScreenUiState.Success() - assertThat(viewModel.screenState.value).isInstanceOf(TypeScreenUiState.Success::class.java) - assertThat(viewModel.screenState.value).isEqualTo(expectedState) + viewModel.screenState.test { + var stateResult = awaitItem() + assertThat(stateResult).isEqualTo(TypeScreenUiState.Loading) + stateResult = awaitItem() + assertThat(stateResult).isEqualTo(TypeScreenUiState.Success()) + assertThat(viewModel.screenState.value).isEqualTo(expectedState) + } } @Test @@ -102,10 +107,9 @@ class TypeViewModelTest { viewModel.screenState.test { var stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(TypeScreenUiState.Success::class.java) - assertThat(stateResult).isEqualTo(TypeScreenUiState.Success()) - + assertThat(stateResult).isEqualTo(TypeScreenUiState.Loading) stateResult = awaitItem() + assertThat(stateResult).isInstanceOf(TypeScreenUiState.Success::class.java) assertThat(stateResult).isEqualTo(expectedState) } @@ -129,38 +133,26 @@ class TypeViewModelTest { val expectedState = TypeScreenUiState.Success( types = listOf(iceType, normalType, grassType, poisonType), - selectedType = SelectedTypeUiState.NoTypeSelected + selectedType = SelectedTypeUiState.Success( + selectedType = iceType, + pokemonState = PokemonUiState.Success(emptyList()), + showLoadingItem = true + ) ) viewModel.screenState.test { var stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(TypeScreenUiState.Success::class.java) - assertThat(stateResult).isEqualTo(TypeScreenUiState.Success()) - + assertThat(stateResult).isEqualTo(TypeScreenUiState.Loading) stateResult = awaitItem() assertThat(stateResult).isInstanceOf(TypeScreenUiState.Success::class.java) - assertThat(stateResult).isEqualTo(expectedState) + assertThat(stateResult).isEqualTo(expectedState.copy(selectedType = SelectedTypeUiState.NoTypeSelected)) viewModel.fetchType(TYPE_ICE_NAME) - stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(TypeScreenUiState.Success::class.java) - assertThat(stateResult).isEqualTo( - expectedState.copy( - selectedType = - SelectedTypeUiState.Loading - ) - ) stateResult = awaitItem() assertThat(stateResult).isInstanceOf(TypeScreenUiState.Success::class.java) assertThat(stateResult).isEqualTo( - expectedState.copy( - selectedType = SelectedTypeUiState.Success( - selectedType = iceType, - pokemonState = PokemonUiState.Success(emptyList()), - showLoadingItem = true - ) - ) + expectedState ) } } @@ -185,22 +177,12 @@ class TypeViewModelTest { viewModel.screenState.test { var stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(TypeScreenUiState.Success::class.java) - assertThat(stateResult).isEqualTo(TypeScreenUiState.Success()) - + assertThat(stateResult).isEqualTo(TypeScreenUiState.Loading) stateResult = awaitItem() assertThat(stateResult).isInstanceOf(TypeScreenUiState.Success::class.java) - assertThat(stateResult).isEqualTo(expectedState) + assertThat(stateResult).isEqualTo(expectedState.copy(selectedType = SelectedTypeUiState.NoTypeSelected)) viewModel.fetchType(TYPE_POISON_NAME) - stateResult = awaitItem() - assertThat(stateResult).isInstanceOf(TypeScreenUiState.Success::class.java) - assertThat(stateResult).isEqualTo( - expectedState.copy( - selectedType = - SelectedTypeUiState.Loading - ) - ) stateResult = awaitItem() assertThat(stateResult).isInstanceOf(TypeScreenUiState.Success::class.java) From 6dddbe14e6b4a77fb3c23d79edb88bc1cdec6043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20Grebe-L=C3=BCth?= Date: Fri, 22 Aug 2025 18:01:23 +0200 Subject: [PATCH 2/2] refactor: Unified use cases for background work. Replaced SuspendUseCase.kt and ParamsSuspendUseCase.kt with BaseSuspendUseCase.kt --- .../usecase/ChangeLightDarkThemeUseCase.kt | 17 +++----- .../domain/usecase/ChangeTypeThemeUseCase.kt | 17 +++----- .../domain/usecase/DeleteLocalDataUseCase.kt | 21 ++++----- .../usecase/GetUserPreferencesUseCase.kt | 9 ++-- .../domain/usecase/SaveRemoteImageUseCase.kt | 30 ++++++------- .../domain/usecase/SaveRemoteSoundUseCase.kt | 25 ++++++----- .../domain/usecase/SetAsFavouriteUseCase.kt | 14 +++--- .../domain/usecase/base/BaseSuspendUseCase.kt | 43 +++++++++++++++++++ .../usecase/base/ParamsSuspendUseCase.kt | 27 ------------ .../domain/usecase/base/SuspendUseCase.kt | 25 ----------- .../composedex/domain/usecase/base/UseCase.kt | 25 ----------- .../domain/usecase/di/UseCaseModule.kt | 22 ++++------ .../ui/screen/favourite/FavouriteViewModel.kt | 4 +- .../screen/generation/GenerationViewModel.kt | 6 +-- .../ui/screen/pokemon/PokemonViewModel.kt | 10 ++--- .../ui/screen/setting/SettingsViewModel.kt | 15 +++---- .../ui/screen/type/TypeViewModel.kt | 6 +-- .../fake/usecase/FakeChangeThemeUseCase.kt | 8 ++-- .../fake/usecase/FakeSaveRemoteCryUseCase.kt | 10 ++--- .../usecase/FakeSaveRemoteImageUseCase.kt | 11 ++--- 20 files changed, 141 insertions(+), 204 deletions(-) create mode 100644 app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/BaseSuspendUseCase.kt delete mode 100644 app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/ParamsSuspendUseCase.kt delete mode 100644 app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/SuspendUseCase.kt delete mode 100644 app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/UseCase.kt diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/ChangeLightDarkThemeUseCase.kt b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/ChangeLightDarkThemeUseCase.kt index 210d30d..0f4e0db 100644 --- a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/ChangeLightDarkThemeUseCase.kt +++ b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/ChangeLightDarkThemeUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,10 +17,9 @@ package de.entikore.composedex.domain.usecase import de.entikore.composedex.domain.model.preferences.AppThemeConfig import de.entikore.composedex.domain.repository.AppSettingsRepository -import de.entikore.composedex.domain.usecase.base.ParamsSuspendUseCase +import de.entikore.composedex.domain.usecase.base.BaseSuspendUseCase import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import javax.inject.Inject /** @@ -28,11 +27,9 @@ import javax.inject.Inject */ class ChangeLightDarkThemeUseCase @Inject constructor( private val repository: AppSettingsRepository, - private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO -) : ParamsSuspendUseCase() { - override suspend operator fun invoke(params: AppThemeConfig) { - withContext(ioDispatcher) { - repository.setTheme(params) - } - } + dispatcher: CoroutineDispatcher = Dispatchers.IO +) : BaseSuspendUseCase(dispatcher) { + + override suspend fun execute(params: AppThemeConfig) = + repository.setTheme(params) } diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/ChangeTypeThemeUseCase.kt b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/ChangeTypeThemeUseCase.kt index 22a387f..cf36138 100644 --- a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/ChangeTypeThemeUseCase.kt +++ b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/ChangeTypeThemeUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,10 +18,9 @@ package de.entikore.composedex.domain.usecase import de.entikore.composedex.domain.model.pokemon.Pokemon import de.entikore.composedex.domain.model.preferences.TypeThemeConfig import de.entikore.composedex.domain.repository.AppSettingsRepository -import de.entikore.composedex.domain.usecase.base.ParamsSuspendUseCase +import de.entikore.composedex.domain.usecase.base.BaseSuspendUseCase import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import javax.inject.Inject /** @@ -29,11 +28,9 @@ import javax.inject.Inject */ class ChangeTypeThemeUseCase @Inject constructor( private val repository: AppSettingsRepository, - private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO -) : ParamsSuspendUseCase() { - override suspend operator fun invoke(params: String) { - withContext(ioDispatcher) { - repository.setTypeTheme(TypeThemeConfig.fromTypeString(params)) - } - } + dispatcher: CoroutineDispatcher = Dispatchers.IO +) : BaseSuspendUseCase(dispatcher) { + + override suspend fun execute(params: String) = + repository.setTypeTheme(TypeThemeConfig.fromTypeString(params)) } diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/DeleteLocalDataUseCase.kt b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/DeleteLocalDataUseCase.kt index 6ddfa21..b4acfae 100644 --- a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/DeleteLocalDataUseCase.kt +++ b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/DeleteLocalDataUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,10 +18,9 @@ package de.entikore.composedex.domain.usecase import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import de.entikore.composedex.domain.repository.LocalStorage -import de.entikore.composedex.domain.usecase.base.SuspendUseCase +import de.entikore.composedex.domain.usecase.base.BaseSuspendUseCase import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext /** * This use case deletes all stored data from the local storage. @@ -29,16 +28,14 @@ import kotlinx.coroutines.withContext class DeleteLocalDataUseCase( @ApplicationContext private val context: Context, private val localStorage: LocalStorage, - private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO -) : SuspendUseCase() { + dispatcher: CoroutineDispatcher = Dispatchers.IO +) : BaseSuspendUseCase(dispatcher) { - override suspend operator fun invoke() { - withContext(ioDispatcher) { - val files = context.fileList() - for (file in files) { - context.deleteFile(file) - } - localStorage.clearData() + override suspend fun execute(params: Unit) { + val files = context.fileList() + for (file in files) { + context.deleteFile(file) } + localStorage.clearData() } } diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetUserPreferencesUseCase.kt b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetUserPreferencesUseCase.kt index d6fae6f..cd294d2 100644 --- a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetUserPreferencesUseCase.kt +++ b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/GetUserPreferencesUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package de.entikore.composedex.domain.usecase import de.entikore.composedex.domain.model.preferences.UserPreferences import de.entikore.composedex.domain.repository.AppSettingsRepository -import de.entikore.composedex.domain.usecase.base.UseCase import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import javax.inject.Inject @@ -27,7 +26,7 @@ import javax.inject.Inject */ class GetUserPreferencesUseCase @Inject constructor( private val repository: AppSettingsRepository -) : UseCase>() { - - override operator fun invoke() = repository.getUserPreferences().distinctUntilChanged() +) { + operator fun invoke(): Flow = + repository.getUserPreferences().distinctUntilChanged() } diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/SaveRemoteImageUseCase.kt b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/SaveRemoteImageUseCase.kt index 8ef533a..8665d16 100644 --- a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/SaveRemoteImageUseCase.kt +++ b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/SaveRemoteImageUseCase.kt @@ -20,7 +20,7 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import dagger.hilt.android.qualifiers.ApplicationContext import de.entikore.composedex.domain.repository.PokemonRepository -import de.entikore.composedex.domain.usecase.base.ParamsSuspendUseCase +import de.entikore.composedex.domain.usecase.base.BaseSuspendUseCase import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -41,28 +41,24 @@ class SaveRemoteImageUseCase @Inject constructor( @ApplicationContext private val context: Context, private val pokemonRepository: PokemonRepository, httpClientBuilder: OkHttpClient.Builder, - private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO -) : ParamsSuspendUseCase() { + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) : BaseSuspendUseCase(dispatcher) { private var client: OkHttpClient = httpClientBuilder.build() - - override suspend operator fun invoke( - params: SaveImageData - ): String = - withContext(ioDispatcher) { - val bitmap = downloadImage(params.imageAddress) - return@withContext if (bitmap == null) { - params.imageAddress - } else { - saveImage(params.id, params.fileName, bitmap, params.isSprite) - } + override suspend fun execute(params: SaveImageData): String { + val bitmap = downloadImage(params.imageAddress) + return if (bitmap == null) { + params.imageAddress + } else { + saveImage(params.id, params.fileName, bitmap, params.isSprite) } + } private suspend fun downloadImage(imageAddress: String): Bitmap? { if (imageAddress.isEmpty()) return null val request: Request = Request.Builder().url(imageAddress).build() - return withContext(ioDispatcher) { + return withContext(dispatcher) { try { client.newCall(request).execute().use { response -> if (!response.isSuccessful) { @@ -99,10 +95,10 @@ class SaveRemoteImageUseCase @Inject constructor( } private suspend fun saveImage(id: Int, fileName: String, bitmap: Bitmap?, isSprite: Boolean) = - withContext(ioDispatcher) { + withContext(dispatcher) { val fos = context.openFileOutput(fileName, Context.MODE_PRIVATE) bitmap?.compress(Bitmap.CompressFormat.PNG, COMPRESSION_QUALITY_PERCENT, fos) - withContext(Dispatchers.IO) { fos.close() } + withContext(dispatcher) { fos.close() } val fileLocation = "${context.filesDir}/$fileName" if (isSprite) { pokemonRepository.updatePokemonSprite(id, fileLocation) diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/SaveRemoteSoundUseCase.kt b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/SaveRemoteSoundUseCase.kt index 7b20dfc..14ccde8 100644 --- a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/SaveRemoteSoundUseCase.kt +++ b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/SaveRemoteSoundUseCase.kt @@ -18,7 +18,7 @@ package de.entikore.composedex.domain.usecase import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import de.entikore.composedex.domain.repository.PokemonRepository -import de.entikore.composedex.domain.usecase.base.ParamsSuspendUseCase +import de.entikore.composedex.domain.usecase.base.BaseSuspendUseCase import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -39,22 +39,21 @@ class SaveRemoteSoundUseCase @Inject constructor( @ApplicationContext private val context: Context, private val pokemonRepository: PokemonRepository, httpClientBuilder: OkHttpClient.Builder, - private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO -) : ParamsSuspendUseCase() { + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) : BaseSuspendUseCase(dispatcher) { private var client: OkHttpClient = httpClientBuilder.build() + override suspend fun execute(params: SaveSoundData): String = + downloadAndSaveSound(params.soundAddress, params.fileName, params.id) + ?: params.soundAddress - override suspend operator fun invoke( - params: SaveSoundData - ): String = - withContext(ioDispatcher) { - return@withContext downloadAndSaveSound(params.soundAddress, params.fileName, params.id) - ?: params.soundAddress - } - - private suspend fun downloadAndSaveSound(soundAddress: String, dataName: String, id: Int): String? { + private suspend fun downloadAndSaveSound( + soundAddress: String, + dataName: String, + id: Int + ): String? { val request = Request.Builder().url(soundAddress).build() - return withContext(ioDispatcher) { + return withContext(dispatcher) { try { client.newCall(request).execute().use { response -> if (!response.isSuccessful) { diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/SetAsFavouriteUseCase.kt b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/SetAsFavouriteUseCase.kt index 57c54e9..6680b08 100644 --- a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/SetAsFavouriteUseCase.kt +++ b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/SetAsFavouriteUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,9 @@ package de.entikore.composedex.domain.usecase import de.entikore.composedex.domain.repository.FavouriteRepository -import de.entikore.composedex.domain.usecase.base.ParamsSuspendUseCase +import de.entikore.composedex.domain.usecase.base.BaseSuspendUseCase import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import javax.inject.Inject /** @@ -27,11 +26,10 @@ import javax.inject.Inject */ class SetAsFavouriteUseCase @Inject constructor( private val repository: FavouriteRepository, - private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO -) : ParamsSuspendUseCase<@JvmSuppressWildcards SetFavouriteData, @JvmSuppressWildcards Unit>() { - - override suspend operator fun invoke(params: SetFavouriteData) = - withContext(ioDispatcher) { repository.updateIsFavourite(params.id, params.isFavourite) } + dispatcher: CoroutineDispatcher = Dispatchers.IO +) : BaseSuspendUseCase<@JvmSuppressWildcards SetFavouriteData, @JvmSuppressWildcards Unit>(dispatcher) { + override suspend fun execute(params: @JvmSuppressWildcards SetFavouriteData) = + repository.updateIsFavourite(params.id, params.isFavourite) } /** diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/BaseSuspendUseCase.kt b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/BaseSuspendUseCase.kt new file mode 100644 index 0000000..966b125 --- /dev/null +++ b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/BaseSuspendUseCase.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2025 Entikore + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.entikore.composedex.domain.usecase.base + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * Base class for Use Cases that do background work. + * It standardizes use case execution by automatically: + * - Running the core logic on a specified [CoroutineDispatcher]. + * + * This class is designed to be implemented by concrete use cases. + */ +abstract class BaseSuspendUseCase( + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + + protected abstract suspend fun execute(params: P): R + + suspend operator fun invoke(params: P): R = withContext(dispatcher) { + return@withContext execute(params) + } + + suspend operator fun invoke(): R { + @Suppress("UNCHECKED_CAST") + return invoke(Unit as P) + } +} diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/ParamsSuspendUseCase.kt b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/ParamsSuspendUseCase.kt deleted file mode 100644 index 70595b7..0000000 --- a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/ParamsSuspendUseCase.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2024 Entikore - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package de.entikore.composedex.domain.usecase.base - -/** - * Represents a generic use case that accepts parameters and uses coroutines for asynchronous - * operations. - * - * @param P The type of parameters accepted by the use case. - * @param T The type of result returned by the use case. - */ -abstract class ParamsSuspendUseCase { - abstract suspend operator fun invoke(params: P): T -} diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/SuspendUseCase.kt b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/SuspendUseCase.kt deleted file mode 100644 index 06d73f9..0000000 --- a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/SuspendUseCase.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2024 Entikore - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package de.entikore.composedex.domain.usecase.base - -/** - * Represents a generic use case that uses coroutines for asynchronous operations. - * - * @param T The type of result returned by the use case. - */ -abstract class SuspendUseCase { - abstract suspend operator fun invoke(): T -} diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/UseCase.kt b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/UseCase.kt deleted file mode 100644 index b2b3853..0000000 --- a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/base/UseCase.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2024 Entikore - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package de.entikore.composedex.domain.usecase.base - -/** - * Represents a generic use case. - * - * @param T The type of result returned by the use case. - */ -abstract class UseCase { - abstract operator fun invoke(): T -} diff --git a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/di/UseCaseModule.kt b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/di/UseCaseModule.kt index 01d58dc..fb3695c 100644 --- a/app/src/main/kotlin/de/entikore/composedex/domain/usecase/di/UseCaseModule.kt +++ b/app/src/main/kotlin/de/entikore/composedex/domain/usecase/di/UseCaseModule.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,6 @@ import dagger.hilt.InstallIn import dagger.hilt.android.components.ViewModelComponent import dagger.hilt.android.qualifiers.ApplicationContext import de.entikore.composedex.domain.model.preferences.AppThemeConfig -import de.entikore.composedex.domain.model.preferences.UserPreferences import de.entikore.composedex.domain.repository.AppSettingsRepository import de.entikore.composedex.domain.repository.FavouriteRepository import de.entikore.composedex.domain.repository.LocalStorage @@ -37,10 +36,7 @@ import de.entikore.composedex.domain.usecase.SaveRemoteSoundUseCase import de.entikore.composedex.domain.usecase.SaveSoundData import de.entikore.composedex.domain.usecase.SetAsFavouriteUseCase import de.entikore.composedex.domain.usecase.SetFavouriteData -import de.entikore.composedex.domain.usecase.base.ParamsSuspendUseCase -import de.entikore.composedex.domain.usecase.base.SuspendUseCase -import de.entikore.composedex.domain.usecase.base.UseCase -import kotlinx.coroutines.flow.Flow +import de.entikore.composedex.domain.usecase.base.BaseSuspendUseCase import okhttp3.OkHttpClient @Module @@ -52,7 +48,7 @@ object UseCaseModule { @ApplicationContext context: Context, repository: PokemonRepository, okHttpClientBuilder: OkHttpClient.Builder - ): ParamsSuspendUseCase = + ): BaseSuspendUseCase = SaveRemoteImageUseCase(context, repository, okHttpClientBuilder) @Provides @@ -60,30 +56,30 @@ object UseCaseModule { @ApplicationContext context: Context, repository: PokemonRepository, okHttpClientBuilder: OkHttpClient.Builder - ): ParamsSuspendUseCase = + ): BaseSuspendUseCase = SaveRemoteSoundUseCase(context, repository, okHttpClientBuilder) @Provides - fun provideSetAsFavouriteUseCase(repository: FavouriteRepository): ParamsSuspendUseCase = + fun provideSetAsFavouriteUseCase(repository: FavouriteRepository): BaseSuspendUseCase = SetAsFavouriteUseCase(repository) @Provides fun provideDeleteLocalDataUseCase( @ApplicationContext context: Context, composeDexDatabase: LocalStorage - ): SuspendUseCase = DeleteLocalDataUseCase(context, composeDexDatabase) + ): BaseSuspendUseCase = DeleteLocalDataUseCase(context, composeDexDatabase) @Provides - fun provideGetUserPreferencesUseCase(repository: AppSettingsRepository): UseCase> = + fun provideGetUserPreferencesUseCase(repository: AppSettingsRepository) = GetUserPreferencesUseCase(repository) @Provides fun provideChangeLightDarkThemeUseCase( repository: AppSettingsRepository - ): ParamsSuspendUseCase = + ): BaseSuspendUseCase = ChangeLightDarkThemeUseCase(repository) @Provides - fun provideChangeTypeThemeUseCase(repository: AppSettingsRepository): ParamsSuspendUseCase = + fun provideChangeTypeThemeUseCase(repository: AppSettingsRepository): BaseSuspendUseCase = ChangeTypeThemeUseCase(repository) } diff --git a/app/src/main/kotlin/de/entikore/composedex/ui/screen/favourite/FavouriteViewModel.kt b/app/src/main/kotlin/de/entikore/composedex/ui/screen/favourite/FavouriteViewModel.kt index 0b96216..738291f 100644 --- a/app/src/main/kotlin/de/entikore/composedex/ui/screen/favourite/FavouriteViewModel.kt +++ b/app/src/main/kotlin/de/entikore/composedex/ui/screen/favourite/FavouriteViewModel.kt @@ -20,7 +20,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import de.entikore.composedex.domain.model.pokemon.Pokemon import de.entikore.composedex.domain.usecase.FetchFavouritesUseCase import de.entikore.composedex.domain.usecase.SetFavouriteData -import de.entikore.composedex.domain.usecase.base.ParamsSuspendUseCase +import de.entikore.composedex.domain.usecase.base.BaseSuspendUseCase import de.entikore.composedex.ui.screen.shared.PokemonFilterOptions import de.entikore.composedex.ui.screen.shared.PokemonFilterViewModel import de.entikore.composedex.ui.screen.shared.PokemonUiState @@ -38,7 +38,7 @@ import javax.inject.Inject @HiltViewModel class FavouriteViewModel @Inject constructor( getFavourites: FetchFavouritesUseCase, - private val setAsFavouriteUseCase: @JvmSuppressWildcards ParamsSuspendUseCase + private val setAsFavouriteUseCase: @JvmSuppressWildcards BaseSuspendUseCase ) : PokemonFilterViewModel() { private val _isUpdatingFavourite = MutableStateFlow(false) diff --git a/app/src/main/kotlin/de/entikore/composedex/ui/screen/generation/GenerationViewModel.kt b/app/src/main/kotlin/de/entikore/composedex/ui/screen/generation/GenerationViewModel.kt index 8b62e2f..cde6099 100644 --- a/app/src/main/kotlin/de/entikore/composedex/ui/screen/generation/GenerationViewModel.kt +++ b/app/src/main/kotlin/de/entikore/composedex/ui/screen/generation/GenerationViewModel.kt @@ -24,7 +24,7 @@ import de.entikore.composedex.domain.usecase.FetchGenerationsUseCase import de.entikore.composedex.domain.usecase.FetchPokemonOfGenerationUseCase import de.entikore.composedex.domain.usecase.SaveImageData import de.entikore.composedex.domain.usecase.SetFavouriteData -import de.entikore.composedex.domain.usecase.base.ParamsSuspendUseCase +import de.entikore.composedex.domain.usecase.base.BaseSuspendUseCase import de.entikore.composedex.ui.screen.shared.PokemonFilterOptions import de.entikore.composedex.ui.screen.shared.PokemonFilterViewModel import de.entikore.composedex.ui.screen.shared.PokemonUiState @@ -50,8 +50,8 @@ class GenerationViewModel @Inject constructor( getGenerationsUseCase: FetchGenerationsUseCase, getGenerationUseCase: FetchGenerationUseCase, getPokemonOfGenerationUseCase: FetchPokemonOfGenerationUseCase, - private val saveRemoteImageUseCase: @JvmSuppressWildcards ParamsSuspendUseCase, - private val setAsFavouriteUseCase: @JvmSuppressWildcards ParamsSuspendUseCase + private val saveRemoteImageUseCase: @JvmSuppressWildcards BaseSuspendUseCase, + private val setAsFavouriteUseCase: @JvmSuppressWildcards BaseSuspendUseCase ) : PokemonFilterViewModel() { private val _selectedGenerationFlow = MutableStateFlow(null) diff --git a/app/src/main/kotlin/de/entikore/composedex/ui/screen/pokemon/PokemonViewModel.kt b/app/src/main/kotlin/de/entikore/composedex/ui/screen/pokemon/PokemonViewModel.kt index d030433..abde62e 100644 --- a/app/src/main/kotlin/de/entikore/composedex/ui/screen/pokemon/PokemonViewModel.kt +++ b/app/src/main/kotlin/de/entikore/composedex/ui/screen/pokemon/PokemonViewModel.kt @@ -28,7 +28,7 @@ import de.entikore.composedex.domain.usecase.FetchPokemonUseCase import de.entikore.composedex.domain.usecase.SaveImageData import de.entikore.composedex.domain.usecase.SaveSoundData import de.entikore.composedex.domain.usecase.SetFavouriteData -import de.entikore.composedex.domain.usecase.base.ParamsSuspendUseCase +import de.entikore.composedex.domain.usecase.base.BaseSuspendUseCase import de.entikore.composedex.ui.ComposeDexTTS import de.entikore.composedex.ui.screen.util.SUFFIX_ARTWORK import de.entikore.composedex.ui.screen.util.SUFFIX_CRY @@ -54,10 +54,10 @@ import javax.inject.Inject @HiltViewModel class PokemonViewModel @Inject constructor( private val getPokemonUseCase: FetchPokemonUseCase, - private val saveRemoteImageUseCase: @JvmSuppressWildcards ParamsSuspendUseCase, - private val saveRemoteCryUseCase: @JvmSuppressWildcards ParamsSuspendUseCase, - private val setAsFavouriteUseCase: @JvmSuppressWildcards ParamsSuspendUseCase, - private val changeThemeUseCase: @JvmSuppressWildcards ParamsSuspendUseCase, + private val saveRemoteImageUseCase: @JvmSuppressWildcards BaseSuspendUseCase, + private val saveRemoteCryUseCase: @JvmSuppressWildcards BaseSuspendUseCase, + private val setAsFavouriteUseCase: @JvmSuppressWildcards BaseSuspendUseCase, + private val changeThemeUseCase: @JvmSuppressWildcards BaseSuspendUseCase, private val exoPlayer: ExoPlayer, private val tts: ComposeDexTTS ) : ViewModel() { diff --git a/app/src/main/kotlin/de/entikore/composedex/ui/screen/setting/SettingsViewModel.kt b/app/src/main/kotlin/de/entikore/composedex/ui/screen/setting/SettingsViewModel.kt index bb7093c..f3f69e7 100644 --- a/app/src/main/kotlin/de/entikore/composedex/ui/screen/setting/SettingsViewModel.kt +++ b/app/src/main/kotlin/de/entikore/composedex/ui/screen/setting/SettingsViewModel.kt @@ -19,11 +19,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import de.entikore.composedex.domain.model.preferences.AppThemeConfig -import de.entikore.composedex.domain.model.preferences.UserPreferences -import de.entikore.composedex.domain.usecase.base.ParamsSuspendUseCase -import de.entikore.composedex.domain.usecase.base.SuspendUseCase -import de.entikore.composedex.domain.usecase.base.UseCase -import kotlinx.coroutines.flow.Flow +import de.entikore.composedex.domain.usecase.GetUserPreferencesUseCase +import de.entikore.composedex.domain.usecase.base.BaseSuspendUseCase import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -36,12 +33,12 @@ import javax.inject.Inject */ @HiltViewModel class SettingsViewModel @Inject constructor( - getSettingsUseCase: @JvmSuppressWildcards UseCase>, - private val changeLightDarkThemeUseCase: @JvmSuppressWildcards ParamsSuspendUseCase, - private val deleteLocalData: @JvmSuppressWildcards SuspendUseCase + userPreferencesUseCase: GetUserPreferencesUseCase, + private val changeLightDarkThemeUseCase: @JvmSuppressWildcards BaseSuspendUseCase, + private val deleteLocalData: @JvmSuppressWildcards BaseSuspendUseCase ) : ViewModel() { - val screenState = getSettingsUseCase.invoke().map { + val screenState = userPreferencesUseCase.invoke().map { SettingScreenUiState(selected = it.appThemeConfig.ordinal) }.stateIn( viewModelScope, diff --git a/app/src/main/kotlin/de/entikore/composedex/ui/screen/type/TypeViewModel.kt b/app/src/main/kotlin/de/entikore/composedex/ui/screen/type/TypeViewModel.kt index 91bf27c..d421608 100644 --- a/app/src/main/kotlin/de/entikore/composedex/ui/screen/type/TypeViewModel.kt +++ b/app/src/main/kotlin/de/entikore/composedex/ui/screen/type/TypeViewModel.kt @@ -24,7 +24,7 @@ import de.entikore.composedex.domain.usecase.FetchTypeUseCase import de.entikore.composedex.domain.usecase.FetchTypesUseCase import de.entikore.composedex.domain.usecase.SaveImageData import de.entikore.composedex.domain.usecase.SetFavouriteData -import de.entikore.composedex.domain.usecase.base.ParamsSuspendUseCase +import de.entikore.composedex.domain.usecase.base.BaseSuspendUseCase import de.entikore.composedex.ui.screen.shared.PokemonFilterOptions import de.entikore.composedex.ui.screen.shared.PokemonFilterViewModel import de.entikore.composedex.ui.screen.shared.PokemonUiState @@ -54,8 +54,8 @@ class TypeViewModel @Inject constructor( getTypesUseCase: FetchTypesUseCase, private val getTypeUseCase: FetchTypeUseCase, private val getPokemonOfTypeUseCase: FetchPokemonOfTypeUseCase, - private val saveRemoteImageUseCase: @JvmSuppressWildcards ParamsSuspendUseCase, - private val setAsFavouriteUseCase: @JvmSuppressWildcards ParamsSuspendUseCase + private val saveRemoteImageUseCase: @JvmSuppressWildcards BaseSuspendUseCase, + private val setAsFavouriteUseCase: @JvmSuppressWildcards BaseSuspendUseCase ) : PokemonFilterViewModel() { private val _selectedTypeFlow = MutableStateFlow("") diff --git a/app/src/test/kotlin/de/entikore/composedex/fake/usecase/FakeChangeThemeUseCase.kt b/app/src/test/kotlin/de/entikore/composedex/fake/usecase/FakeChangeThemeUseCase.kt index 72aab22..5198939 100644 --- a/app/src/test/kotlin/de/entikore/composedex/fake/usecase/FakeChangeThemeUseCase.kt +++ b/app/src/test/kotlin/de/entikore/composedex/fake/usecase/FakeChangeThemeUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,10 @@ */ package de.entikore.composedex.fake.usecase -import de.entikore.composedex.domain.usecase.base.ParamsSuspendUseCase +import de.entikore.composedex.domain.usecase.base.BaseSuspendUseCase -class FakeChangeThemeUseCase : ParamsSuspendUseCase() { - override suspend fun invoke(params: String) { +class FakeChangeThemeUseCase : BaseSuspendUseCase() { + override suspend fun execute(params: String) { println("Changes theme to $params") } } diff --git a/app/src/test/kotlin/de/entikore/composedex/fake/usecase/FakeSaveRemoteCryUseCase.kt b/app/src/test/kotlin/de/entikore/composedex/fake/usecase/FakeSaveRemoteCryUseCase.kt index 24ef84b..050d19e 100644 --- a/app/src/test/kotlin/de/entikore/composedex/fake/usecase/FakeSaveRemoteCryUseCase.kt +++ b/app/src/test/kotlin/de/entikore/composedex/fake/usecase/FakeSaveRemoteCryUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,8 @@ package de.entikore.composedex.fake.usecase import de.entikore.composedex.domain.usecase.SaveSoundData -import de.entikore.composedex.domain.usecase.base.ParamsSuspendUseCase +import de.entikore.composedex.domain.usecase.base.BaseSuspendUseCase -class FakeSaveRemoteCryUseCase : ParamsSuspendUseCase() { - override suspend fun invoke(params: SaveSoundData): String { - return params.soundAddress - } +class FakeSaveRemoteCryUseCase : BaseSuspendUseCase() { + override suspend fun execute(params: SaveSoundData): String = params.soundAddress } diff --git a/app/src/test/kotlin/de/entikore/composedex/fake/usecase/FakeSaveRemoteImageUseCase.kt b/app/src/test/kotlin/de/entikore/composedex/fake/usecase/FakeSaveRemoteImageUseCase.kt index a020a33..63265de 100644 --- a/app/src/test/kotlin/de/entikore/composedex/fake/usecase/FakeSaveRemoteImageUseCase.kt +++ b/app/src/test/kotlin/de/entikore/composedex/fake/usecase/FakeSaveRemoteImageUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Entikore + * Copyright 2025 Entikore * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,8 @@ package de.entikore.composedex.fake.usecase import de.entikore.composedex.domain.usecase.SaveImageData -import de.entikore.composedex.domain.usecase.base.ParamsSuspendUseCase +import de.entikore.composedex.domain.usecase.base.BaseSuspendUseCase -class FakeSaveRemoteImageUseCase : ParamsSuspendUseCase() { - - override suspend fun invoke(params: SaveImageData): String { - return params.imageAddress - } +class FakeSaveRemoteImageUseCase : BaseSuspendUseCase() { + override suspend fun execute(params: SaveImageData): String = params.imageAddress }