diff --git a/composeApp/src/commonMain/kotlin/App.kt b/composeApp/src/commonMain/kotlin/App.kt index cbe5dd85..90ad4e7a 100644 --- a/composeApp/src/commonMain/kotlin/App.kt +++ b/composeApp/src/commonMain/kotlin/App.kt @@ -3,6 +3,7 @@ import com.stslex.core.core.coreModule import com.stslex.core.database.di.coreDatabaseModule import com.stslex.core.database.di.userSettingsModule import com.stslex.core.network.di.coreNetworkModule +import com.stslex.core.ui.di.coreUiModule import com.stslex.core.ui.theme.AppTheme import com.stslex.feature.auth.di.featureAuthModule import com.stslex.feature.favourite.di.featureFavouriteModule @@ -38,6 +39,7 @@ private fun KoinApplication.setupCommonModules() { listOf( appModule, coreModule, + coreUiModule, coreNetworkModule, userSettingsModule, coreDatabaseModule, diff --git a/core/core/src/commonMain/kotlin/com/stslex/core/core/coroutine/AppCoroutineScope.kt b/core/core/src/commonMain/kotlin/com/stslex/core/core/coroutine/AppCoroutineScope.kt index c828c174..bacbffa6 100644 --- a/core/core/src/commonMain/kotlin/com/stslex/core/core/coroutine/AppCoroutineScope.kt +++ b/core/core/src/commonMain/kotlin/com/stslex/core/core/coroutine/AppCoroutineScope.kt @@ -2,6 +2,7 @@ package com.stslex.core.core.coroutine import com.stslex.core.core.AppDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow @@ -17,6 +18,7 @@ interface AppCoroutineScope { * @see AppDispatcher * */ fun launch( + start: CoroutineStart = CoroutineStart.DEFAULT, onError: suspend (Throwable) -> Unit = {}, onSuccess: suspend CoroutineScope.(T) -> Unit = {}, action: suspend CoroutineScope.() -> T, diff --git a/core/core/src/commonMain/kotlin/com/stslex/core/core/coroutine/AppCoroutineScopeImpl.kt b/core/core/src/commonMain/kotlin/com/stslex/core/core/coroutine/AppCoroutineScopeImpl.kt index 499e1507..38956cf4 100644 --- a/core/core/src/commonMain/kotlin/com/stslex/core/core/coroutine/AppCoroutineScopeImpl.kt +++ b/core/core/src/commonMain/kotlin/com/stslex/core/core/coroutine/AppCoroutineScopeImpl.kt @@ -5,6 +5,7 @@ import com.stslex.core.core.Logger import com.stslex.core.core.coroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch @@ -24,10 +25,12 @@ class AppCoroutineScopeImpl( } override fun launch( + start: CoroutineStart, onError: suspend (Throwable) -> Unit, onSuccess: suspend CoroutineScope.(T) -> Unit, action: suspend CoroutineScope.() -> T ): Job = scope.launch( + start = start, context = exceptionHandler(onError) + appDispatcher.default, block = { onSuccess(action()) diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/di/NetworkModule.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/di/NetworkModule.kt index 4cf256b6..37d7f8db 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/di/NetworkModule.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/di/NetworkModule.kt @@ -18,8 +18,6 @@ import com.stslex.core.network.clients.match.client.MatchClient import com.stslex.core.network.clients.match.client.MockMatchClientImpl import com.stslex.core.network.clients.profile.client.ProfileClient import com.stslex.core.network.clients.profile.client.ProfileClientImpl -import com.stslex.core.network.utils.PagingWorker -import com.stslex.core.network.utils.PagingWorkerImpl import com.stslex.core.network.utils.token.AuthController import com.stslex.core.network.utils.token.AuthControllerImpl import org.koin.dsl.module @@ -81,8 +79,4 @@ val coreNetworkModule = module { userStore = get(), ) } - - factory { - PagingWorkerImpl() - } } \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/utils/PagingWorker.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/utils/PagingWorker.kt deleted file mode 100644 index 5033b37a..00000000 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/utils/PagingWorker.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.stslex.core.network.utils - -interface PagingWorker { - - suspend operator fun invoke( - request: suspend () -> Unit - ) - - suspend fun cancel() -} - diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/utils/PagingWorkerImpl.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/utils/PagingWorkerImpl.kt deleted file mode 100644 index 454b9a88..00000000 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/utils/PagingWorkerImpl.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.stslex.core.network.utils - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import kotlin.coroutines.coroutineContext - -class PagingWorkerImpl : PagingWorker { - - private var job: Job? = null - private var nextPageJob: Job? = null - private var lastRequestTime = 0L - - override suspend fun invoke( - request: suspend () -> Unit - ) { - if (lastRequestTime + REQUEST_DELAY > currentTimeMs) { - nextPageJob = startRequest( - request = request, - start = CoroutineStart.LAZY - ) - } - startRequest(request = request) - } - - override suspend fun cancel() { - job?.cancel() - nextPageJob?.cancel() - } - - private suspend fun startRequest( - request: suspend () -> Unit, - start: CoroutineStart = CoroutineStart.DEFAULT, - ): Job = CoroutineScope(coroutineContext) - .launch( - start = start - ) { - job = nextPageJob - nextPageJob = null - request() - }.apply { - invokeOnCompletion { - nextPageJob?.start() - } - } - - companion object { - private const val REQUEST_DELAY = 500L - } -} \ No newline at end of file diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/di/CoreUiModule.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/di/CoreUiModule.kt new file mode 100644 index 00000000..091e8adc --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/di/CoreUiModule.kt @@ -0,0 +1,11 @@ +package com.stslex.core.ui.di + +import com.stslex.core.ui.pager.pager.StorePagerFactory +import com.stslex.core.ui.pager.pager.StorePagerFactoryImpl +import org.koin.dsl.module + +val coreUiModule = module { + factory { + StorePagerFactoryImpl() + } +} \ No newline at end of file diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/StorePager.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/pager/StorePager.kt similarity index 69% rename from core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/StorePager.kt rename to core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/pager/StorePager.kt index d99b07c5..77ebc854 100644 --- a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/StorePager.kt +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/pager/StorePager.kt @@ -1,7 +1,9 @@ -package com.stslex.core.ui.pager +package com.stslex.core.ui.pager.pager import com.stslex.core.ui.base.paging.PagingItem import com.stslex.core.ui.base.paging.PagingState +import com.stslex.core.ui.pager.states.PagerLoadEvents +import com.stslex.core.ui.pager.states.PagerLoadState import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow @@ -17,7 +19,7 @@ interface StorePager { fun load() - fun refresh() + fun refresh(isForceLoad: Boolean) fun retry() } \ No newline at end of file diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/pager/StorePagerFactory.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/pager/StorePagerFactory.kt new file mode 100644 index 00000000..4abedc98 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/pager/StorePagerFactory.kt @@ -0,0 +1,18 @@ +package com.stslex.core.ui.pager.pager + +import com.stslex.core.core.coroutine.AppCoroutineScope +import com.stslex.core.core.paging.PagingCoreData +import com.stslex.core.core.paging.PagingCoreItem +import com.stslex.core.core.paging.PagingResponse +import com.stslex.core.ui.base.paging.PagingItem +import com.stslex.core.ui.pager.utils.PagingMapper + +interface StorePagerFactory { + + fun create( + scope: AppCoroutineScope, + request: suspend (page: Int, pageSize: Int) -> PagingResponse, + mapper: PagingMapper, + pageSize: Int = PagingCoreData.DEFAULT_PAGE_SIZE, + ): StorePager +} \ No newline at end of file diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/pager/StorePagerFactoryImpl.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/pager/StorePagerFactoryImpl.kt new file mode 100644 index 00000000..7f7ecd92 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/pager/StorePagerFactoryImpl.kt @@ -0,0 +1,25 @@ +package com.stslex.core.ui.pager.pager + +import com.stslex.core.core.coroutine.AppCoroutineScope +import com.stslex.core.core.paging.PagingCoreItem +import com.stslex.core.core.paging.PagingResponse +import com.stslex.core.ui.base.paging.PagingItem +import com.stslex.core.ui.pager.utils.PagingMapper +import com.stslex.core.ui.pager.utils.PagingWorkerImpl + +class StorePagerFactoryImpl : StorePagerFactory { + + override fun create( + scope: AppCoroutineScope, + request: suspend (page: Int, pageSize: Int) -> PagingResponse, + mapper: PagingMapper, + pageSize: Int + ): StorePager { + return StorePagerImpl( + pagingWorker = PagingWorkerImpl(scope = scope), + request = request, + mapper = mapper, + pageSize = pageSize + ) + } +} \ No newline at end of file diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/StorePagerImpl.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/pager/StorePagerImpl.kt similarity index 89% rename from core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/StorePagerImpl.kt rename to core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/pager/StorePagerImpl.kt index eaccb711..06527706 100644 --- a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/StorePagerImpl.kt +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/pager/StorePagerImpl.kt @@ -1,6 +1,5 @@ -package com.stslex.core.ui.pager +package com.stslex.core.ui.pager.pager -import com.stslex.core.core.coroutine.AppCoroutineScope import com.stslex.core.core.paging.PagingCoreData import com.stslex.core.core.paging.PagingCoreData.Companion.DEFAULT_PAGE import com.stslex.core.core.paging.PagingCoreItem @@ -9,6 +8,10 @@ import com.stslex.core.ui.base.mapToAppError import com.stslex.core.ui.base.paging.PagingItem import com.stslex.core.ui.base.paging.PagingState import com.stslex.core.ui.base.paging.pagingMap +import com.stslex.core.ui.pager.states.PagerLoadEvents +import com.stslex.core.ui.pager.states.PagerLoadState +import com.stslex.core.ui.pager.utils.PagingMapper +import com.stslex.core.ui.pager.utils.PagingWorker import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableSharedFlow @@ -18,8 +21,8 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update class StorePagerImpl( + private val pagingWorker: PagingWorker, private val request: suspend (page: Int, pageSize: Int) -> PagingResponse, - private val scope: AppCoroutineScope, private val mapper: PagingMapper, pageSize: Int = PagingCoreData.DEFAULT_PAGE_SIZE, ) : StorePager { @@ -57,14 +60,14 @@ class StorePagerImpl( requestItems(isForceLoad = false) } - override fun refresh() { + override fun refresh(isForceLoad: Boolean) { _loadState.value = PagerLoadState.Refresh _state.update { currentState -> currentState.copy( - page = DEFAULT_PAGE + page = DEFAULT_PAGE, ) } - requestItems(isForceLoad = true) + requestItems(isForceLoad = isForceLoad) } override fun retry() { @@ -85,7 +88,7 @@ class StorePagerImpl( return } loadJob?.cancel() - loadJob = scope.launch( + loadJob = pagingWorker.launch( action = { val page = state.value.page val pageSize = state.value.pageSize diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/PagerLoadEvents.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/states/PagerLoadEvents.kt similarity index 77% rename from core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/PagerLoadEvents.kt rename to core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/states/PagerLoadEvents.kt index 1382c0c9..6aaacce6 100644 --- a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/PagerLoadEvents.kt +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/states/PagerLoadEvents.kt @@ -1,4 +1,4 @@ -package com.stslex.core.ui.pager +package com.stslex.core.ui.pager.states import com.stslex.core.ui.base.AppError diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/PagerLoadState.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/states/PagerLoadState.kt similarity index 90% rename from core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/PagerLoadState.kt rename to core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/states/PagerLoadState.kt index 3bdacd36..88e37571 100644 --- a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/PagerLoadState.kt +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/states/PagerLoadState.kt @@ -1,4 +1,4 @@ -package com.stslex.core.ui.pager +package com.stslex.core.ui.pager.states import com.stslex.core.ui.base.AppError diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/PagingMapper.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/utils/PagingMapper.kt similarity index 84% rename from core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/PagingMapper.kt rename to core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/utils/PagingMapper.kt index bed3780d..8cd00072 100644 --- a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/PagingMapper.kt +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/utils/PagingMapper.kt @@ -1,4 +1,4 @@ -package com.stslex.core.ui.pager +package com.stslex.core.ui.pager.utils import com.stslex.core.core.paging.PagingCoreItem import com.stslex.core.ui.base.paging.PagingItem diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/utils/PagingWorker.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/utils/PagingWorker.kt new file mode 100644 index 00000000..13c62f99 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/utils/PagingWorker.kt @@ -0,0 +1,18 @@ +package com.stslex.core.ui.pager.utils + +import com.stslex.core.core.paging.PagingCoreItem +import com.stslex.core.core.paging.PagingResponse +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job + +interface PagingWorker { + + fun launch( + onError: suspend (Throwable) -> Unit = {}, + onSuccess: suspend CoroutineScope.(PagingResponse) -> Unit = {}, + action: suspend CoroutineScope.() -> PagingResponse, + ): Job + + fun cancel() +} + diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/utils/PagingWorkerImpl.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/utils/PagingWorkerImpl.kt new file mode 100644 index 00000000..cc8a9068 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/utils/PagingWorkerImpl.kt @@ -0,0 +1,69 @@ +package com.stslex.core.ui.pager.utils + +import com.stslex.core.core.coroutine.AppCoroutineScope +import com.stslex.core.core.paging.PagingCoreItem +import com.stslex.core.core.paging.PagingResponse +import com.stslex.core.network.utils.currentTimeMs +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Job + +class PagingWorkerImpl( + private val scope: AppCoroutineScope +) : PagingWorker { + + private var job: Job? = null + private var nextPageJob: Job? = null + private var lastRequestTime = 0L + + override fun launch( + onError: suspend (Throwable) -> Unit, + onSuccess: suspend CoroutineScope.(PagingResponse) -> Unit, + action: suspend CoroutineScope.() -> PagingResponse + ): Job = if (lastRequestTime + REQUEST_DELAY > currentTimeMs) { + startRequest( + onError = onError, + onSuccess = onSuccess, + action = action, + start = CoroutineStart.LAZY + ).apply { + nextPageJob = this + } + } else { + startRequest( + onError = onError, + onSuccess = onSuccess, + action = action, + ) + } + + override fun cancel() { + job?.cancel() + nextPageJob?.cancel() + } + + private fun startRequest( + onError: suspend (Throwable) -> Unit, + onSuccess: suspend CoroutineScope.(PagingResponse) -> Unit, + action: suspend CoroutineScope.() -> PagingResponse, + start: CoroutineStart = CoroutineStart.DEFAULT, + ): Job = scope + .launch( + start = start, + onError = onError, + onSuccess = onSuccess, + action = { + job = nextPageJob + nextPageJob = null + action() + } + ).apply { + invokeOnCompletion { + nextPageJob?.start() + } + } + + companion object { + private const val REQUEST_DELAY = 500L + } +} \ No newline at end of file diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/FavouriteScreen.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/FavouriteScreen.kt index a663b5f0..23a165f9 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/FavouriteScreen.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/FavouriteScreen.kt @@ -8,8 +8,8 @@ import androidx.compose.runtime.remember import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.koin.getScreenModel import com.stslex.feature.favourite.ui.components.FavouriteScreen -import com.stslex.feature.favourite.ui.store.FavouriteStore -import com.stslex.feature.favourite.ui.store.FavouriteStoreComponent.Action +import com.stslex.feature.favourite.ui.store.FavouriteStore.Action +import com.stslex.feature.favourite.ui.store.FavouriteStoreImpl data class FavouriteScreen( val uuid: String @@ -17,7 +17,7 @@ data class FavouriteScreen( @Composable override fun Content() { - val store = getScreenModel() + val store = getScreenModel() val state by remember { store.state }.collectAsState() LaunchedEffect(key1 = Unit) { diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/data/model/FavouriteDataModel.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/data/model/FavouriteDataModel.kt index e9826e08..383d5047 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/data/model/FavouriteDataModel.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/data/model/FavouriteDataModel.kt @@ -1,7 +1,9 @@ package com.stslex.feature.favourite.data.model +import com.stslex.core.core.paging.PagingCoreItem + data class FavouriteDataModel( - val uuid: String, + override val uuid: String, val title: String, val isFavourite: Boolean, -) +) : PagingCoreItem diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/data/repository/FavouriteRepository.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/data/repository/FavouriteRepository.kt index 39800fb9..5be72c19 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/data/repository/FavouriteRepository.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/data/repository/FavouriteRepository.kt @@ -1,5 +1,6 @@ package com.stslex.feature.favourite.data.repository +import com.stslex.core.core.paging.PagingResponse import com.stslex.feature.favourite.data.model.FavouriteDataModel interface FavouriteRepository { @@ -9,7 +10,7 @@ interface FavouriteRepository { query: String, page: Int, pageSize: Int - ): List + ): PagingResponse suspend fun addFavourite(model: FavouriteDataModel) diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/data/repository/FavouriteRepositoryImpl.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/data/repository/FavouriteRepositoryImpl.kt index 9f621552..1c51ed26 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/data/repository/FavouriteRepositoryImpl.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/data/repository/FavouriteRepositoryImpl.kt @@ -1,5 +1,7 @@ package com.stslex.feature.favourite.data.repository +import com.stslex.core.core.paging.PagingResponse +import com.stslex.core.core.paging.pagingMap import com.stslex.core.network.clients.profile.client.ProfileClient import com.stslex.core.network.clients.profile.model.request.PagingProfileRequest import com.stslex.feature.favourite.data.model.FavouriteDataModel @@ -14,7 +16,7 @@ class FavouriteRepositoryImpl( query: String, page: Int, pageSize: Int - ): List = client + ): PagingResponse = client .getFavourites( PagingProfileRequest( uuid = uuid, @@ -23,8 +25,7 @@ class FavouriteRepositoryImpl( pageSize = pageSize ) ) - .result - .map { result -> + .pagingMap { result -> result.toData() } diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/di/FeatureFavouriteModule.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/di/FeatureFavouriteModule.kt index 531e1b55..9982d4d5 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/di/FeatureFavouriteModule.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/di/FeatureFavouriteModule.kt @@ -6,24 +6,22 @@ import com.stslex.feature.favourite.domain.interactor.FavouriteInteractor import com.stslex.feature.favourite.domain.interactor.FavouriteInteractorImpl import com.stslex.feature.favourite.navigation.FavouriteRouter import com.stslex.feature.favourite.navigation.FavouriteRouterImpl -import com.stslex.feature.favourite.ui.store.FavouriteStore +import com.stslex.feature.favourite.ui.store.FavouriteStoreImpl import org.koin.dsl.module val featureFavouriteModule = module { factory { FavouriteRepositoryImpl(client = get()) } factory { - FavouriteInteractorImpl( - repository = get(), - pagingWorker = get() - ) + FavouriteInteractorImpl(repository = get()) } factory { FavouriteRouterImpl(navigator = get()) } factory { - FavouriteStore( + FavouriteStoreImpl( interactor = get(), appDispatcher = get(), router = get(), + pagingFactory = get() ) } } \ No newline at end of file diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/domain/interactor/FavouriteInteractor.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/domain/interactor/FavouriteInteractor.kt index 0bde6571..4ead1a1d 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/domain/interactor/FavouriteInteractor.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/domain/interactor/FavouriteInteractor.kt @@ -1,18 +1,16 @@ package com.stslex.feature.favourite.domain.interactor +import com.stslex.core.core.paging.PagingResponse import com.stslex.feature.favourite.domain.model.FavouriteDomainModel -import kotlinx.coroutines.flow.SharedFlow interface FavouriteInteractor { - val favourites: SharedFlow> - suspend fun getFavourites( uuid: String, query: String, page: Int, pageSize: Int - ) + ): PagingResponse suspend fun setFavourite(model: FavouriteDomainModel) } diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/domain/interactor/FavouriteInteractorImpl.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/domain/interactor/FavouriteInteractorImpl.kt index b12a0099..cfcaa3d9 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/domain/interactor/FavouriteInteractorImpl.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/domain/interactor/FavouriteInteractorImpl.kt @@ -1,47 +1,30 @@ package com.stslex.feature.favourite.domain.interactor -import com.stslex.core.network.utils.PagingWorker +import com.stslex.core.core.paging.PagingResponse +import com.stslex.core.core.paging.pagingMap import com.stslex.feature.favourite.data.repository.FavouriteRepository import com.stslex.feature.favourite.domain.model.FavouriteDomainModel import com.stslex.feature.favourite.domain.model.toData import com.stslex.feature.favourite.domain.model.toDomain -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.asSharedFlow class FavouriteInteractorImpl( - private val pagingWorker: PagingWorker, private val repository: FavouriteRepository ) : FavouriteInteractor { - private val _favourites = MutableSharedFlow>() - override val favourites: SharedFlow> = _favourites.asSharedFlow() - override suspend fun getFavourites( uuid: String, query: String, page: Int, pageSize: Int - ) { - pagingWorker { - val items = repository - .getFavourites( - uuid = uuid, - query = query, - page = page, - pageSize = pageSize - ) - .map { favourite -> - favourite.toDomain() - } - val screenItems = favourites - .replayCache - .lastOrNull() - .orEmpty() - .plus(items) - _favourites.emit(screenItems) + ): PagingResponse = repository + .getFavourites( + uuid = uuid, + query = query, + page = page, + pageSize = pageSize + ).pagingMap { favourite -> + favourite.toDomain() } - } override suspend fun setFavourite(model: FavouriteDomainModel) { if (model.isFavourite) { diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/domain/model/FavouriteDomainModel.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/domain/model/FavouriteDomainModel.kt index 41b04199..be086250 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/domain/model/FavouriteDomainModel.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/domain/model/FavouriteDomainModel.kt @@ -1,7 +1,9 @@ package com.stslex.feature.favourite.domain.model +import com.stslex.core.core.paging.PagingCoreItem + data class FavouriteDomainModel( - val uuid: String, + override val uuid: String, val title: String, val isFavourite: Boolean, -) +) : PagingCoreItem diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/navigation/FavouriteRouter.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/navigation/FavouriteRouter.kt index 14b352d2..b7792e84 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/navigation/FavouriteRouter.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/navigation/FavouriteRouter.kt @@ -1,6 +1,6 @@ package com.stslex.feature.favourite.navigation import com.stslex.core.ui.mvi.Router -import com.stslex.feature.favourite.ui.store.FavouriteStoreComponent.Navigation +import com.stslex.feature.favourite.ui.store.FavouriteStore.Navigation interface FavouriteRouter : Router diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/navigation/FavouriteRouterImpl.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/navigation/FavouriteRouterImpl.kt index d9f32528..f7148e1f 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/navigation/FavouriteRouterImpl.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/navigation/FavouriteRouterImpl.kt @@ -2,8 +2,8 @@ package com.stslex.feature.favourite.navigation import com.stslex.core.ui.navigation.AppNavigator import com.stslex.core.ui.navigation.AppScreen -import com.stslex.feature.favourite.ui.store.FavouriteStoreComponent.Navigation -import com.stslex.feature.favourite.ui.store.FavouriteStoreComponent.Navigation.OpenFilm +import com.stslex.feature.favourite.ui.store.FavouriteStore.Navigation +import com.stslex.feature.favourite.ui.store.FavouriteStore.Navigation.OpenFilm class FavouriteRouterImpl( private val navigator: AppNavigator diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/components/FavouriteScreen.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/components/FavouriteScreen.kt index 310086c0..cb540da3 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/components/FavouriteScreen.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/components/FavouriteScreen.kt @@ -9,12 +9,12 @@ import com.stslex.feature.favourite.ui.components.content.FavouriteScreenContent import com.stslex.feature.favourite.ui.components.error.FavouriteScreenError import com.stslex.feature.favourite.ui.components.shimmer.FavouriteScreenShimmer import com.stslex.feature.favourite.ui.store.FavouriteScreenState -import com.stslex.feature.favourite.ui.store.FavouriteStoreComponent -import com.stslex.feature.favourite.ui.store.FavouriteStoreComponent.Action +import com.stslex.feature.favourite.ui.store.FavouriteStore +import com.stslex.feature.favourite.ui.store.FavouriteStore.Action @Composable internal fun FavouriteScreen( - state: FavouriteStoreComponent.State, + state: FavouriteStore.State, onAction: (Action) -> Unit ) { Box( @@ -25,7 +25,7 @@ internal fun FavouriteScreen( when (state.screen) { is FavouriteScreenState.Content -> FavouriteScreenContent( state = state.screen, - items = state.data, + items = state.pagingState, query = state.query, isLoading = state.isLoading, onItemClick = { uuid -> diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/model/FavouriteModel.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/model/FavouriteModel.kt index 32ca5b11..ed67bfe6 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/model/FavouriteModel.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/model/FavouriteModel.kt @@ -1,7 +1,11 @@ package com.stslex.feature.favourite.ui.model +import androidx.compose.runtime.Stable +import com.stslex.core.ui.base.paging.PagingItem + +@Stable data class FavouriteModel( - val uuid: String, + override val uuid: String, val title: String, val isFavourite: Boolean, -) \ No newline at end of file +) : PagingItem \ No newline at end of file diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/store/FavouriteScreenState.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/store/FavouriteScreenState.kt index fa5a58f8..9f2c606d 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/store/FavouriteScreenState.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/store/FavouriteScreenState.kt @@ -1,6 +1,8 @@ package com.stslex.feature.favourite.ui.store import androidx.compose.runtime.Stable +import com.stslex.core.ui.base.AppError +import com.stslex.core.ui.pager.states.PagerLoadState @Stable sealed interface FavouriteScreenState { @@ -12,12 +14,28 @@ sealed interface FavouriteScreenState { data object Data : Content @Stable - data object Empty : Content + data object Loading : Content + + @Stable + data object Refresh : Content } @Stable data object Shimmer : FavouriteScreenState @Stable - data class Error(val error: Throwable) : FavouriteScreenState + data object Empty : Content + + @Stable + data class Error(val error: AppError) : FavouriteScreenState +} + +fun PagerLoadState.toUi() = when (this) { + PagerLoadState.Data -> FavouriteScreenState.Content.Data + PagerLoadState.Empty -> FavouriteScreenState.Empty + is PagerLoadState.Error -> FavouriteScreenState.Error(error) + PagerLoadState.Initial -> FavouriteScreenState.Shimmer + PagerLoadState.Loading -> FavouriteScreenState.Content.Loading + PagerLoadState.Refresh -> FavouriteScreenState.Content.Refresh + PagerLoadState.Retry -> FavouriteScreenState.Shimmer } \ No newline at end of file diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/store/FavouriteStore.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/store/FavouriteStore.kt index 043ccee2..ed4e3fee 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/store/FavouriteStore.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/store/FavouriteStore.kt @@ -1,176 +1,73 @@ package com.stslex.feature.favourite.ui.store -import com.stslex.core.core.AppDispatcher -import com.stslex.core.ui.mvi.BaseStore -import com.stslex.feature.favourite.domain.interactor.FavouriteInteractor -import com.stslex.feature.favourite.navigation.FavouriteRouter -import com.stslex.feature.favourite.ui.model.toDomain -import com.stslex.feature.favourite.ui.model.toUI -import com.stslex.feature.favourite.ui.store.FavouriteStoreComponent.Action -import com.stslex.feature.favourite.ui.store.FavouriteStoreComponent.Event -import com.stslex.feature.favourite.ui.store.FavouriteStoreComponent.Navigation -import com.stslex.feature.favourite.ui.store.FavouriteStoreComponent.State -import com.stslex.feature.favourite.ui.store.FavouriteStoreComponent.State.Companion.DEFAULT_PAGE -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map - -class FavouriteStore( - private val interactor: FavouriteInteractor, - router: FavouriteRouter, - appDispatcher: AppDispatcher -) : BaseStore( - router = router, - appDispatcher = appDispatcher, - initialState = State.INITIAL -) { - private var likeJob: Job? = null - - override fun process(action: Action) { - when (action) { - is Action.Init -> actionInit(action) - is Action.LoadMore -> actionLoadMore() - is Action.ItemClick -> actionItemClick(action) - is Action.LikeClick -> actionLikeClick(action) - is Action.InputSearch -> actionInputSearch(action) - } - } +import androidx.compose.runtime.Stable +import com.stslex.core.ui.base.paging.PagingState +import com.stslex.core.ui.mvi.Store +import com.stslex.core.ui.mvi.Store.Event.Snackbar +import com.stslex.feature.favourite.ui.model.FavouriteModel +import com.stslex.feature.favourite.ui.store.FavouriteStore.Action +import com.stslex.feature.favourite.ui.store.FavouriteStore.Event +import com.stslex.feature.favourite.ui.store.FavouriteStore.State - private fun actionInputSearch(action: Action.InputSearch) { - updateState { state -> - state.copy(query = action.query) - } - } +interface FavouriteStore : Store { - private fun actionItemClick(action: Action.ItemClick) { - navigate(Navigation.OpenFilm(action.uuid)) - } + @Stable + data class State( + val uuid: String, + val query: String, + val pagingState: PagingState, + val screen: FavouriteScreenState, + val isLoading: Boolean + ) : Store.State { + + companion object { - private fun actionLikeClick(action: Action.LikeClick) { - if (likeJob?.isActive == true) return - val item = state.value.data.firstOrNull { it.uuid == action.uuid } ?: return - val isFavourite = item.isFavourite.not() - updateState { state -> - state.copy( - data = state.data.map { favourite -> - if (favourite.uuid == item.uuid) { - favourite.copy(isFavourite = isFavourite) - } else { - favourite - } - }.toImmutableList() + val INITIAL = State( + uuid = "", + query = "", + pagingState = PagingState.default(), + screen = FavouriteScreenState.Shimmer, + isLoading = true ) } - likeJob = launch( - onSuccess = { /* do nothing */ }, - onError = { error -> - sendEvent(Event.ErrorSnackBar(error.message.orEmpty())) - }, - action = { - interactor.setFavourite( - model = item.toDomain().copy(isFavourite = isFavourite) - ) - }) } - private fun actionInit(action: Action.Init) { - updateState { state -> - state.copy( - uuid = action.uuid, - ) - } + @Stable + sealed interface Action : Store.Action { - state.map { it.query } - .distinctUntilChanged() - .launch { query -> - updateState { state -> - state.copy( - page = DEFAULT_PAGE, - query = query, - ) - } - loadNextItems() - } - - interactor.favourites - .launch { data -> - val screen = if (data.isEmpty()) { - FavouriteScreenState.Content.Empty - } else { - FavouriteScreenState.Content.Data - } - updateState { state -> - state.copy( - data = data.map { it.toUI() }.toImmutableList(), - screen = screen, - isLoading = false - ) - } - } - } + @Stable + data class Init( + val uuid: String + ) : Action - private fun actionLoadMore() { - loadNextItems() - } + @Stable + data object LoadMore : Action - private fun loadNextItems() { - val currentState = state.value - val loadingScreen = if ( - currentState.screen is FavouriteScreenState.Content && - currentState.data.isNotEmpty() - ) { - currentState.screen - } else { - FavouriteScreenState.Shimmer - } + @Stable + data object Refresh : Action - updateState { state -> - state.copy( - screen = loadingScreen, - isLoading = true - ) - } + @Stable + data object Retry : Action - val page = if (currentState.page == DEFAULT_PAGE) { - FIRST_PAGE - } else { - currentState.page.inc() - } + @Stable + data class LikeClick(val uuid: String) : Action - launch( - onSuccess = { - updateState { state -> - state.copy( - page = page - ) - } - }, - onError = { error -> - if (currentState.data.isEmpty()) { - updateState { state -> - state.copy( - screen = FavouriteScreenState.Error(error), - isLoading = false - ) - } - } else { - updateState { it.copy(isLoading = false) } - sendEvent(Event.ErrorSnackBar(error.message.orEmpty())) - } - } - ) { - interactor.getFavourites( - uuid = state.value.uuid, - query = state.value.query, - page = page, - pageSize = PAGE_SIZE, - ) - } + @Stable + data class ItemClick(val uuid: String) : Action + + @Stable + data class InputSearch(val query: String) : Action } - companion object { - private const val PAGE_SIZE = 20 - private const val FIRST_PAGE = 1 + @Stable + sealed interface Event : Store.Event { + + @Stable + data class ShowSnackbar(val snackbar: Snackbar) : Event + } + + sealed interface Navigation : Store.Navigation { + + data class OpenFilm(val uuid: String) : Navigation } -} +} \ No newline at end of file diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/store/FavouriteStoreComponent.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/store/FavouriteStoreComponent.kt deleted file mode 100644 index 549e1c6b..00000000 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/store/FavouriteStoreComponent.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.stslex.feature.favourite.ui.store - -import androidx.compose.runtime.Stable -import com.stslex.core.ui.mvi.Store -import com.stslex.feature.favourite.ui.model.FavouriteModel -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList - -interface FavouriteStoreComponent : Store { - - @Stable - data class State( - val uuid: String, - val page: Int, - val query: String, - val data: ImmutableList, - val screen: FavouriteScreenState, - val isLoading: Boolean - ) : Store.State { - - companion object { - - const val DEFAULT_PAGE = -1 - - val INITIAL = State( - uuid = "", - page = DEFAULT_PAGE, - query = "", - data = emptyList().toImmutableList(), - screen = FavouriteScreenState.Shimmer, - isLoading = true - ) - } - } - - @Stable - sealed interface Action : Store.Action { - - @Stable - data class Init( - val uuid: String - ) : Action - - @Stable - data object LoadMore : Action - - @Stable - data class LikeClick(val uuid: String) : Action - - @Stable - data class ItemClick(val uuid: String) : Action - - @Stable - data class InputSearch(val query: String) : Action - } - - @Stable - sealed interface Event : Store.Event { - - @Stable - data class ErrorSnackBar(val message: String) : Event - } - - sealed interface Navigation : Store.Navigation { - - data class OpenFilm(val uuid: String) : Navigation - } -} \ No newline at end of file diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/store/FavouriteStoreImpl.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/store/FavouriteStoreImpl.kt new file mode 100644 index 00000000..30effeaa --- /dev/null +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/store/FavouriteStoreImpl.kt @@ -0,0 +1,170 @@ +package com.stslex.feature.favourite.ui.store + +import com.stslex.core.core.AppDispatcher +import com.stslex.core.ui.base.mapToAppError +import com.stslex.core.ui.mvi.BaseStore +import com.stslex.core.ui.mvi.Store.Event.Snackbar +import com.stslex.core.ui.pager.pager.StorePager +import com.stslex.core.ui.pager.pager.StorePagerFactory +import com.stslex.core.ui.pager.states.PagerLoadState +import com.stslex.feature.favourite.domain.interactor.FavouriteInteractor +import com.stslex.feature.favourite.navigation.FavouriteRouter +import com.stslex.feature.favourite.ui.model.FavouriteModel +import com.stslex.feature.favourite.ui.model.toDomain +import com.stslex.feature.favourite.ui.model.toUI +import com.stslex.feature.favourite.ui.store.FavouriteStore.Action +import com.stslex.feature.favourite.ui.store.FavouriteStore.Event +import com.stslex.feature.favourite.ui.store.FavouriteStore.Navigation +import com.stslex.feature.favourite.ui.store.FavouriteStore.State +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map + +class FavouriteStoreImpl( + private val interactor: FavouriteInteractor, + router: FavouriteRouter, + appDispatcher: AppDispatcher, + pagingFactory: StorePagerFactory, +) : BaseStore( + router = router, + appDispatcher = appDispatcher, + initialState = State.INITIAL +), FavouriteStore { + + private var likeJob: Job? = null + + private val pager: StorePager = pagingFactory.create( + request = { page, pageSize -> + interactor.getFavourites( + uuid = state.value.uuid, + query = state.value.query, + page = page, + pageSize = pageSize + ) + }, + scope = scope, + mapper = { it.toUI() } + ) + + override fun process(action: Action) { + when (action) { + is Action.Init -> actionInit(action) + is Action.LoadMore -> actionLoadMore() + is Action.ItemClick -> actionItemClick(action) + is Action.LikeClick -> actionLikeClick(action) + is Action.InputSearch -> actionInputSearch(action) + is Action.Refresh -> actionRefresh() + is Action.Retry -> actionRetryClick() + } + } + + private fun actionInit(action: Action.Init) { + updateState { state -> + state.copy( + uuid = action.uuid, + ) + } + + pager.state.launch { pagerState -> + updateState { currentState -> + currentState.copy( + pagingState = pagerState + ) + } + } + pager.loadState.launch { loadState -> + updateState { currentState -> + currentState.copy( + screen = loadState.toUi() + ) + } + } + pager.loadEvents.launch { + sendEvent( + Event.ShowSnackbar(Snackbar.Error("error load matches")) + ) + } + + updateState { currentState -> + currentState.copy(uuid = action.uuid) + } + + state + .map { it.query } + .distinctUntilChanged() + .launch( + onError = ::showError + ) { + if (pager.loadState.value is PagerLoadState.Initial) { + pager.initialLoad() + } else { + pager.refresh(isForceLoad = false) + } + } + } + + private fun actionInputSearch(action: Action.InputSearch) { + updateState { state -> + state.copy(query = action.query) + } + } + + private fun actionItemClick(action: Action.ItemClick) { + navigate(Navigation.OpenFilm(action.uuid)) + } + + private fun actionLikeClick(action: Action.LikeClick) { + if (likeJob?.isActive == true) return + val items = state.value.pagingState.result.toMutableList() + val itemIndex = items + .indexOfFirst { + it.uuid == action.uuid + } + .takeIf { it != -1 } + ?: return + val item = state.value.pagingState.result.getOrNull(itemIndex) ?: return + val newItem = item.copy(isFavourite = item.isFavourite.not()) + items[itemIndex] = newItem + updateState { state -> + state.copy( + pagingState = state.pagingState.copy( + result = items.toImmutableList() + ) + ) + } + likeJob = launch( + onSuccess = { /* do nothing */ }, + onError = ::showError, + action = { + interactor.setFavourite(newItem.toDomain()) + }) + } + + private fun actionLoadMore() { + pager.load() + } + + private fun actionRefresh() { + pager.refresh(isForceLoad = true) + } + + private fun actionRetryClick() { + pager.retry() + } + + private fun showError(error: Throwable) { + val appError = error.mapToAppError("error logout") + if (state.value.screen is FavouriteScreenState.Content) { + sendEvent( + Event.ShowSnackbar(Snackbar.Error(appError.message)) + ) + } else { + updateState { currentState -> + currentState.copy( + screen = FavouriteScreenState.Error(appError) + ) + } + } + } +} diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/di/MatchModule.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/di/MatchModule.kt index fe810eeb..6642b0ed 100644 --- a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/di/MatchModule.kt +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/di/MatchModule.kt @@ -23,7 +23,8 @@ val featureMatchModule = module { interactor = get(), router = get(), appDispatcher = get(), - userStore = get() + userStore = get(), + pagerFactory = get() ) } } \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/interactor/MatchInteractor.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/interactor/MatchInteractor.kt index 4149bb9d..b1931e5e 100644 --- a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/interactor/MatchInteractor.kt +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/interactor/MatchInteractor.kt @@ -7,6 +7,7 @@ interface MatchInteractor { suspend fun getMatches( uuid: String, + query: String, page: Int, pageSize: Int, ): PagingResponse diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/interactor/MatchInteractorImpl.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/interactor/MatchInteractorImpl.kt index 2828e174..c3fe2856 100644 --- a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/interactor/MatchInteractorImpl.kt +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/domain/interactor/MatchInteractorImpl.kt @@ -14,6 +14,7 @@ class MatchInteractorImpl( override suspend fun getMatches( uuid: String, + query: String, page: Int, pageSize: Int, ): PagingResponse = repository @@ -21,7 +22,7 @@ class MatchInteractorImpl( uuid = uuid, page = page, pageSize = pageSize, - query = "" // todo add query + query = query ) .pagingMap { it.toDomain() diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/model/MatchDataMapper.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/model/MatchDataMapper.kt index bc0e4c7a..ee42da2d 100644 --- a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/model/MatchDataMapper.kt +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/model/MatchDataMapper.kt @@ -1,7 +1,7 @@ package com.stslex.feature.match.ui.model import com.stslex.core.core.asyncMap -import com.stslex.core.ui.pager.PagerLoadState +import com.stslex.core.ui.pager.states.PagerLoadState import com.stslex.feature.match.domain.model.MatchDomainModel import com.stslex.feature.match.domain.model.MatchDomainStatus import com.stslex.feature.match.domain.model.MatchUserDomainModel diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchStore.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchStore.kt index b4ace7c3..82ad1420 100644 --- a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchStore.kt +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchStore.kt @@ -18,6 +18,7 @@ interface MatchStore : Store { val screen: MatchScreenState, val uuid: String, val isSelf: Boolean, + val query: String, val pagingState: PagingState ) : Store.State { @@ -27,7 +28,8 @@ interface MatchStore : Store { screen = MatchScreenState.Shimmer, pagingState = PagingState.default(), uuid = "", - isSelf = false + isSelf = false, + query = "" ) } } @@ -60,6 +62,10 @@ interface MatchStore : Store { data object Logout : Action data object RepeatLastAction : Action, Store.Action.RepeatLastAction + + data class OnQueryChanged( + val query: String + ) : Action } @Stable diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchStoreImpl.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchStoreImpl.kt index af8d0247..6695a3bc 100644 --- a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchStoreImpl.kt +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchStoreImpl.kt @@ -5,8 +5,9 @@ import com.stslex.core.database.store.UserStore import com.stslex.core.ui.base.mapToAppError import com.stslex.core.ui.mvi.BaseStore import com.stslex.core.ui.mvi.Store.Event.Snackbar -import com.stslex.core.ui.pager.StorePager -import com.stslex.core.ui.pager.StorePagerImpl +import com.stslex.core.ui.pager.pager.StorePager +import com.stslex.core.ui.pager.pager.StorePagerFactory +import com.stslex.core.ui.pager.states.PagerLoadState import com.stslex.feature.match.domain.interactor.MatchInteractor import com.stslex.feature.match.navigation.MatchRouter import com.stslex.feature.match.ui.model.MatchUiModel @@ -15,6 +16,8 @@ import com.stslex.feature.match.ui.store.MatchStore.Action import com.stslex.feature.match.ui.store.MatchStore.Event import com.stslex.feature.match.ui.store.MatchStore.Navigation import com.stslex.feature.match.ui.store.MatchStore.State +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map // todo refactor pager state for UI https://github.com/stslex/Wizard/issues/35 // todo add base store pager binding https://github.com/stslex/Wizard/issues/36 @@ -23,18 +26,20 @@ import com.stslex.feature.match.ui.store.MatchStore.State class MatchStoreImpl( appDispatcher: AppDispatcher, router: MatchRouter, + pagerFactory: StorePagerFactory, private val interactor: MatchInteractor, - private val userStore: UserStore + private val userStore: UserStore, ) : BaseStore( appDispatcher = appDispatcher, router = router, initialState = State.INITIAL ), MatchStore { - private val pager: StorePager = StorePagerImpl( + private val pager: StorePager = pagerFactory.create( request = { page, pageSize -> interactor.getMatches( uuid = state.value.uuid, + query = state.value.query, page = page, pageSize = pageSize ) @@ -52,6 +57,7 @@ class MatchStoreImpl( is Action.Refresh -> actionRefresh() is Action.Logout -> actionLogout() is Action.RepeatLastAction -> actionRepeatLastAction() + is Action.OnQueryChanged -> actionOnQueryChanged(action) } } @@ -75,14 +81,26 @@ class MatchStoreImpl( Event.ShowSnackbar(Snackbar.Error("error load matches")) ) } - pager.initialLoad() + updateState { currentState -> currentState.copy( isSelf = action.args.isSelf, uuid = action.args.uuid ?: userStore.uuid, ) } - pager.initialLoad() + + state + .map { it.query } + .distinctUntilChanged() + .launch( + onError = ::showError + ) { + if (pager.loadState.value is PagerLoadState.Initial) { + pager.initialLoad() + } else { + pager.refresh(isForceLoad = false) + } + } } private fun actionLoadMore() { @@ -98,7 +116,7 @@ class MatchStoreImpl( } private fun actionRefresh() { - pager.refresh() + pager.refresh(isForceLoad = true) } private fun actionLogout() { @@ -109,20 +127,7 @@ class MatchStoreImpl( onSuccess = { navigate(Navigation.LogOut) }, - onError = { error -> - val appError = error.mapToAppError("error logout") - if (state.value.screen is MatchScreenState.Content) { - sendEvent( - Event.ShowSnackbar(Snackbar.Error(appError.message)) - ) - } else { - updateState { currentState -> - currentState.copy( - screen = MatchScreenState.Error(appError) - ) - } - } - } + onError = ::showError ) } @@ -139,4 +144,27 @@ class MatchStoreImpl( } process(lastAction) } + + private fun actionOnQueryChanged(action: Action.OnQueryChanged) { + updateState { currentState -> + currentState.copy( + query = action.query + ) + } + } + + private fun showError(error: Throwable) { + val appError = error.mapToAppError("error logout") + if (state.value.screen is MatchScreenState.Content) { + sendEvent( + Event.ShowSnackbar(Snackbar.Error(appError.message)) + ) + } else { + updateState { currentState -> + currentState.copy( + screen = MatchScreenState.Error(appError) + ) + } + } + } } \ No newline at end of file