diff --git a/.github/workflows/build_linux.yml b/.github/workflows/build_linux.yml index 49714cf6..db437943 100644 --- a/.github/workflows/build_linux.yml +++ b/.github/workflows/build_linux.yml @@ -33,31 +33,4 @@ jobs: run: echo "$LOCAL_PROPERTIES" > ./local.properties - name: Build with Gradle - run: ./gradlew assembleAndroidTest - - build-desktop: - - runs-on: ubuntu-latest - - steps: - - - name: Checkout branch - uses: actions/checkout@v2 - - - name: set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: gradle - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: set up LOCAL_PROPERTIES - env: - LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }} - run: echo "$LOCAL_PROPERTIES" > ./local.properties - - - name: Build with Gradle - run: ./gradlew desktopMainClasses desktopSourcesJar desktopTestClasses \ No newline at end of file + run: ./gradlew assembleAndroidTest \ No newline at end of file diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 302e7956..2964121e 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -1,5 +1,3 @@ -import org.jetbrains.compose.desktop.application.dsl.TargetFormat - plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidApplication) @@ -16,8 +14,6 @@ kotlin { } } - jvm("desktop") - iosX64() iosArm64() iosSimulatorArm64() @@ -35,11 +31,6 @@ kotlin { } sourceSets { - val desktopMain by getting - - desktopMain.dependencies { - implementation(compose.desktop.currentOs) - } commonMain.dependencies { implementation(project(":core:core")) @@ -106,18 +97,6 @@ dependencies { implementation(project(":feature:settings")) } -compose.desktop { - application { - mainClass = "MainKt" - - nativeDistributions { - targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) - packageName = "com.stslex.wizard" - packageVersion = "1.0.0" - } - } -} - tasks.withType { compilerOptions.freeCompilerArgs.addAll( "-P", diff --git a/composeApp/src/desktopMain/kotlin/main.kt b/composeApp/src/desktopMain/kotlin/main.kt deleted file mode 100644 index 8f82e677..00000000 --- a/composeApp/src/desktopMain/kotlin/main.kt +++ /dev/null @@ -1,16 +0,0 @@ -import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.runtime.Composable -import androidx.compose.ui.window.Window -import androidx.compose.ui.window.application - -fun main() = application { - Window(onCloseRequest = ::exitApplication) { - App() - } -} - -@Preview -@Composable -fun AppDesktopPreview() { - App() -} \ No newline at end of file diff --git a/core/core/build.gradle.kts b/core/core/build.gradle.kts index 6f63384f..173f7bad 100644 --- a/core/core/build.gradle.kts +++ b/core/core/build.gradle.kts @@ -15,8 +15,6 @@ kotlin { } } - jvm("desktop") - iosX64() iosArm64() iosSimulatorArm64() diff --git a/core/core/src/commonMain/kotlin/com/stslex/core/core/paging/PagingCoreData.kt b/core/core/src/commonMain/kotlin/com/stslex/core/core/paging/PagingCoreData.kt index 6ef67403..e8125012 100644 --- a/core/core/src/commonMain/kotlin/com/stslex/core/core/paging/PagingCoreData.kt +++ b/core/core/src/commonMain/kotlin/com/stslex/core/core/paging/PagingCoreData.kt @@ -15,5 +15,8 @@ interface PagingCoreData { const val DEFAULT_PAGING_TYPE = "ItemPaging" const val DEFAULT_APPEND_TYPE = "AppendPaging" const val DEFAULT_BOTTOM_TYPE = "BottomPaging" + const val DEFAULT_PAGING_DELAY = 300L + const val DEFAULT_PAGING_LOAD_SIZE = 3 + const val DEFAULT_QUERY_LOAD_SIZE = 2 } } diff --git a/core/core/src/desktopMain/kotlin/com/stslex/core/core/CommonUtils.desktop.kt b/core/core/src/desktopMain/kotlin/com/stslex/core/core/CommonUtils.desktop.kt deleted file mode 100644 index bfe6747c..00000000 --- a/core/core/src/desktopMain/kotlin/com/stslex/core/core/CommonUtils.desktop.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.stslex.core.core - -import java.util.UUID - -actual fun randomUuid(): String = UUID.randomUUID().toString() \ No newline at end of file diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index 74c140ac..4ca813b3 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -15,8 +15,6 @@ kotlin { } } - jvm("desktop") - iosX64() iosArm64() iosSimulatorArm64() diff --git a/core/database/src/desktopMain/kotlin/com/stslex/core/database/di/DatabaseModel.desktop.kt b/core/database/src/desktopMain/kotlin/com/stslex/core/database/di/DatabaseModel.desktop.kt deleted file mode 100644 index faae458a..00000000 --- a/core/database/src/desktopMain/kotlin/com/stslex/core/database/di/DatabaseModel.desktop.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.stslex.core.database.di - -import com.russhwolf.settings.PreferencesSettings -import com.stslex.core.database.store.UserSettings -import com.stslex.core.database.store.UserSettingsImpl -import org.koin.dsl.module -import java.util.prefs.Preferences - -actual val userSettingsModule = module { - val delegate = Preferences.userRoot().node(UserSettings.NAME) - val prefsSettings = PreferencesSettings(delegate) - single { UserSettingsImpl(prefsSettings) } -} \ No newline at end of file diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index e3d68328..7f4d2c1b 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -19,8 +19,6 @@ kotlin { } } - jvm("desktop") - iosX64() iosArm64() iosSimulatorArm64() diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 8913ae1a..d7ab0d4e 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -1,5 +1,3 @@ -import org.jetbrains.compose.ExperimentalComposeLibrary - plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidLibrary) @@ -16,8 +14,6 @@ kotlin { } } - jvm("desktop") - iosX64() iosArm64() iosSimulatorArm64() @@ -33,11 +29,6 @@ kotlin { } sourceSets { - val desktopMain by getting - - desktopMain.dependencies { - implementation(compose.desktop.currentOs) - } commonMain.dependencies { implementation(project(":core:core")) @@ -47,10 +38,11 @@ kotlin { api(compose.foundation) api(compose.material) api(compose.material3) - @OptIn(ExperimentalComposeLibrary::class) api(compose.components.resources) api(libs.bundles.voyager) api(libs.kamel) + + implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0-beta02") } commonTest.dependencies { implementation(libs.kotlin.test) diff --git a/core/ui/src/androidMain/kotlin/com/stslex/core/ui/mvi/StoreExt.android.kt b/core/ui/src/androidMain/kotlin/com/stslex/core/ui/mvi/StoreExt.android.kt new file mode 100644 index 00000000..3d17a969 --- /dev/null +++ b/core/ui/src/androidMain/kotlin/com/stslex/core/ui/mvi/StoreExt.android.kt @@ -0,0 +1,16 @@ +package com.stslex.core.ui.mvi + +import androidx.lifecycle.ViewModel +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.definition.Definition +import org.koin.core.definition.KoinDefinition +import org.koin.core.module.Module +import org.koin.core.qualifier.Qualifier + +actual inline fun Module.viewModelDefinition( + qualifier: Qualifier?, + noinline definition: Definition, +): KoinDefinition = viewModel( + qualifier = qualifier, + definition = definition +) diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/base/paging/PagingConfig.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/base/paging/PagingConfig.kt index b111c64d..18f49ff1 100644 --- a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/base/paging/PagingConfig.kt +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/base/paging/PagingConfig.kt @@ -6,14 +6,20 @@ import com.stslex.core.core.paging.PagingCoreData @Stable data class PagingConfig( val pageSize: Int, - val pageOffset: Float = PagingCoreData.DEFAULT_PAGE_OFFSET + val pageOffset: Float = PagingCoreData.DEFAULT_PAGE_OFFSET, + val delay: Long = PagingCoreData.DEFAULT_PAGING_DELAY, + val defaultLoadSize: Int = PagingCoreData.DEFAULT_PAGING_LOAD_SIZE, + val queryLoadSize: Int = PagingCoreData.DEFAULT_QUERY_LOAD_SIZE ) { companion object { val DEFAULT = PagingConfig( pageSize = PagingCoreData.DEFAULT_PAGE_SIZE, - pageOffset = PagingCoreData.DEFAULT_PAGE_OFFSET + pageOffset = PagingCoreData.DEFAULT_PAGE_OFFSET, + delay = PagingCoreData.DEFAULT_PAGING_DELAY, + defaultLoadSize = PagingCoreData.DEFAULT_PAGING_LOAD_SIZE, + queryLoadSize = PagingCoreData.DEFAULT_QUERY_LOAD_SIZE ) } } \ 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 index 091e8adc..ba87e4cd 100644 --- 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 @@ -2,10 +2,17 @@ package com.stslex.core.ui.di import com.stslex.core.ui.pager.pager.StorePagerFactory import com.stslex.core.ui.pager.pager.StorePagerFactoryImpl +import com.stslex.core.ui.pager.paging_worker.PagingWorkerFactory +import com.stslex.core.ui.pager.paging_worker.PagingWorkerFactoryImpl import org.koin.dsl.module val coreUiModule = module { + factory { + PagingWorkerFactoryImpl() + } factory { - StorePagerFactoryImpl() + StorePagerFactoryImpl( + workerFactory = get() + ) } } \ No newline at end of file diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/BaseStore.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/BaseStore.kt deleted file mode 100644 index d4cca4fe..00000000 --- a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/BaseStore.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.stslex.core.ui.mvi - -import cafe.adriel.voyager.core.model.StateScreenModel -import cafe.adriel.voyager.core.model.screenModelScope -import com.stslex.core.core.AppDispatcher -import com.stslex.core.core.Logger -import com.stslex.core.core.coroutine.AppCoroutineScope -import com.stslex.core.core.coroutine.AppCoroutineScopeImpl -import com.stslex.core.core.coroutineExceptionHandler -import com.stslex.core.ui.mvi.Store.Action -import com.stslex.core.ui.mvi.Store.Event -import com.stslex.core.ui.mvi.Store.Navigation -import com.stslex.core.ui.mvi.Store.State -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch - -abstract class BaseStore( - private val router: Router, - private val appDispatcher: AppDispatcher, - initialState: S -) : Store, StateScreenModel(initialState) { - - private var _lastAction: A? = null - protected val lastAction: A? - get() = _lastAction - - /** - * Flow of events that are sent to the screen. - * */ - private val _event: MutableSharedFlow = MutableSharedFlow() - override val event: SharedFlow = _event.asSharedFlow() - - protected val scope: AppCoroutineScope = AppCoroutineScopeImpl( - scope = screenModelScope, - appDispatcher = appDispatcher - ) - - /** - * Sends an action to the store. Checks if the action is not the same as the last action. - * If the action is not the same as the last action, the last action is updated. - * The action is then processed. - * @param action - action to be sent - */ - override fun sendAction(action: A) { - if (lastAction != action && action !is Action.RepeatLastAction) { - _lastAction = action - } - process(action) - } - - /** Process the action. This method should be overridden in the child class.*/ - protected abstract fun process(action: A) - - private fun exceptionHandler( - onError: suspend (cause: Throwable) -> Unit = {}, - ) = CoroutineExceptionHandler { _, throwable -> - Logger.exception(throwable) - screenModelScope.launch(appDispatcher.default + coroutineExceptionHandler) { - onError(throwable) - } - } - - /** - * Updates the state of the screen. - * @param update - function that updates the state - * */ - protected fun updateState(update: (S) -> S) { - mutableState.update(update) - } - - /** - * Sends an event to the screen. The event is sent on the default dispatcher of the AppDispatcher. - * @param event - event to be sent - * @see AppDispatcher - * */ - protected fun sendEvent(event: E) { - screenModelScope.launch(appDispatcher.default) { - this@BaseStore._event.emit(event) - } - } - - /** - * Navigates to the specified screen. The router is called with the specified event. - * @param event - event to be passed to the router - * @see Router - * */ - protected fun navigate(event: N) { - router(event) - } - - /** - * Launches a coroutine and catches exceptions. The coroutine is launched on the default dispatcher of the AppDispatcher. - * @param onError - error handler - * @param onSuccess - success handler - * @param action - action to be executed - * @return Job - * @see Job - * @see AppDispatcher - * */ - protected fun launch( - onError: suspend (Throwable) -> Unit = {}, - onSuccess: suspend CoroutineScope.(T) -> Unit = {}, - action: suspend CoroutineScope.() -> T, - ): Job = scope.launch( - onError = onError, - onSuccess = onSuccess, - action = action - ) - - /** - * Launches a flow and collects it in the screenModelScope. The flow is collected on the default dispatcher. of the AppDispatcher. - * @param onError - error handler - * @param each - action for each element of the flow - * @return Job - * @see Flow - * @see Job - * @see AppDispatcher - * */ - protected fun Flow.launch( - onError: suspend (cause: Throwable) -> Unit = {}, - each: suspend (T) -> Unit - ): Job = scope.launch( - flow = this, - onError = onError, - each = each, - ) -} \ No newline at end of file diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/Router.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/Router.kt index b2814bd4..3bd46477 100644 --- a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/Router.kt +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/Router.kt @@ -1,5 +1,5 @@ package com.stslex.core.ui.mvi -fun interface Router { +fun interface Router { operator fun invoke(event: E) } diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/Store.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/Store.kt index a421adc1..bea49bd9 100644 --- a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/Store.kt +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/Store.kt @@ -1,80 +1,131 @@ package com.stslex.core.ui.mvi -import androidx.compose.material3.SnackbarDuration -import androidx.compose.runtime.Stable -import com.stslex.core.ui.components.SnackbarType +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.stslex.core.core.AppDispatcher +import com.stslex.core.core.Logger +import com.stslex.core.core.coroutine.AppCoroutineScope +import com.stslex.core.core.coroutine.AppCoroutineScopeImpl +import com.stslex.core.core.coroutineExceptionHandler +import com.stslex.core.ui.mvi.StoreComponent.Action +import com.stslex.core.ui.mvi.StoreComponent.Event +import com.stslex.core.ui.mvi.StoreComponent.Navigation +import com.stslex.core.ui.mvi.StoreComponent.State +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch -interface Store { +abstract class Store( + private val router: Router, + private val appDispatcher: AppDispatcher, + initialState: S +) : ViewModel(), StoreAbstraction { - /** Flow of the state of the screen. */ - val state: StateFlow + private val _event: MutableSharedFlow = MutableSharedFlow() + override val event: SharedFlow = _event.asSharedFlow() - /** Flow of events that are sent to the screen. */ - val event: SharedFlow + private val _state: MutableStateFlow = MutableStateFlow(initialState) + override val state: StateFlow = _state.asStateFlow() - /** - * Sends an action to the store. Checks if the action is not the same as the last action. - * If the action is not the same as the last action, the last action is updated. - * The action is then processed. - * @param action - action to be sent - */ - fun sendAction(action: A) + protected val scope: AppCoroutineScope = AppCoroutineScopeImpl( + scope = viewModelScope, + appDispatcher = appDispatcher + ) - interface State + private var _lastAction: A? = null + protected val lastAction: A? + get() = _lastAction - interface Event { + override fun sendAction(action: A) { + if (lastAction != action && action !is Action.RepeatLastAction) { + _lastAction = action + } + process(action) + } - @Stable - sealed class Snackbar( - open val message: String, - open val duration: SnackbarDuration, - open val withDismissAction: Boolean, - val action: String, - ) : Event { + /** Process the action. This method should be overridden in the child class.*/ + protected abstract fun process(action: A) - @Stable - data class Error( - override val message: String, - override val duration: SnackbarDuration = SnackbarDuration.Short, - override val withDismissAction: Boolean = false, - ) : Snackbar( - message = message, - action = SnackbarType.ERROR.label, - duration = duration, - withDismissAction = withDismissAction - ) + private fun exceptionHandler( + onError: suspend (cause: Throwable) -> Unit = {}, + ) = CoroutineExceptionHandler { _, throwable -> + Logger.exception(throwable) + viewModelScope.launch(appDispatcher.default + coroutineExceptionHandler) { + onError(throwable) + } + } - @Stable - data class Success( - override val message: String, - override val duration: SnackbarDuration = SnackbarDuration.Short, - override val withDismissAction: Boolean = false, - ) : Snackbar( - message = message, - action = SnackbarType.SUCCESS.label, - duration = duration, - withDismissAction = withDismissAction - ) + /** + * Updates the state of the screen. + * @param update - function that updates the state + * */ + protected fun updateState(update: (S) -> S) { + _state.update(update) + } - @Stable - data class Info( - override val message: String, - override val duration: SnackbarDuration = SnackbarDuration.Short, - override val withDismissAction: Boolean = false, - ) : Snackbar( - message = message, - action = SnackbarType.INFO.label, - duration = duration, - withDismissAction = withDismissAction - ) + /** + * Sends an event to the screen. The event is sent on the default dispatcher of the AppDispatcher. + * @param event - event to be sent + * @see AppDispatcher + * */ + protected fun sendEvent(event: E) { + viewModelScope.launch(appDispatcher.default) { + this@Store._event.emit(event) } } - interface Navigation - - interface Action { - interface RepeatLastAction : Action + /** + * Navigates to the specified screen. The router is called with the specified event. + * @param event - event to be passed to the router + * @see Router + * */ + protected fun consumeNavigation(event: N) { + router(event) } -} + + /** + * Launches a coroutine and catches exceptions. The coroutine is launched on the default dispatcher of the AppDispatcher. + * @param onError - error handler + * @param onSuccess - success handler + * @param action - action to be executed + * @return Job + * @see Job + * @see AppDispatcher + * */ + protected fun launch( + onError: suspend (Throwable) -> Unit = {}, + onSuccess: suspend CoroutineScope.(T) -> Unit = {}, + action: suspend CoroutineScope.() -> T, + ): Job = scope.launch( + onError = onError, + onSuccess = onSuccess, + action = action + ) + + /** + * Launches a flow and collects it in the screenModelScope. The flow is collected on the default dispatcher. of the AppDispatcher. + * @param onError - error handler + * @param each - action for each element of the flow + * @return Job + * @see Flow + * @see Job + * @see AppDispatcher + * */ + protected fun Flow.launch( + onError: suspend (cause: Throwable) -> Unit = {}, + each: suspend (T) -> Unit + ): Job = scope.launch( + flow = this, + onError = onError, + each = each, + ) +} \ No newline at end of file diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/StoreAbstraction.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/StoreAbstraction.kt new file mode 100644 index 00000000..b22695d8 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/StoreAbstraction.kt @@ -0,0 +1,24 @@ +package com.stslex.core.ui.mvi + +import com.stslex.core.ui.mvi.StoreComponent.Action +import com.stslex.core.ui.mvi.StoreComponent.Event +import com.stslex.core.ui.mvi.StoreComponent.State +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow + +interface StoreAbstraction { + + /** Flow of the state of the screen. */ + val state: StateFlow + + /** Flow of events that are sent to the screen. */ + val event: SharedFlow + + /** + * Sends an action to the store. Checks if the action is not the same as the last action. + * If the action is not the same as the last action, the last action is updated. + * The action is then processed. + * @param action - action to be sent + */ + fun sendAction(action: A) +} \ No newline at end of file diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/StoreComponent.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/StoreComponent.kt new file mode 100644 index 00000000..6c7f5258 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/StoreComponent.kt @@ -0,0 +1,64 @@ +package com.stslex.core.ui.mvi + +import androidx.compose.material3.SnackbarDuration +import androidx.compose.runtime.Stable +import com.stslex.core.ui.components.SnackbarType + +interface StoreComponent { + + interface State : StoreComponent + + interface Navigation : StoreComponent + + interface Event : StoreComponent { + + @Stable + sealed class Snackbar( + open val message: String, + open val duration: SnackbarDuration, + open val withDismissAction: Boolean, + val action: String, + ) : Event { + + @Stable + data class Error( + override val message: String, + override val duration: SnackbarDuration = SnackbarDuration.Short, + override val withDismissAction: Boolean = false, + ) : Snackbar( + message = message, + action = SnackbarType.ERROR.label, + duration = duration, + withDismissAction = withDismissAction + ) + + @Stable + data class Success( + override val message: String, + override val duration: SnackbarDuration = SnackbarDuration.Short, + override val withDismissAction: Boolean = false, + ) : Snackbar( + message = message, + action = SnackbarType.SUCCESS.label, + duration = duration, + withDismissAction = withDismissAction + ) + + @Stable + data class Info( + override val message: String, + override val duration: SnackbarDuration = SnackbarDuration.Short, + override val withDismissAction: Boolean = false, + ) : Snackbar( + message = message, + action = SnackbarType.INFO.label, + duration = duration, + withDismissAction = withDismissAction + ) + } + } + + interface Action : StoreComponent { + interface RepeatLastAction : Action + } +} \ No newline at end of file diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/StoreExt.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/StoreExt.kt index a0b94945..8aae1dda 100644 --- a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/StoreExt.kt +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/StoreExt.kt @@ -2,13 +2,15 @@ package com.stslex.core.ui.mvi import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import cafe.adriel.voyager.core.model.ScreenModel -import cafe.adriel.voyager.core.model.rememberScreenModel -import cafe.adriel.voyager.core.screen.Screen +import androidx.lifecycle.ViewModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import com.stslex.core.ui.navigation.AppNavigator import org.koin.compose.getKoin +import org.koin.compose.koinInject +import org.koin.core.definition.Definition +import org.koin.core.definition.KoinDefinition +import org.koin.core.module.Module import org.koin.core.parameter.ParametersDefinition import org.koin.core.qualifier.Qualifier @@ -21,16 +23,21 @@ fun setupNavigator() { } } +expect inline fun Module.viewModelDefinition( + qualifier: Qualifier? = null, + noinline definition: Definition +): KoinDefinition + +inline fun > Module.storeDefinition( + qualifier: Qualifier? = null, + noinline definition: Definition +): KoinDefinition = viewModelDefinition(qualifier, definition) + @Composable -inline fun > Screen.getStoreTest( +inline fun > getStore( qualifier: Qualifier? = null, noinline parameters: ParametersDefinition? = null -): T { - val koin = getKoin() - return rememberScreenModel(tag = qualifier?.value) { - koin.get( - qualifier, - parameters - ) as ScreenModel - } as T -} +): T = koinInject( + qualifier = qualifier, + parameters = parameters +) diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/utils/PagingMapper.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/mapper/PagingMapper.kt similarity index 84% rename from core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/utils/PagingMapper.kt rename to core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/mapper/PagingMapper.kt index 8cd00072..6c4a20cc 100644 --- a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/utils/PagingMapper.kt +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/mapper/PagingMapper.kt @@ -1,4 +1,4 @@ -package com.stslex.core.ui.pager.utils +package com.stslex.core.ui.pager.mapper 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/pager/StorePagerFactory.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/pager/StorePagerFactory.kt index c3eaffe2..a698cebe 100644 --- 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 @@ -5,7 +5,7 @@ import com.stslex.core.core.paging.PagingCoreItem import com.stslex.core.core.paging.PagingResponse import com.stslex.core.ui.base.paging.PagingConfig import com.stslex.core.ui.base.paging.PagingItem -import com.stslex.core.ui.pager.utils.PagingMapper +import com.stslex.core.ui.pager.mapper.PagingMapper interface StorePagerFactory { 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 index 9081b999..b9dec3d7 100644 --- 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 @@ -5,10 +5,12 @@ import com.stslex.core.core.paging.PagingCoreItem import com.stslex.core.core.paging.PagingResponse import com.stslex.core.ui.base.paging.PagingConfig import com.stslex.core.ui.base.paging.PagingItem -import com.stslex.core.ui.pager.utils.PagingMapper -import com.stslex.core.ui.pager.utils.PagingWorkerImpl +import com.stslex.core.ui.pager.mapper.PagingMapper +import com.stslex.core.ui.pager.paging_worker.PagingWorkerFactory -class StorePagerFactoryImpl : StorePagerFactory { +class StorePagerFactoryImpl( + private val workerFactory: PagingWorkerFactory +) : StorePagerFactory { override fun create( scope: AppCoroutineScope, @@ -17,10 +19,15 @@ class StorePagerFactoryImpl : StorePagerFactory { config: PagingConfig ): StorePager { return StorePagerImpl( - pagingWorker = PagingWorkerImpl(scope = scope), + pagingWorker = workerFactory.create( + scope = scope, + delay = config.delay, + defaultLoadSize = config.defaultLoadSize, + queryLoadSize = config.queryLoadSize + ), request = request, mapper = mapper, pagingConfig = config ) } -} \ No newline at end of file +} diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/pager/StorePagerImpl.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/pager/StorePagerImpl.kt index e3ad71d0..e1de5ff6 100644 --- a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/pager/StorePagerImpl.kt +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/pager/StorePagerImpl.kt @@ -10,8 +10,9 @@ 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 com.stslex.core.ui.pager.mapper.PagingMapper +import com.stslex.core.ui.pager.paging_worker.PagingRequestType +import com.stslex.core.ui.pager.paging_worker.PagingWorker import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableSharedFlow @@ -82,13 +83,14 @@ class StorePagerImpl( } private fun requestItems( - isForceLoad: Boolean + isForceLoad: Boolean, + requestType: PagingRequestType = PagingRequestType.DEFAULT ) { if (loadJob?.isActive == true && isForceLoad.not()) { return } - loadJob?.cancel() - loadJob = pagingWorker.launch( + pagingWorker.launch( + requestType = requestType, action = { val page = state.value.page val pageSize = state.value.pageSize diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/paging_worker/PagingRequestType.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/paging_worker/PagingRequestType.kt new file mode 100644 index 00000000..c00cb4b5 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/paging_worker/PagingRequestType.kt @@ -0,0 +1,6 @@ +package com.stslex.core.ui.pager.paging_worker + +enum class PagingRequestType { + DEFAULT, + QUERY +} \ No newline at end of file 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/paging_worker/PagingWorker.kt similarity index 72% rename from core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/utils/PagingWorker.kt rename to core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/paging_worker/PagingWorker.kt index 13c62f99..3de5db0c 100644 --- 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/paging_worker/PagingWorker.kt @@ -1,18 +1,18 @@ -package com.stslex.core.ui.pager.utils +package com.stslex.core.ui.pager.paging_worker 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( + requestType: PagingRequestType = PagingRequestType.DEFAULT, + isForceLoad: Boolean = false, 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/paging_worker/PagingWorkerFactory.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/paging_worker/PagingWorkerFactory.kt new file mode 100644 index 00000000..69480db1 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/paging_worker/PagingWorkerFactory.kt @@ -0,0 +1,14 @@ +package com.stslex.core.ui.pager.paging_worker + +import com.stslex.core.core.coroutine.AppCoroutineScope + +interface PagingWorkerFactory { + + fun create( + scope: AppCoroutineScope, + delay: Long, + defaultLoadSize: Int, + queryLoadSize: Int + ): PagingWorker +} + diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/paging_worker/PagingWorkerFactoryImpl.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/paging_worker/PagingWorkerFactoryImpl.kt new file mode 100644 index 00000000..ee6c8763 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/paging_worker/PagingWorkerFactoryImpl.kt @@ -0,0 +1,20 @@ +package com.stslex.core.ui.pager.paging_worker + +import com.stslex.core.core.coroutine.AppCoroutineScope + +class PagingWorkerFactoryImpl : PagingWorkerFactory { + + override fun create( + scope: AppCoroutineScope, + delay: Long, + defaultLoadSize: Int, + queryLoadSize: Int + ): PagingWorker { + return PagingWorkerImpl( + scope = scope, + delay = delay, + defaultLoadSize = defaultLoadSize, + queryLoadSize = queryLoadSize + ) + } +} \ No newline at end of file diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/paging_worker/PagingWorkerImpl.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/paging_worker/PagingWorkerImpl.kt new file mode 100644 index 00000000..c882bec6 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/paging_worker/PagingWorkerImpl.kt @@ -0,0 +1,111 @@ +package com.stslex.core.ui.pager.paging_worker + +import com.stslex.core.core.coroutine.AppCoroutineScope +import com.stslex.core.core.paging.PagingCoreItem +import com.stslex.core.core.paging.PagingResponse +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay + +class PagingWorkerImpl( + private val scope: AppCoroutineScope, + private val delay: Long, + private val defaultLoadSize: Int, + private val queryLoadSize: Int +) : PagingWorker { + + private val _jobs: MutableList = mutableListOf() + private val jobs: List + get() = _jobs.toList() + + override fun launch( + requestType: PagingRequestType, + isForceLoad: Boolean, + onError: suspend (Throwable) -> Unit, + onSuccess: suspend CoroutineScope.(PagingResponse) -> Unit, + action: suspend CoroutineScope.() -> PagingResponse + ) { + if (isForceLoad) { + cancel() + } + when (requestType) { + PagingRequestType.DEFAULT -> startDefaultJob( + onError = onError, + onSuccess = onSuccess, + action = action + ) + + PagingRequestType.QUERY -> startQueryJob( + onError = onError, + onSuccess = onSuccess, + action = action + ) + } + } + + private fun startDefaultJob( + onError: suspend (Throwable) -> Unit, + onSuccess: suspend CoroutineScope.(PagingResponse) -> Unit, + action: suspend CoroutineScope.() -> PagingResponse + ) { + if (jobs.size >= defaultLoadSize) { + return + } + startNewJob( + onError = onError, + onSuccess = onSuccess, + action = action + ) + } + + private fun startQueryJob( + onError: suspend (Throwable) -> Unit, + onSuccess: suspend CoroutineScope.(PagingResponse) -> Unit, + action: suspend CoroutineScope.() -> PagingResponse + ) { + if (jobs.size >= queryLoadSize) { + val removeJobs = jobs.subList(1, jobs.size.dec()) + removeJobs.forEach { it.cancel() } + _jobs.removeAll(removeJobs) + } + startNewJob( + onError = onError, + onSuccess = onSuccess, + action = action + ) + } + + private fun startNewJob( + onError: suspend (Throwable) -> Unit, + onSuccess: suspend CoroutineScope.(PagingResponse) -> Unit, + action: suspend CoroutineScope.() -> PagingResponse + ) { + val newJob = scope.launch( + start = CoroutineStart.LAZY, + onError = onError, + onSuccess = onSuccess, + action = { + delay(delay) + action() + } + ) + val lastJob = jobs.lastOrNull() + if (lastJob == null || lastJob.isActive.not()) { + lastJob?.let(_jobs::remove) + newJob.start() + _jobs.add(newJob) + return + } + lastJob.invokeOnCompletion { + _jobs.remove(lastJob) + newJob.start() + _jobs.add(newJob) + } + } + + override fun cancel() { + _jobs.forEach { it.cancel() } + _jobs.clear() + } +} \ No newline at end of file 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 deleted file mode 100644 index cc8a9068..00000000 --- a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/pager/utils/PagingWorkerImpl.kt +++ /dev/null @@ -1,69 +0,0 @@ -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/core/ui/src/desktopMain/kotlin/com/stslex/core/ui/base/image/NativeNetworkImage.desktop.kt b/core/ui/src/desktopMain/kotlin/com/stslex/core/ui/base/image/NativeNetworkImage.desktop.kt deleted file mode 100644 index 6cf21c58..00000000 --- a/core/ui/src/desktopMain/kotlin/com/stslex/core/ui/base/image/NativeNetworkImage.desktop.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.stslex.core.ui.base.image - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale - -@Composable -actual fun NativeNetworkImage( - url: String, - contentDescription: String?, - contentScale: ContentScale, - modifier: Modifier -) { - // TODO implement native loader - KamelNetworkImage( - url = url, - contentDescription = contentDescription, - contentScale = contentScale, - modifier = modifier - ) -} \ No newline at end of file diff --git a/core/ui/src/iosMain/kotlin/com/stslex/core/ui/mvi/StoreExt.ios.kt b/core/ui/src/iosMain/kotlin/com/stslex/core/ui/mvi/StoreExt.ios.kt new file mode 100644 index 00000000..37b75872 --- /dev/null +++ b/core/ui/src/iosMain/kotlin/com/stslex/core/ui/mvi/StoreExt.ios.kt @@ -0,0 +1,15 @@ +package com.stslex.core.ui.mvi + +import androidx.lifecycle.ViewModel +import org.koin.core.definition.Definition +import org.koin.core.definition.KoinDefinition +import org.koin.core.module.Module +import org.koin.core.qualifier.Qualifier + +actual inline fun Module.viewModelDefinition( + qualifier: Qualifier?, + noinline definition: Definition, +): KoinDefinition = factory( + qualifier = qualifier, + definition = definition +) \ No newline at end of file diff --git a/feature/auth/build.gradle.kts b/feature/auth/build.gradle.kts index 165822b6..ec3b4b8a 100644 --- a/feature/auth/build.gradle.kts +++ b/feature/auth/build.gradle.kts @@ -14,8 +14,6 @@ kotlin { } } - jvm("desktop") - iosX64() iosArm64() iosSimulatorArm64() diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/di/AuthModule.kt b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/di/AuthModule.kt index deba1e10..baaa4f5e 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/di/AuthModule.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/di/AuthModule.kt @@ -1,5 +1,6 @@ package com.stslex.feature.auth.di +import com.stslex.core.ui.mvi.storeDefinition import com.stslex.feature.auth.data.AuthRepository import com.stslex.feature.auth.data.AuthRepositoryImpl import com.stslex.feature.auth.domain.AuthInteractor @@ -7,15 +8,14 @@ import com.stslex.feature.auth.domain.AuthInteractorImpl import com.stslex.feature.auth.navigation.AuthRouter import com.stslex.feature.auth.navigation.AuthRouterImpl import com.stslex.feature.auth.ui.store.AuthStore -import com.stslex.feature.auth.ui.store.AuthStoreImpl import org.koin.dsl.module val featureAuthModule = module { factory { AuthRepositoryImpl(client = get()) } factory { AuthInteractorImpl(authRepository = get()) } factory { AuthRouterImpl(navigator = get()) } - factory { - AuthStoreImpl( + storeDefinition { + AuthStore( interactor = get(), router = get(), dispatcher = get() diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/navigation/AuthRouter.kt b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/navigation/AuthRouter.kt index df1b4d80..bd545d46 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/navigation/AuthRouter.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/navigation/AuthRouter.kt @@ -1,7 +1,7 @@ package com.stslex.feature.auth.navigation import com.stslex.core.ui.mvi.Router -import com.stslex.feature.auth.ui.store.AuthStore.Navigation +import com.stslex.feature.auth.ui.store.AuthStoreComponent.Navigation interface AuthRouter : Router diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/navigation/AuthRouterImpl.kt b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/navigation/AuthRouterImpl.kt index 49d26c4e..7659fd3f 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/navigation/AuthRouterImpl.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/navigation/AuthRouterImpl.kt @@ -2,7 +2,7 @@ package com.stslex.feature.auth.navigation import com.stslex.core.ui.navigation.AppNavigator import com.stslex.core.ui.navigation.AppScreen -import com.stslex.feature.auth.ui.store.AuthStore.Navigation +import com.stslex.feature.auth.ui.store.AuthStoreComponent.Navigation class AuthRouterImpl( private val navigator: AppNavigator diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/AuthScreen.kt b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/AuthScreen.kt index 9845098d..b910aadc 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/AuthScreen.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/AuthScreen.kt @@ -21,7 +21,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import cafe.adriel.voyager.core.screen.Screen import com.stslex.core.ui.components.AppSnackbarHost -import com.stslex.core.ui.mvi.getStoreTest +import com.stslex.core.ui.mvi.getStore import com.stslex.core.ui.mvi.setupNavigator import com.stslex.core.ui.theme.AppDimension import com.stslex.core.ui.theme.toPx @@ -30,16 +30,16 @@ import com.stslex.feature.auth.ui.components.AuthTitle import com.stslex.feature.auth.ui.model.screen.AuthScreenState import com.stslex.feature.auth.ui.model.screen.rememberAuthScreenState import com.stslex.feature.auth.ui.store.AuthStore -import com.stslex.feature.auth.ui.store.AuthStore.AuthFieldsState -import com.stslex.feature.auth.ui.store.AuthStore.Event -import com.stslex.feature.auth.ui.store.AuthStore.ScreenLoadingState +import com.stslex.feature.auth.ui.store.AuthStoreComponent.AuthFieldsState +import com.stslex.feature.auth.ui.store.AuthStoreComponent.Event +import com.stslex.feature.auth.ui.store.AuthStoreComponent.ScreenLoadingState object AuthScreen : Screen { @Composable override fun Content() { setupNavigator() - val store = getStoreTest() + val store = getStore() val state by remember { store.state }.collectAsState() val snackbarHostState = remember { SnackbarHostState() } diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/components/AuthSubmitButton.kt b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/components/AuthSubmitButton.kt index 42df3356..7b201609 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/components/AuthSubmitButton.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/components/AuthSubmitButton.kt @@ -11,7 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.style.TextAlign -import com.stslex.feature.auth.ui.store.AuthStore.AuthFieldsState +import com.stslex.feature.auth.ui.store.AuthStoreComponent.AuthFieldsState @Composable internal fun AuthSubmitButton( diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/components/AuthTitle.kt b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/components/AuthTitle.kt index e45f913e..1f7984b3 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/components/AuthTitle.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/components/AuthTitle.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.draw.scale import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.stslex.core.ui.theme.AppDimension -import com.stslex.feature.auth.ui.store.AuthStore.AuthFieldsState +import com.stslex.feature.auth.ui.store.AuthStoreComponent.AuthFieldsState import kotlinx.coroutines.launch @OptIn(ExperimentalMaterialApi::class) diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/model/screen/AuthScreenState.kt b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/model/screen/AuthScreenState.kt index a6391525..e691431c 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/model/screen/AuthScreenState.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/model/screen/AuthScreenState.kt @@ -23,12 +23,11 @@ import com.stslex.feature.auth.ui.model.screen.text_field.rememberLoginTextField import com.stslex.feature.auth.ui.model.screen.text_field.rememberPasswordInputTextFieldState import com.stslex.feature.auth.ui.model.screen.text_field.rememberPasswordSubmitTextFieldState import com.stslex.feature.auth.ui.model.screen.text_field.rememberUsernameTextFieldState -import com.stslex.feature.auth.ui.store.AuthStore.Action -import com.stslex.feature.auth.ui.store.AuthStore.AuthFieldsState -import com.stslex.feature.auth.ui.store.AuthStore.ScreenLoadingState -import com.stslex.feature.auth.ui.store.AuthStore.State +import com.stslex.feature.auth.ui.store.AuthStoreComponent.Action +import com.stslex.feature.auth.ui.store.AuthStoreComponent.AuthFieldsState +import com.stslex.feature.auth.ui.store.AuthStoreComponent.ScreenLoadingState +import com.stslex.feature.auth.ui.store.AuthStoreComponent.State -@OptIn(ExperimentalComposeUiApi::class) @Stable data class AuthScreenState @OptIn(ExperimentalMaterialApi::class) constructor( val screenLoadingState: ScreenLoadingState = ScreenLoadingState.Content, diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/model/screen/text_field/LoginTextFieldState.kt b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/model/screen/text_field/LoginTextFieldState.kt index 6ef1a278..317a8a5d 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/model/screen/text_field/LoginTextFieldState.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/model/screen/text_field/LoginTextFieldState.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import com.stslex.feature.auth.ui.model.screen.text_field.base.AuthTextField -import com.stslex.feature.auth.ui.store.AuthStore.Action.InputAction +import com.stslex.feature.auth.ui.store.AuthStoreComponent.Action.InputAction @Stable data class LoginTextFieldState( diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/model/screen/text_field/PasswordInputTextFieldState.kt b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/model/screen/text_field/PasswordInputTextFieldState.kt index f15c3ec3..0b0dccd3 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/model/screen/text_field/PasswordInputTextFieldState.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/model/screen/text_field/PasswordInputTextFieldState.kt @@ -5,10 +5,10 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.ui.hapticfeedback.HapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback -import com.stslex.feature.auth.ui.model.screen.text_field.hidden.HiddenState import com.stslex.feature.auth.ui.model.screen.text_field.base.PasswordTextFieldState -import com.stslex.feature.auth.ui.store.AuthStore.Action.InputAction -import com.stslex.feature.auth.ui.store.AuthStore.AuthFieldsState +import com.stslex.feature.auth.ui.model.screen.text_field.hidden.HiddenState +import com.stslex.feature.auth.ui.store.AuthStoreComponent.Action.InputAction +import com.stslex.feature.auth.ui.store.AuthStoreComponent.AuthFieldsState @Stable data class PasswordInputTextFieldState( diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/model/screen/text_field/PasswordSubmitTextFieldState.kt b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/model/screen/text_field/PasswordSubmitTextFieldState.kt index 2ee82081..5dc9bd3e 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/model/screen/text_field/PasswordSubmitTextFieldState.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/model/screen/text_field/PasswordSubmitTextFieldState.kt @@ -5,9 +5,9 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.ui.hapticfeedback.HapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback -import com.stslex.feature.auth.ui.model.screen.text_field.hidden.HiddenState import com.stslex.feature.auth.ui.model.screen.text_field.base.PasswordTextFieldState -import com.stslex.feature.auth.ui.store.AuthStore.Action.InputAction.PasswordSubmitInput +import com.stslex.feature.auth.ui.model.screen.text_field.hidden.HiddenState +import com.stslex.feature.auth.ui.store.AuthStoreComponent.Action.InputAction.PasswordSubmitInput @Stable data class PasswordSubmitTextFieldState( diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/model/screen/text_field/UsernameTextFieldState.kt b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/model/screen/text_field/UsernameTextFieldState.kt index c9635133..fdbf4e52 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/model/screen/text_field/UsernameTextFieldState.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/model/screen/text_field/UsernameTextFieldState.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import com.stslex.feature.auth.ui.model.screen.text_field.base.AuthTextField -import com.stslex.feature.auth.ui.store.AuthStore.Action.InputAction +import com.stslex.feature.auth.ui.store.AuthStoreComponent.Action.InputAction @Stable data class UsernameTextFieldState( diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/store/AuthStore.kt b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/store/AuthStore.kt index dc4a8741..b99fe1c3 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/store/AuthStore.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/store/AuthStore.kt @@ -1,99 +1,172 @@ package com.stslex.feature.auth.ui.store +import com.stslex.core.core.AppDispatcher import com.stslex.core.ui.mvi.Store -import com.stslex.core.ui.mvi.Store.Event.Snackbar -import com.stslex.feature.auth.ui.store.AuthStore.Action -import com.stslex.feature.auth.ui.store.AuthStore.Event -import com.stslex.feature.auth.ui.store.AuthStore.State - -interface AuthStore : Store { +import com.stslex.core.ui.mvi.StoreComponent.Event.Snackbar +import com.stslex.feature.auth.domain.AuthInteractor +import com.stslex.feature.auth.navigation.AuthRouter +import com.stslex.feature.auth.ui.store.AuthStoreComponent.Action +import com.stslex.feature.auth.ui.store.AuthStoreComponent.AuthFieldsState +import com.stslex.feature.auth.ui.store.AuthStoreComponent.Event +import com.stslex.feature.auth.ui.store.AuthStoreComponent.Navigation +import com.stslex.feature.auth.ui.store.AuthStoreComponent.ScreenLoadingState +import com.stslex.feature.auth.ui.store.AuthStoreComponent.State +import kotlinx.coroutines.delay + +class AuthStore( + private val interactor: AuthInteractor, + dispatcher: AppDispatcher, + router: AuthRouter +) : Store( + appDispatcher = dispatcher, + initialState = State.INITIAL, + router = router +) { + + override fun process(action: Action) { + when (action) { + is Action.InputAction.PasswordInput -> processPasswordInput(action) + is Action.InputAction.PasswordSubmitInput -> processPasswordSubmitInput(action) + is Action.InputAction.LoginInput -> processLoginInput(action) + is Action.InputAction.UsernameInput -> processUsernameInput(action) + is Action.OnAuthFieldChange -> processAuthFieldChange(action) + is Action.OnSubmitClicked -> processSubmitClicked(action) + } + } - data class State( - val screenLoadingState: ScreenLoadingState, - val login: String, - val username: String, - val password: String, - val passwordSubmit: String, - val authFieldsState: AuthFieldsState - ) : Store.State { - companion object { - val INITIAL = State( - screenLoadingState = ScreenLoadingState.Content, - login = "", - username = "", - password = "", - passwordSubmit = "", - authFieldsState = AuthFieldsState.AUTH + private fun processLoginInput(action: Action.InputAction.LoginInput) { + updateState { currentValue -> + currentValue.copy( + login = action.value ) } } - sealed interface Event : Store.Event { - - data class ShowSnackbar( - val snackbar: Snackbar - ) : Event + private fun processUsernameInput(action: Action.InputAction.UsernameInput) { + updateState { currentValue -> + currentValue.copy( + username = action.value + ) + } } - sealed interface Action : Store.Action { - data class OnSubmitClicked( - val state: AuthFieldsState - ) : Action - - data class OnAuthFieldChange( - val targetState: AuthFieldsState - ) : Action - - sealed class InputAction( - open val value: String - ) : Action { - - data class LoginInput( - override val value: String - ) : InputAction(value) - - data class UsernameInput( - override val value: String - ) : InputAction(value) - - data class PasswordInput( - override val value: String - ) : InputAction(value) - - data class PasswordSubmitInput( - override val value: String - ) : InputAction(value) + private fun processPasswordInput(action: Action.InputAction.PasswordInput) { + updateState { currentValue -> + currentValue.copy( + password = action.value + ) } } - sealed interface Navigation : Store.Navigation { - - data object HomeFeature : Navigation + private fun processPasswordSubmitInput(action: Action.InputAction.PasswordSubmitInput) { + updateState { currentValue -> + currentValue.copy( + passwordSubmit = action.value, + ) + } } - // TODO get from resources - enum class AuthFieldsState( - val buttonRes: String, - val titleRes: String - ) { - AUTH( - buttonRes = "log in (if not have account)", - titleRes = "Auth" - ), - REGISTER( - buttonRes = "register(if not have account)", - titleRes = "Register" - ); + private fun processAuthFieldChange(action: Action.OnAuthFieldChange) { + updateState { currentValue -> + currentValue.copy( + authFieldsState = action.targetState + ) + } } - sealed interface ScreenLoadingState { + private fun processSubmitClicked(action: Action.OnSubmitClicked) { + when (action.state) { + AuthFieldsState.AUTH -> auth() + AuthFieldsState.REGISTER -> register() + } + } - data object Loading : ScreenLoadingState + private fun register() { + val state = state.value + setLoadingState(ScreenLoadingState.Loading) + + launch( + action = { + interactor.register( + login = state.login, + username = state.username, + password = state.password + ) + }, + onError = { error -> + sendEvent( + Event.ShowSnackbar( + snackbar = Snackbar.Error( + message = error.message ?: "Unknown error" + ) + ) + ) + setLoadingState(ScreenLoadingState.Content) + }, + onSuccess = { + sendEvent( + Event.ShowSnackbar( + snackbar = Snackbar.Success( + message = "Success register" + ) + ) + ) + delay(1000L) + updateState { currentState -> + currentState.copy( + screenLoadingState = ScreenLoadingState.Content, + authFieldsState = AuthFieldsState.AUTH + ) + } + consumeNavigation(Navigation.HomeFeature) + }) + } - data object Content : ScreenLoadingState + private fun auth() { + val state = state.value + setLoadingState(ScreenLoadingState.Loading) + launch( + action = { + interactor.auth( + login = state.login, + password = state.password + ) + }, + onError = { error -> + sendEvent( + Event.ShowSnackbar( + snackbar = Snackbar.Error( + message = error.message ?: "Unknown error" + ) + ) + ) + setLoadingState(ScreenLoadingState.Content) + }, + onSuccess = { + sendEvent( + Event.ShowSnackbar( + snackbar = Snackbar.Success( + message = "Success auth" + ) + ) + ) + delay(1000L) + updateState { currentState -> + currentState.copy( + screenLoadingState = ScreenLoadingState.Content, + authFieldsState = AuthFieldsState.AUTH + ) + } + consumeNavigation(Navigation.HomeFeature) + }) + } - data class Error( - val error: Throwable - ) : ScreenLoadingState + private fun setLoadingState(screenLoadingState: ScreenLoadingState) { + updateState { state -> + state.copy( + screenLoadingState = screenLoadingState + ) + } } -} \ No newline at end of file +} + diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/store/AuthStoreComponent.kt b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/store/AuthStoreComponent.kt new file mode 100644 index 00000000..715e2839 --- /dev/null +++ b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/store/AuthStoreComponent.kt @@ -0,0 +1,95 @@ +package com.stslex.feature.auth.ui.store + +import com.stslex.core.ui.mvi.StoreComponent + +interface AuthStoreComponent : StoreComponent { + + data class State( + val screenLoadingState: ScreenLoadingState, + val login: String, + val username: String, + val password: String, + val passwordSubmit: String, + val authFieldsState: AuthFieldsState + ) : StoreComponent.State { + companion object { + val INITIAL = State( + screenLoadingState = ScreenLoadingState.Content, + login = "", + username = "", + password = "", + passwordSubmit = "", + authFieldsState = AuthFieldsState.AUTH + ) + } + } + + sealed interface Event : StoreComponent.Event { + + data class ShowSnackbar( + val snackbar: StoreComponent.Event.Snackbar + ) : Event + } + + sealed interface Action : StoreComponent.Action { + data class OnSubmitClicked( + val state: AuthFieldsState + ) : Action + + data class OnAuthFieldChange( + val targetState: AuthFieldsState + ) : Action + + sealed class InputAction( + open val value: String + ) : Action { + + data class LoginInput( + override val value: String + ) : InputAction(value) + + data class UsernameInput( + override val value: String + ) : InputAction(value) + + data class PasswordInput( + override val value: String + ) : InputAction(value) + + data class PasswordSubmitInput( + override val value: String + ) : InputAction(value) + } + } + + sealed interface Navigation : StoreComponent.Navigation { + + data object HomeFeature : Navigation + } + + // TODO get from resources + enum class AuthFieldsState( + val buttonRes: String, + val titleRes: String + ) { + AUTH( + buttonRes = "log in (if not have account)", + titleRes = "Auth" + ), + REGISTER( + buttonRes = "register(if not have account)", + titleRes = "Register" + ); + } + + sealed interface ScreenLoadingState { + + data object Loading : ScreenLoadingState + + data object Content : ScreenLoadingState + + data class Error( + val error: Throwable + ) : ScreenLoadingState + } +} \ No newline at end of file diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/store/AuthStoreImpl.kt b/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/store/AuthStoreImpl.kt deleted file mode 100644 index a1090bb2..00000000 --- a/feature/auth/src/commonMain/kotlin/com/stslex/feature/auth/ui/store/AuthStoreImpl.kt +++ /dev/null @@ -1,172 +0,0 @@ -package com.stslex.feature.auth.ui.store - -import com.stslex.core.core.AppDispatcher -import com.stslex.core.ui.mvi.BaseStore -import com.stslex.core.ui.mvi.Store.Event.Snackbar -import com.stslex.feature.auth.domain.AuthInteractor -import com.stslex.feature.auth.navigation.AuthRouter -import com.stslex.feature.auth.ui.store.AuthStore.Action -import com.stslex.feature.auth.ui.store.AuthStore.AuthFieldsState -import com.stslex.feature.auth.ui.store.AuthStore.Event -import com.stslex.feature.auth.ui.store.AuthStore.Navigation -import com.stslex.feature.auth.ui.store.AuthStore.ScreenLoadingState -import com.stslex.feature.auth.ui.store.AuthStore.State -import kotlinx.coroutines.delay - -class AuthStoreImpl( - private val interactor: AuthInteractor, - dispatcher: AppDispatcher, - router: AuthRouter -) : AuthStore, BaseStore( - appDispatcher = dispatcher, - initialState = State.INITIAL, - router = router -) { - - override fun process(action: Action) { - when (action) { - is Action.InputAction.PasswordInput -> processPasswordInput(action) - is Action.InputAction.PasswordSubmitInput -> processPasswordSubmitInput(action) - is Action.InputAction.LoginInput -> processLoginInput(action) - is Action.InputAction.UsernameInput -> processUsernameInput(action) - is Action.OnAuthFieldChange -> processAuthFieldChange(action) - is Action.OnSubmitClicked -> processSubmitClicked(action) - } - } - - private fun processLoginInput(action: Action.InputAction.LoginInput) { - updateState { currentValue -> - currentValue.copy( - login = action.value - ) - } - } - - private fun processUsernameInput(action: Action.InputAction.UsernameInput) { - updateState { currentValue -> - currentValue.copy( - username = action.value - ) - } - } - - private fun processPasswordInput(action: Action.InputAction.PasswordInput) { - updateState { currentValue -> - currentValue.copy( - password = action.value - ) - } - } - - private fun processPasswordSubmitInput(action: Action.InputAction.PasswordSubmitInput) { - updateState { currentValue -> - currentValue.copy( - passwordSubmit = action.value, - ) - } - } - - private fun processAuthFieldChange(action: Action.OnAuthFieldChange) { - updateState { currentValue -> - currentValue.copy( - authFieldsState = action.targetState - ) - } - } - - private fun processSubmitClicked(action: Action.OnSubmitClicked) { - when (action.state) { - AuthFieldsState.AUTH -> auth() - AuthFieldsState.REGISTER -> register() - } - } - - private fun register() { - val state = state.value - setLoadingState(ScreenLoadingState.Loading) - - launch( - action = { - interactor.register( - login = state.login, - username = state.username, - password = state.password - ) - }, - onError = { error -> - sendEvent( - Event.ShowSnackbar( - snackbar = Snackbar.Error( - message = error.message ?: "Unknown error" - ) - ) - ) - setLoadingState(ScreenLoadingState.Content) - }, - onSuccess = { - sendEvent( - Event.ShowSnackbar( - snackbar = Snackbar.Success( - message = "Success register" - ) - ) - ) - delay(1000L) - updateState { currentState -> - currentState.copy( - screenLoadingState = ScreenLoadingState.Content, - authFieldsState = AuthFieldsState.AUTH - ) - } - navigate(Navigation.HomeFeature) - }) - } - - private fun auth() { - val state = state.value - setLoadingState(ScreenLoadingState.Loading) - launch( - action = { - interactor.auth( - login = state.login, - password = state.password - ) - }, - onError = { error -> - sendEvent( - Event.ShowSnackbar( - snackbar = Snackbar.Error( - message = error.message ?: "Unknown error" - ) - ) - ) - setLoadingState(ScreenLoadingState.Content) - }, - onSuccess = { - sendEvent( - Event.ShowSnackbar( - snackbar = Snackbar.Success( - message = "Success auth" - ) - ) - ) - delay(1000L) - updateState { currentState -> - currentState.copy( - screenLoadingState = ScreenLoadingState.Content, - authFieldsState = AuthFieldsState.AUTH - ) - } - navigate(Navigation.HomeFeature) - }) - } - - private fun setLoadingState(screenLoadingState: ScreenLoadingState) { - updateState { state -> - state.copy( - screenLoadingState = screenLoadingState - ) - } - } -} - diff --git a/feature/favourite/build.gradle.kts b/feature/favourite/build.gradle.kts index b7fda26e..681c5104 100644 --- a/feature/favourite/build.gradle.kts +++ b/feature/favourite/build.gradle.kts @@ -14,8 +14,6 @@ kotlin { } } - jvm("desktop") - iosX64() iosArm64() iosSimulatorArm64() 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 58401dfe..5a1fab44 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 @@ -6,10 +6,10 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import cafe.adriel.voyager.core.screen.Screen -import com.stslex.core.ui.mvi.getStoreTest +import com.stslex.core.ui.mvi.getStore import com.stslex.feature.favourite.ui.components.FavouriteScreen import com.stslex.feature.favourite.ui.store.FavouriteStore -import com.stslex.feature.favourite.ui.store.FavouriteStore.Action +import com.stslex.feature.favourite.ui.store.FavouriteStoreComponent.Action data class FavouriteScreen( val uuid: String @@ -17,7 +17,7 @@ data class FavouriteScreen( @Composable override fun Content() { - val store = getStoreTest() + val store = getStore() val state by remember { store.state }.collectAsState() LaunchedEffect(key1 = Unit) { 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 caaf74c0..8a369a46 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 @@ -1,5 +1,6 @@ package com.stslex.feature.favourite.di +import com.stslex.core.ui.mvi.storeDefinition import com.stslex.feature.favourite.data.repository.FavouriteRepository import com.stslex.feature.favourite.data.repository.FavouriteRepositoryImpl import com.stslex.feature.favourite.domain.interactor.FavouriteInteractor @@ -7,7 +8,6 @@ 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 { @@ -17,8 +17,8 @@ val featureFavouriteModule = module { FavouriteInteractorImpl(repository = get()) } factory { FavouriteRouterImpl(navigator = get()) } - factory { - FavouriteStoreImpl( + storeDefinition { + FavouriteStore( interactor = get(), appDispatcher = get(), router = get(), 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 b7792e84..14b352d2 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.FavouriteStore.Navigation +import com.stslex.feature.favourite.ui.store.FavouriteStoreComponent.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 f7148e1f..d9f32528 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.FavouriteStore.Navigation -import com.stslex.feature.favourite.ui.store.FavouriteStore.Navigation.OpenFilm +import com.stslex.feature.favourite.ui.store.FavouriteStoreComponent.Navigation +import com.stslex.feature.favourite.ui.store.FavouriteStoreComponent.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 d3d462a8..6b652ca0 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.FavouriteStore -import com.stslex.feature.favourite.ui.store.FavouriteStore.Action +import com.stslex.feature.favourite.ui.store.FavouriteStoreComponent +import com.stslex.feature.favourite.ui.store.FavouriteStoreComponent.Action @Composable internal fun FavouriteScreen( - state: FavouriteStore.State, + state: FavouriteStoreComponent.State, onAction: (Action) -> Unit ) { Box( 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 619c9b7b..2b573f5f 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,74 +1,172 @@ package com.stslex.feature.favourite.ui.store -import androidx.compose.runtime.Stable -import com.stslex.core.ui.base.paging.PagingConfig -import com.stslex.core.ui.base.paging.PagingUiState +import com.stslex.core.core.AppDispatcher +import com.stslex.core.ui.base.mapToAppError +import com.stslex.core.ui.base.paging.toUi import com.stslex.core.ui.mvi.Store -import com.stslex.core.ui.mvi.Store.Event.Snackbar +import com.stslex.core.ui.mvi.StoreComponent.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.store.FavouriteStore.Action -import com.stslex.feature.favourite.ui.store.FavouriteStore.Event -import com.stslex.feature.favourite.ui.store.FavouriteStore.State - -interface FavouriteStore : Store { - - @Stable - data class State( - val uuid: String, - val query: String, - val paging: PagingUiState, - val screen: FavouriteScreenState, - val isLoading: Boolean - ) : Store.State { - - companion object { - - val INITIAL = State( - uuid = "", - query = "", - paging = PagingUiState.default(PagingConfig.DEFAULT), - screen = FavouriteScreenState.Shimmer, - isLoading = true +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 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, + pagingFactory: StorePagerFactory, +) : Store( + router = router, + appDispatcher = appDispatcher, + initialState = State.INITIAL +) { + + 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() }, + config = state.value.paging.config + ) + + 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() } } - @Stable - sealed interface Action : Store.Action { - - @Stable - data class Init( - val uuid: String - ) : Action + private fun actionInit(action: Action.Init) { + updateState { state -> + state.copy( + uuid = action.uuid, + ) + } - @Stable - data object LoadMore : Action + pager.state.launch { pagerState -> + updateState { currentState -> + currentState.copy( + paging = pagerState.toUi(currentState.paging.config) + ) + } + } + pager.loadState.launch { loadState -> + updateState { currentState -> + currentState.copy( + screen = loadState.toUi() + ) + } + } + pager.loadEvents.launch { + sendEvent( + Event.ShowSnackbar(Snackbar.Error("error load matches")) + ) + } - @Stable - data object Refresh : Action + updateState { currentState -> + currentState.copy(uuid = action.uuid) + } - @Stable - data object Retry : Action + state + .map { it.query } + .distinctUntilChanged() + .launch( + onError = ::showError + ) { + if (pager.loadState.value is PagerLoadState.Initial) { + pager.initialLoad() + } else { + pager.refresh(isForceLoad = false) + } + } + } - @Stable - data class LikeClick(val uuid: String) : Action + private fun actionInputSearch(action: Action.InputSearch) { + updateState { state -> + state.copy(query = action.query) + } + } - @Stable - data class ItemClick(val uuid: String) : Action + private fun actionItemClick(action: Action.ItemClick) { + consumeNavigation(Navigation.OpenFilm(action.uuid)) + } - @Stable - data class InputSearch(val query: String) : Action + private fun actionLikeClick(action: Action.LikeClick) { + if (likeJob?.isActive == true) return + val items = state.value.paging.items.toMutableList() + val itemIndex = items + .indexOfFirst { + it.uuid == action.uuid + } + .takeIf { it != -1 } + ?: return + val item = state.value.paging.items.getOrNull(itemIndex) ?: return + val newItem = item.copy(isFavourite = item.isFavourite.not()) + items[itemIndex] = newItem + updateState { state -> + state.copy( + paging = state.paging.copy( + items = items.toImmutableList() + ) + ) + } + likeJob = launch( + onSuccess = { /* do nothing */ }, + onError = ::showError, + action = { + interactor.setFavourite(newItem.toDomain()) + }) } - @Stable - sealed interface Event : Store.Event { + private fun actionLoadMore() { + pager.load() + } - @Stable - data class ShowSnackbar(val snackbar: Snackbar) : Event + private fun actionRefresh() { + pager.refresh(isForceLoad = true) } - sealed interface Navigation : Store.Navigation { + private fun actionRetryClick() { + pager.retry() + } - data class OpenFilm(val uuid: String) : Navigation + 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) + ) + } + } } -} \ 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 new file mode 100644 index 00000000..33525170 --- /dev/null +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/store/FavouriteStoreComponent.kt @@ -0,0 +1,71 @@ +package com.stslex.feature.favourite.ui.store + +import androidx.compose.runtime.Stable +import com.stslex.core.ui.base.paging.PagingConfig +import com.stslex.core.ui.base.paging.PagingUiState +import com.stslex.core.ui.mvi.StoreComponent +import com.stslex.core.ui.mvi.StoreComponent.Event.Snackbar +import com.stslex.feature.favourite.ui.model.FavouriteModel + +interface FavouriteStoreComponent : StoreComponent { + + @Stable + data class State( + val uuid: String, + val query: String, + val paging: PagingUiState, + val screen: FavouriteScreenState, + val isLoading: Boolean + ) : StoreComponent.State { + + companion object { + + val INITIAL = State( + uuid = "", + query = "", + paging = PagingUiState.default(PagingConfig.DEFAULT), + screen = FavouriteScreenState.Shimmer, + isLoading = true + ) + } + } + + @Stable + sealed interface Action : StoreComponent.Action { + + @Stable + data class Init( + val uuid: String + ) : Action + + @Stable + data object LoadMore : Action + + @Stable + data object Refresh : Action + + @Stable + data object Retry : 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 : StoreComponent.Event { + + @Stable + data class ShowSnackbar(val snackbar: Snackbar) : Event + } + + sealed interface Navigation : StoreComponent.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 deleted file mode 100644 index fecf6884..00000000 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/feature/favourite/ui/store/FavouriteStoreImpl.kt +++ /dev/null @@ -1,172 +0,0 @@ -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.base.paging.toUi -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() }, - config = state.value.paging.config - ) - - 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( - paging = pagerState.toUi(currentState.paging.config) - ) - } - } - 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.paging.items.toMutableList() - val itemIndex = items - .indexOfFirst { - it.uuid == action.uuid - } - .takeIf { it != -1 } - ?: return - val item = state.value.paging.items.getOrNull(itemIndex) ?: return - val newItem = item.copy(isFavourite = item.isFavourite.not()) - items[itemIndex] = newItem - updateState { state -> - state.copy( - paging = state.paging.copy( - items = 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/film/build.gradle.kts b/feature/film/build.gradle.kts index 4826e47c..7f34883a 100644 --- a/feature/film/build.gradle.kts +++ b/feature/film/build.gradle.kts @@ -14,8 +14,6 @@ kotlin { } } - jvm("desktop") - iosX64() iosArm64() iosSimulatorArm64() diff --git a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/di/FilmModule.kt b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/di/FilmModule.kt index 2dda26a2..118c5a00 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/di/FilmModule.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/di/FilmModule.kt @@ -1,5 +1,6 @@ package com.stslex.feature.film.di +import com.stslex.core.ui.mvi.storeDefinition import com.stslex.feature.film.data.repository.FilmRepository import com.stslex.feature.film.data.repository.FilmRepositoryImpl import com.stslex.feature.film.domain.interactor.FilmInteractor @@ -7,12 +8,11 @@ import com.stslex.feature.film.domain.interactor.FilmInteractorImpl import com.stslex.feature.film.navigation.FilmRouter import com.stslex.feature.film.navigation.FilmRouterImpl import com.stslex.feature.film.ui.store.FilmStore -import com.stslex.feature.film.ui.store.FilmStoreImpl import org.koin.dsl.module val featureFilmModule = module { - factory { - FilmStoreImpl( + storeDefinition { + FilmStore( interactor = get(), appDispatcher = get(), router = get(), diff --git a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/navigation/FilmRouter.kt b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/navigation/FilmRouter.kt index f739e420..9d0323d8 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/navigation/FilmRouter.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/navigation/FilmRouter.kt @@ -1,6 +1,6 @@ package com.stslex.feature.film.navigation import com.stslex.core.ui.mvi.Router -import com.stslex.feature.film.ui.store.FilmStore.Navigation +import com.stslex.feature.film.ui.store.FilmStoreComponent.Navigation interface FilmRouter : Router diff --git a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/navigation/FilmRouterImpl.kt b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/navigation/FilmRouterImpl.kt index a4d985ae..5b933b7d 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/navigation/FilmRouterImpl.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/navigation/FilmRouterImpl.kt @@ -2,17 +2,17 @@ package com.stslex.feature.film.navigation import com.stslex.core.ui.navigation.AppNavigator import com.stslex.core.ui.navigation.AppScreen -import com.stslex.feature.film.ui.store.FilmStore +import com.stslex.feature.film.ui.store.FilmStoreComponent class FilmRouterImpl( private val navigator: AppNavigator ) : FilmRouter { override fun invoke( - event: FilmStore.Navigation + event: FilmStoreComponent.Navigation ) { when (event) { - is FilmStore.Navigation.Back -> navigator.navigate(AppScreen.Back) + is FilmStoreComponent.Navigation.Back -> navigator.navigate(AppScreen.Back) } } } \ No newline at end of file diff --git a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/FilmScreen.kt b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/FilmScreen.kt index 79e82b0c..17187021 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/FilmScreen.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/FilmScreen.kt @@ -13,13 +13,13 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import cafe.adriel.voyager.core.screen.Screen -import com.stslex.core.ui.mvi.getStoreTest +import com.stslex.core.ui.mvi.getStore import com.stslex.core.ui.mvi.setupNavigator import com.stslex.feature.film.ui.components.FilmContentScreen import com.stslex.feature.film.ui.store.FilmScreenState import com.stslex.feature.film.ui.store.FilmStore -import com.stslex.feature.film.ui.store.FilmStore.Action -import com.stslex.feature.film.ui.store.FilmStore.State +import com.stslex.feature.film.ui.store.FilmStoreComponent.Action +import com.stslex.feature.film.ui.store.FilmStoreComponent.State data class FilmScreen( val id: String @@ -28,7 +28,7 @@ data class FilmScreen( @Composable override fun Content() { setupNavigator() - val store = getStoreTest() + val store = getStore() LaunchedEffect(Unit) { store.sendAction(Action.Init(id)) } diff --git a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/store/FilmStore.kt b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/store/FilmStore.kt index 5b750669..a4d5ce61 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/store/FilmStore.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/store/FilmStore.kt @@ -1,45 +1,85 @@ package com.stslex.feature.film.ui.store -import androidx.compose.runtime.Stable +import com.stslex.core.core.AppDispatcher import com.stslex.core.ui.mvi.Store -import com.stslex.feature.film.ui.store.FilmStore.Action -import com.stslex.feature.film.ui.store.FilmStore.Event -import com.stslex.feature.film.ui.store.FilmStore.State +import com.stslex.feature.film.domain.interactor.FilmInteractor +import com.stslex.feature.film.navigation.FilmRouter +import com.stslex.feature.film.ui.model.toDomain +import com.stslex.feature.film.ui.model.toUi +import com.stslex.feature.film.ui.store.FilmStoreComponent.Action +import com.stslex.feature.film.ui.store.FilmStoreComponent.Event +import com.stslex.feature.film.ui.store.FilmStoreComponent.Navigation +import com.stslex.feature.film.ui.store.FilmStoreComponent.State +import kotlinx.coroutines.Job -interface FilmStore : Store { +class FilmStore( + private val interactor: FilmInteractor, + appDispatcher: AppDispatcher, + router: FilmRouter, +) : Store( + router = router, + appDispatcher = appDispatcher, + initialState = State.INITIAL, +) { - @Stable - data class State( - val filmId: String, - val screenState: FilmScreenState - ) : Store.State { + private var likeJob: Job? = null - companion object { - val INITIAL = State( - filmId = "", - screenState = FilmScreenState.Loading - ) + override fun process(action: Action) { + when (action) { + is Action.Init -> actionInit(action) + is Action.BackButtonClick -> actionBackButtonClick() + is Action.LikeButtonClick -> actionLikeButtonClick() } } - @Stable - sealed interface Action : Store.Action { - - @Stable - data class Init(val id: String) : Action - - data object BackButtonClick : Action - - data object LikeButtonClick : Action + private fun actionLikeButtonClick() { + if (likeJob?.isActive == true) return + val film = state.value.screenState.result ?: return + updateState { state -> + state.copy( + screenState = FilmScreenState.Content(film.copy(isFavorite = !film.isFavorite)) + ) + } + likeJob = launch( + onError = { + updateState { state -> + state.copy( + screenState = FilmScreenState.Content(film.copy(isFavorite = !film.isFavorite)) + ) + } + sendEvent(Event.ErrorSnackbar(it)) + }, + onSuccess = { + // TODO show toast success + } + ) { + if (film.isFavorite) { + interactor.dislikeFilm(film.id) + } else { + interactor.likeFilm(film.toDomain()) + } + } } - sealed interface Event : Store.Event { - - data class ErrorSnackbar(val throwable: Throwable) : Event + private fun actionBackButtonClick() { + consumeNavigation(Navigation.Back) } - sealed interface Navigation : Store.Navigation { - - data object Back : Navigation + private fun actionInit(action: Action.Init) { + updateState { currentState -> + currentState.copy( + screenState = FilmScreenState.Loading, + filmId = action.id + ) + } + interactor + .getFilm(action.id) + .launch { film -> + updateState { currentState -> + currentState.copy( + screenState = FilmScreenState.Content(film.toUi()) + ) + } + } } -} \ No newline at end of file +} diff --git a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/store/FilmStoreComponent.kt b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/store/FilmStoreComponent.kt new file mode 100644 index 00000000..ace47c8b --- /dev/null +++ b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/store/FilmStoreComponent.kt @@ -0,0 +1,42 @@ +package com.stslex.feature.film.ui.store + +import androidx.compose.runtime.Stable +import com.stslex.core.ui.mvi.StoreComponent + +interface FilmStoreComponent : StoreComponent { + + @Stable + data class State( + val filmId: String, + val screenState: FilmScreenState + ) : StoreComponent.State { + + companion object { + val INITIAL = State( + filmId = "", + screenState = FilmScreenState.Loading + ) + } + } + + @Stable + sealed interface Action : StoreComponent.Action { + + @Stable + data class Init(val id: String) : Action + + data object BackButtonClick : Action + + data object LikeButtonClick : Action + } + + sealed interface Event : StoreComponent.Event { + + data class ErrorSnackbar(val throwable: Throwable) : Event + } + + sealed interface Navigation : StoreComponent.Navigation { + + data object Back : Navigation + } +} \ No newline at end of file diff --git a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/store/FilmStoreImpl.kt b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/store/FilmStoreImpl.kt deleted file mode 100644 index 6061352e..00000000 --- a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/store/FilmStoreImpl.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.stslex.feature.film.ui.store - -import com.stslex.core.core.AppDispatcher -import com.stslex.core.ui.mvi.BaseStore -import com.stslex.feature.film.domain.interactor.FilmInteractor -import com.stslex.feature.film.navigation.FilmRouter -import com.stslex.feature.film.ui.model.toDomain -import com.stslex.feature.film.ui.model.toUi -import com.stslex.feature.film.ui.store.FilmStore.Action -import com.stslex.feature.film.ui.store.FilmStore.Event -import com.stslex.feature.film.ui.store.FilmStore.Navigation -import com.stslex.feature.film.ui.store.FilmStore.State -import kotlinx.coroutines.Job - -class FilmStoreImpl( - private val interactor: FilmInteractor, - appDispatcher: AppDispatcher, - router: FilmRouter, -) : FilmStore, 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.BackButtonClick -> actionBackButtonClick() - is Action.LikeButtonClick -> actionLikeButtonClick() - } - } - - private fun actionLikeButtonClick() { - if (likeJob?.isActive == true) return - val film = state.value.screenState.result ?: return - updateState { state -> - state.copy( - screenState = FilmScreenState.Content(film.copy(isFavorite = !film.isFavorite)) - ) - } - likeJob = launch( - onError = { - updateState { state -> - state.copy( - screenState = FilmScreenState.Content(film.copy(isFavorite = !film.isFavorite)) - ) - } - sendEvent(Event.ErrorSnackbar(it)) - }, - onSuccess = { - // TODO show toast success - } - ) { - if (film.isFavorite) { - interactor.dislikeFilm(film.id) - } else { - interactor.likeFilm(film.toDomain()) - } - } - } - - private fun actionBackButtonClick() { - navigate(Navigation.Back) - } - - private fun actionInit(action: Action.Init) { - updateState { currentState -> - currentState.copy( - screenState = FilmScreenState.Loading, - filmId = action.id - ) - } - interactor - .getFilm(action.id) - .launch { film -> - updateState { currentState -> - currentState.copy( - screenState = FilmScreenState.Content(film.toUi()) - ) - } - } - } -} diff --git a/feature/film_feed/build.gradle.kts b/feature/film_feed/build.gradle.kts index fc44451f..0a31941f 100644 --- a/feature/film_feed/build.gradle.kts +++ b/feature/film_feed/build.gradle.kts @@ -14,8 +14,6 @@ kotlin { } } - jvm("desktop") - iosX64() iosArm64() iosSimulatorArm64() diff --git a/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/di/FeedModule.kt b/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/di/FeedModule.kt index 9f7dae92..c5b383ab 100644 --- a/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/di/FeedModule.kt +++ b/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/di/FeedModule.kt @@ -1,5 +1,6 @@ package com.stslex.feature.film_feed.di +import com.stslex.core.ui.mvi.storeDefinition import com.stslex.feature.film_feed.data.repository.FeedRepository import com.stslex.feature.film_feed.data.repository.FeedRepositoryImpl import com.stslex.feature.film_feed.domain.interactor.FeedInteractor @@ -7,14 +8,11 @@ import com.stslex.feature.film_feed.domain.interactor.FeedInteractorImpl import com.stslex.feature.film_feed.navigation.FeedScreenRouter import com.stslex.feature.film_feed.navigation.FeedScreenRouterImpl import com.stslex.feature.film_feed.ui.store.FeedStore -import com.stslex.feature.film_feed.ui.store.FeedStoreImpl -import org.koin.core.qualifier.Qualifier -import org.koin.core.qualifier.named import org.koin.dsl.module val featureFeedModule = module { - factory { - FeedStoreImpl( + storeDefinition { + FeedStore( interactor = get(), appDispatcher = get(), router = get() diff --git a/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/navigation/FeedScreenRouter.kt b/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/navigation/FeedScreenRouter.kt index 44af4945..e3469aea 100644 --- a/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/navigation/FeedScreenRouter.kt +++ b/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/navigation/FeedScreenRouter.kt @@ -1,6 +1,6 @@ package com.stslex.feature.film_feed.navigation import com.stslex.core.ui.mvi.Router -import com.stslex.feature.film_feed.ui.store.FeedStore.Navigation +import com.stslex.feature.film_feed.ui.store.FeedStoreComponent.Navigation interface FeedScreenRouter : Router diff --git a/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/navigation/FeedScreenRouterImpl.kt b/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/navigation/FeedScreenRouterImpl.kt index 9f8a7f58..64246e8c 100644 --- a/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/navigation/FeedScreenRouterImpl.kt +++ b/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/navigation/FeedScreenRouterImpl.kt @@ -2,19 +2,19 @@ package com.stslex.feature.film_feed.navigation import com.stslex.core.ui.navigation.AppNavigator import com.stslex.core.ui.navigation.AppScreen -import com.stslex.feature.film_feed.ui.store.FeedStore +import com.stslex.feature.film_feed.ui.store.FeedStoreComponent class FeedScreenRouterImpl( private val navigator: AppNavigator ) : FeedScreenRouter { - override fun invoke(event: FeedStore.Navigation) { + override fun invoke(event: FeedStoreComponent.Navigation) { when (event) { - is FeedStore.Navigation.Film -> navigateToFilm(event) + is FeedStoreComponent.Navigation.Film -> navigateToFilm(event) } } - private fun navigateToFilm(event: FeedStore.Navigation.Film) { + private fun navigateToFilm(event: FeedStoreComponent.Navigation.Film) { navigator.navigate(AppScreen.Film(event.filmId)) } } \ No newline at end of file diff --git a/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/ui/FeedScreen.kt b/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/ui/FeedScreen.kt index 660a0086..10576e45 100644 --- a/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/ui/FeedScreen.kt +++ b/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/ui/FeedScreen.kt @@ -9,21 +9,21 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import cafe.adriel.voyager.core.screen.Screen -import com.stslex.core.ui.mvi.getStoreTest +import com.stslex.core.ui.mvi.getStore import com.stslex.feature.film_feed.ui.components.FeedScreenContent import com.stslex.feature.film_feed.ui.components.FeedScreenError import com.stslex.feature.film_feed.ui.components.FeedScreenLoading import com.stslex.feature.film_feed.ui.model.ScreenState import com.stslex.feature.film_feed.ui.store.FeedStore -import com.stslex.feature.film_feed.ui.store.FeedStore.Action -import com.stslex.feature.film_feed.ui.store.FeedStore.Event -import com.stslex.feature.film_feed.ui.store.FeedStore.State +import com.stslex.feature.film_feed.ui.store.FeedStoreComponent.Action +import com.stslex.feature.film_feed.ui.store.FeedStoreComponent.Event +import com.stslex.feature.film_feed.ui.store.FeedStoreComponent.State object FeedScreen : Screen { @Composable override fun Content() { - val store = getStoreTest() + val store = getStore() val state by remember { store.state }.collectAsState() LaunchedEffect(Unit) { store.event.collect { event -> diff --git a/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/ui/components/FeedScreenFilmItem.kt b/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/ui/components/FeedScreenFilmItem.kt index e9461733..e52c9653 100644 --- a/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/ui/components/FeedScreenFilmItem.kt +++ b/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/ui/components/FeedScreenFilmItem.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -30,7 +29,6 @@ import com.stslex.core.ui.theme.AppDimension import com.stslex.feature.film_feed.ui.model.FilmModel import kotlinx.collections.immutable.ImmutableList -@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun FeedScreenFilmItem( modifier: Modifier = Modifier, diff --git a/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/ui/store/FeedStore.kt b/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/ui/store/FeedStore.kt index b0635e24..5ec7eafc 100644 --- a/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/ui/store/FeedStore.kt +++ b/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/ui/store/FeedStore.kt @@ -1,53 +1,103 @@ package com.stslex.feature.film_feed.ui.store import androidx.compose.runtime.Stable +import com.stslex.core.core.AppDispatcher +import com.stslex.core.core.Logger import com.stslex.core.ui.mvi.Store -import com.stslex.feature.film_feed.ui.model.FilmModel +import com.stslex.feature.film_feed.domain.interactor.FeedInteractor +import com.stslex.feature.film_feed.navigation.FeedScreenRouter import com.stslex.feature.film_feed.ui.model.ScreenState -import com.stslex.feature.film_feed.ui.store.FeedStore.Action -import com.stslex.feature.film_feed.ui.store.FeedStore.Event -import com.stslex.feature.film_feed.ui.store.FeedStore.State -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf - -interface FeedStore : Store { - - @Stable - data class State( - val films: ImmutableList, - val screen: ScreenState, - val currentPage: Int, - val hasNextPage: Boolean - ) : Store.State { - - companion object { - val INITIAL = State( - films = persistentListOf(), - screen = ScreenState.Loading, - currentPage = 0, - hasNextPage = true - ) +import com.stslex.feature.film_feed.ui.model.toUI +import com.stslex.feature.film_feed.ui.store.FeedStoreComponent.Action +import com.stslex.feature.film_feed.ui.store.FeedStoreComponent.Event +import com.stslex.feature.film_feed.ui.store.FeedStoreComponent.Navigation +import com.stslex.feature.film_feed.ui.store.FeedStoreComponent.State +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Job + +@Stable +class FeedStore( + private val interactor: FeedInteractor, + appDispatcher: AppDispatcher, + router: FeedScreenRouter +) : Store( + router = router, + initialState = State.INITIAL, + appDispatcher = appDispatcher +) { + + private var loadingJob: Job? = null + + override fun process(action: Action) { + when (action) { + is Action.LoadFilms -> actionLoadFilms() + is Action.FilmClick -> actionFilmClick(action) } } - sealed interface Event : Store.Event { - - @Stable - data class ErrorSnackBar(val message: String) : Event + private fun actionFilmClick(action: Action.FilmClick) { + consumeNavigation(Navigation.Film(action.filmId)) } - @Stable - sealed interface Action : Store.Action { + private fun actionLoadFilms() { + if (loadingJob?.isActive == true) { + Logger.debug("Loading job is active") + return + } + val hasNextPage = state.value.hasNextPage + if (hasNextPage.not()) { + Logger.debug("No more pages") + return + } + val loadScreenState = when (state.value.screen) { + is ScreenState.Content -> ScreenState.Content.AppendLoading + is ScreenState.Loading -> ScreenState.Loading + is ScreenState.Error -> ScreenState.Loading + } - data object LoadFilms : Action + updateState { currentState -> + currentState.copy( + screen = loadScreenState + ) + } - @Stable - data class FilmClick(val filmId: String) : Action + val currentPage = state.value.currentPage + loadingJob = launch( + action = { + interactor.getFeed( + page = currentPage.inc(), + pageSize = PAGE_SIZE + ) + }, + onSuccess = { feed -> + val films = feed.films.toUI() + updateState { currentState -> + val currentFilms = currentState.films.toMutableList() + currentFilms.addAll(films) + currentState.copy( + films = currentFilms.toImmutableList(), + screen = ScreenState.Content.Success, + currentPage = currentPage.inc(), + hasNextPage = films.size == PAGE_SIZE + ) + } + }, + onError = { throwable -> + if (state.value.films.isEmpty()) { + updateState { + it.copy( + screen = ScreenState.Error(throwable.message ?: "Unknown error") + ) + } + } else { + sendEvent(Event.ErrorSnackBar(throwable.message ?: "Unknown error")) + } + }, + ) } - @Stable - sealed interface Navigation : Store.Navigation { + companion object { - data class Film(val filmId: String) : Navigation + private const val PAGE_SIZE = 15 } } \ No newline at end of file diff --git a/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/ui/store/FeedStoreComponent.kt b/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/ui/store/FeedStoreComponent.kt new file mode 100644 index 00000000..f8d131ef --- /dev/null +++ b/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/ui/store/FeedStoreComponent.kt @@ -0,0 +1,50 @@ +package com.stslex.feature.film_feed.ui.store + +import androidx.compose.runtime.Stable +import com.stslex.core.ui.mvi.StoreComponent +import com.stslex.feature.film_feed.ui.model.FilmModel +import com.stslex.feature.film_feed.ui.model.ScreenState +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +interface FeedStoreComponent : StoreComponent { + + @Stable + data class State( + val films: ImmutableList, + val screen: ScreenState, + val currentPage: Int, + val hasNextPage: Boolean + ) : StoreComponent.State { + + companion object { + val INITIAL = State( + films = persistentListOf(), + screen = ScreenState.Loading, + currentPage = 0, + hasNextPage = true + ) + } + } + + sealed interface Event : StoreComponent.Event { + + @Stable + data class ErrorSnackBar(val message: String) : Event + } + + @Stable + sealed interface Action : StoreComponent.Action { + + data object LoadFilms : Action + + @Stable + data class FilmClick(val filmId: String) : Action + } + + @Stable + sealed interface Navigation : StoreComponent.Navigation { + + data class Film(val filmId: String) : Navigation + } +} \ No newline at end of file diff --git a/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/ui/store/FeedStoreImpl.kt b/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/ui/store/FeedStoreImpl.kt deleted file mode 100644 index fbebf084..00000000 --- a/feature/film_feed/src/commonMain/kotlin/com/stslex/feature/film_feed/ui/store/FeedStoreImpl.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.stslex.feature.film_feed.ui.store - -import androidx.compose.runtime.Stable -import com.stslex.core.core.AppDispatcher -import com.stslex.core.core.Logger -import com.stslex.core.ui.mvi.BaseStore -import com.stslex.feature.film_feed.domain.interactor.FeedInteractor -import com.stslex.feature.film_feed.navigation.FeedScreenRouter -import com.stslex.feature.film_feed.ui.model.ScreenState -import com.stslex.feature.film_feed.ui.model.toUI -import com.stslex.feature.film_feed.ui.store.FeedStore.Action -import com.stslex.feature.film_feed.ui.store.FeedStore.Event -import com.stslex.feature.film_feed.ui.store.FeedStore.Navigation -import com.stslex.feature.film_feed.ui.store.FeedStore.State -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.Job - -@Stable -class FeedStoreImpl( - private val interactor: FeedInteractor, - appDispatcher: AppDispatcher, - router: FeedScreenRouter -) : FeedStore, BaseStore( - router = router, - initialState = State.INITIAL, - appDispatcher = appDispatcher -) { - - private var loadingJob: Job? = null - - override fun process(action: Action) { - when (action) { - is Action.LoadFilms -> actionLoadFilms() - is Action.FilmClick -> actionFilmClick(action) - } - } - - private fun actionFilmClick(action: Action.FilmClick) { - navigate(Navigation.Film(action.filmId)) - } - - private fun actionLoadFilms() { - if (loadingJob?.isActive == true) { - Logger.debug("Loading job is active") - return - } - val hasNextPage = state.value.hasNextPage - if (hasNextPage.not()) { - Logger.debug("No more pages") - return - } - val loadScreenState = when (state.value.screen) { - is ScreenState.Content -> ScreenState.Content.AppendLoading - is ScreenState.Loading -> ScreenState.Loading - is ScreenState.Error -> ScreenState.Loading - } - - updateState { currentState -> - currentState.copy( - screen = loadScreenState - ) - } - - val currentPage = state.value.currentPage - loadingJob = launch( - action = { - interactor.getFeed( - page = currentPage.inc(), - pageSize = PAGE_SIZE - ) - }, - onSuccess = { feed -> - val films = feed.films.toUI() - updateState { currentState -> - val currentFilms = currentState.films.toMutableList() - currentFilms.addAll(films) - currentState.copy( - films = currentFilms.toImmutableList(), - screen = ScreenState.Content.Success, - currentPage = currentPage.inc(), - hasNextPage = films.size == PAGE_SIZE - ) - } - }, - onError = { throwable -> - if (state.value.films.isEmpty()) { - updateState { - it.copy( - screen = ScreenState.Error(throwable.message ?: "Unknown error") - ) - } - } else { - sendEvent(Event.ErrorSnackBar(throwable.message ?: "Unknown error")) - } - }, - ) - } - - companion object { - - private const val PAGE_SIZE = 15 - } -} \ No newline at end of file diff --git a/feature/follower/build.gradle.kts b/feature/follower/build.gradle.kts index 085eab35..33d3484e 100644 --- a/feature/follower/build.gradle.kts +++ b/feature/follower/build.gradle.kts @@ -14,8 +14,6 @@ kotlin { } } - jvm("desktop") - iosX64() iosArm64() iosSimulatorArm64() diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/di/FollowerModule.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/di/FollowerModule.kt index 23447caf..ad2bd08c 100644 --- a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/di/FollowerModule.kt +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/di/FollowerModule.kt @@ -1,5 +1,6 @@ package com.stslex.feature.follower.di +import com.stslex.core.ui.mvi.storeDefinition import com.stslex.feature.follower.data.repository.FollowerRepository import com.stslex.feature.follower.data.repository.FollowerRepositoryImpl import com.stslex.feature.follower.domain.interactor.FollowerInteractor @@ -7,15 +8,14 @@ import com.stslex.feature.follower.domain.interactor.FollowerInteractorImpl import com.stslex.feature.follower.navigation.FollowerRouter import com.stslex.feature.follower.navigation.FollowerRouterImpl import com.stslex.feature.follower.ui.store.FollowerStore -import com.stslex.feature.follower.ui.store.FollowerStoreImpl import org.koin.dsl.module val featureFollowerModule = module { factory { FollowerRepositoryImpl(client = get()) } factory { FollowerInteractorImpl(repository = get()) } factory { FollowerRouterImpl(navigator = get()) } - factory { - FollowerStoreImpl( + storeDefinition { + FollowerStore( interactor = get(), appDispatcher = get(), router = get(), diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/navigation/FollowerRouter.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/navigation/FollowerRouter.kt index 03005f54..48d0f9c2 100644 --- a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/navigation/FollowerRouter.kt +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/navigation/FollowerRouter.kt @@ -1,7 +1,7 @@ package com.stslex.feature.follower.navigation import com.stslex.core.ui.mvi.Router -import com.stslex.feature.follower.ui.store.FollowerStore.Navigation +import com.stslex.feature.follower.ui.store.FollowerStoreComponent.Navigation interface FollowerRouter : Router diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/navigation/FollowerRouterImpl.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/navigation/FollowerRouterImpl.kt index 02e1383b..ff9b04d0 100644 --- a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/navigation/FollowerRouterImpl.kt +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/navigation/FollowerRouterImpl.kt @@ -1,13 +1,13 @@ package com.stslex.feature.follower.navigation import com.stslex.core.ui.navigation.AppNavigator -import com.stslex.feature.follower.ui.store.FollowerStore +import com.stslex.feature.follower.ui.store.FollowerStoreComponent class FollowerRouterImpl( private val navigator: AppNavigator ) : FollowerRouter { - override fun invoke(event: FollowerStore.Navigation) { + override fun invoke(event: FollowerStoreComponent.Navigation) { TODO("Not yet implemented") } } \ No newline at end of file diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/FollowerScreen.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/FollowerScreen.kt index 08cf5efe..a42a869a 100644 --- a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/FollowerScreen.kt +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/FollowerScreen.kt @@ -12,12 +12,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import cafe.adriel.voyager.core.screen.Screen import com.stslex.core.ui.base.paging.PagingColumn -import com.stslex.core.ui.mvi.getStoreTest +import com.stslex.core.ui.mvi.getStore import com.stslex.feature.follower.navigation.FollowerScreenArgs import com.stslex.feature.follower.ui.store.FollowerScreenState import com.stslex.feature.follower.ui.store.FollowerStore -import com.stslex.feature.follower.ui.store.FollowerStore.Action -import com.stslex.feature.follower.ui.store.FollowerStore.State +import com.stslex.feature.follower.ui.store.FollowerStoreComponent.Action +import com.stslex.feature.follower.ui.store.FollowerStoreComponent.State data class FollowerScreen( val args: FollowerScreenArgs @@ -25,7 +25,7 @@ data class FollowerScreen( @Composable override fun Content() { - val store = getStoreTest() + val store = getStore() val state by remember { store.state }.collectAsState() LaunchedEffect(key1 = Unit) { diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStore.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStore.kt index 497e2b91..07066d14 100644 --- a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStore.kt +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStore.kt @@ -1,67 +1,148 @@ package com.stslex.feature.follower.ui.store -import androidx.compose.runtime.Stable -import com.stslex.core.ui.base.paging.PagingConfig -import com.stslex.core.ui.base.paging.PagingUiState +import com.stslex.core.core.AppDispatcher +import com.stslex.core.ui.base.mapToAppError +import com.stslex.core.ui.base.paging.toUi import com.stslex.core.ui.mvi.Store -import com.stslex.core.ui.mvi.Store.Event.Snackbar +import com.stslex.core.ui.mvi.StoreComponent.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.follower.domain.interactor.FollowerInteractor +import com.stslex.feature.follower.navigation.FollowerRouter import com.stslex.feature.follower.navigation.FollowerScreenArgs import com.stslex.feature.follower.ui.model.FollowerModel -import com.stslex.feature.follower.ui.store.FollowerStore.Action -import com.stslex.feature.follower.ui.store.FollowerStore.Event -import com.stslex.feature.follower.ui.store.FollowerStore.State - -interface FollowerStore : Store { - - @Stable - data class State( - val type: FollowerScreenArgs, - val paging: PagingUiState, - val screen: FollowerScreenState, - val query: String - ) : Store.State { - - companion object { - - val INITIAL = State( - type = FollowerScreenArgs.Follower(""), - paging = PagingUiState.default(PagingConfig.DEFAULT), - screen = FollowerScreenState.Shimmer, - query = "" - ) +import com.stslex.feature.follower.ui.model.toUi +import com.stslex.feature.follower.ui.store.FollowerStoreComponent.Action +import com.stslex.feature.follower.ui.store.FollowerStoreComponent.Event +import com.stslex.feature.follower.ui.store.FollowerStoreComponent.Navigation +import com.stslex.feature.follower.ui.store.FollowerStoreComponent.State +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map + +class FollowerStore( + private val interactor: FollowerInteractor, + router: FollowerRouter, + appDispatcher: AppDispatcher, + pagerFactory: StorePagerFactory, +) : FollowerStoreComponent, Store( + router = router, + appDispatcher = appDispatcher, + initialState = State.INITIAL, +) { + + private val pager: StorePager = pagerFactory.create( + request = { page, pageSize -> + val currentState = state.value + when (currentState.type) { + is FollowerScreenArgs.Follower -> interactor.getFollowers( + uuid = currentState.type.uuid, + query = currentState.query, + page = page, + pageSize = pageSize + ) + + is FollowerScreenArgs.Following -> interactor.getFollowing( + uuid = currentState.type.uuid, + query = currentState.query, + page = page, + pageSize = pageSize + ) + } + }, + scope = scope, + mapper = { it.toUi() }, + config = state.value.paging.config + ) + + override fun process(action: Action) { + when (action) { + is Action.Init -> actionInit(action) + is Action.Load -> actionLoad() + is Action.OnUserClick -> actionUserClick(action) + is Action.QueryChanged -> actionQueryChanged(action) + Action.Refresh -> actionRefresh() + Action.Retry -> actionRetry() } } - @Stable - sealed interface Action : Store.Action { + private fun actionInit(action: Action.Init) { + updateState { state -> + state.copy(type = action.args) + } - @Stable - data class Init( - val args: FollowerScreenArgs - ) : Action + pager.state.launch { pagerState -> + updateState { currentState -> + currentState.copy( + paging = pagerState.toUi(currentState.paging.config) + ) + } + } - @Stable - data object Load : Action + pager.loadState.launch { loadState -> + updateState { currentState -> + currentState.copy( + screen = loadState.toUi() + ) + } + } + + pager.loadEvents.launch { + sendEvent( + Event.ShowSnackbar(Snackbar.Error("error load matches")) + ) + } - @Stable - data object Refresh : Action + state + .map { it.query } + .distinctUntilChanged() + .launch( + onError = ::showError + ) { + if (pager.loadState.value is PagerLoadState.Initial) { + pager.initialLoad() + } else { + pager.refresh(isForceLoad = false) + } + } + } - @Stable - data object Retry : Action + private fun actionLoad() { + pager.load() + } - @Stable - data class QueryChanged(val query: String) : Action + private fun actionUserClick(action: Action.OnUserClick) { + // todo ("navigate to user") + } - @Stable - data class OnUserClick(val uuid: String) : Action + private fun actionQueryChanged(action: Action.QueryChanged) { + updateState { currentState -> + currentState.copy( + query = action.query + ) + } } - @Stable - sealed interface Event : Store.Event { + private fun actionRefresh() { + pager.refresh(isForceLoad = true) + } - @Stable - data class ShowSnackbar(val snackbar: Snackbar) : Event + private fun actionRetry() { + pager.retry() } - sealed interface Navigation : Store.Navigation + private fun showError(error: Throwable) { + val appError = error.mapToAppError("error logout") + if (state.value.screen is FollowerScreenState.Content) { + sendEvent( + Event.ShowSnackbar(Snackbar.Error(appError.message)) + ) + } else { + updateState { currentState -> + currentState.copy( + screen = FollowerScreenState.Error(appError) + ) + } + } + } } diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStoreComponent.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStoreComponent.kt new file mode 100644 index 00000000..e86a14f0 --- /dev/null +++ b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStoreComponent.kt @@ -0,0 +1,64 @@ +package com.stslex.feature.follower.ui.store + +import androidx.compose.runtime.Stable +import com.stslex.core.ui.base.paging.PagingConfig +import com.stslex.core.ui.base.paging.PagingUiState +import com.stslex.core.ui.mvi.StoreComponent +import com.stslex.core.ui.mvi.StoreComponent.Event.Snackbar +import com.stslex.feature.follower.navigation.FollowerScreenArgs +import com.stslex.feature.follower.ui.model.FollowerModel + +interface FollowerStoreComponent : StoreComponent { + + @Stable + data class State( + val type: FollowerScreenArgs, + val paging: PagingUiState, + val screen: FollowerScreenState, + val query: String + ) : StoreComponent.State { + + companion object { + + val INITIAL = State( + type = FollowerScreenArgs.Follower(""), + paging = PagingUiState.default(PagingConfig.DEFAULT), + screen = FollowerScreenState.Shimmer, + query = "" + ) + } + } + + @Stable + sealed interface Action : StoreComponent.Action { + + @Stable + data class Init( + val args: FollowerScreenArgs + ) : Action + + @Stable + data object Load : Action + + @Stable + data object Refresh : Action + + @Stable + data object Retry : Action + + @Stable + data class QueryChanged(val query: String) : Action + + @Stable + data class OnUserClick(val uuid: String) : Action + } + + @Stable + sealed interface Event : StoreComponent.Event { + + @Stable + data class ShowSnackbar(val snackbar: Snackbar) : Event + } + + sealed interface Navigation : StoreComponent.Navigation +} diff --git a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStoreImpl.kt b/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStoreImpl.kt deleted file mode 100644 index 04ec44a9..00000000 --- a/feature/follower/src/commonMain/kotlin/com.stslex.feature.follower/ui/store/FollowerStoreImpl.kt +++ /dev/null @@ -1,148 +0,0 @@ -package com.stslex.feature.follower.ui.store - -import com.stslex.core.core.AppDispatcher -import com.stslex.core.ui.base.mapToAppError -import com.stslex.core.ui.base.paging.toUi -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.follower.domain.interactor.FollowerInteractor -import com.stslex.feature.follower.navigation.FollowerRouter -import com.stslex.feature.follower.navigation.FollowerScreenArgs -import com.stslex.feature.follower.ui.model.FollowerModel -import com.stslex.feature.follower.ui.model.toUi -import com.stslex.feature.follower.ui.store.FollowerStore.Action -import com.stslex.feature.follower.ui.store.FollowerStore.Event -import com.stslex.feature.follower.ui.store.FollowerStore.Navigation -import com.stslex.feature.follower.ui.store.FollowerStore.State -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map - -class FollowerStoreImpl( - private val interactor: FollowerInteractor, - router: FollowerRouter, - appDispatcher: AppDispatcher, - pagerFactory: StorePagerFactory, -) : FollowerStore, BaseStore( - router = router, - appDispatcher = appDispatcher, - initialState = State.INITIAL, -) { - - private val pager: StorePager = pagerFactory.create( - request = { page, pageSize -> - val currentState = state.value - when (currentState.type) { - is FollowerScreenArgs.Follower -> interactor.getFollowers( - uuid = currentState.type.uuid, - query = currentState.query, - page = page, - pageSize = pageSize - ) - - is FollowerScreenArgs.Following -> interactor.getFollowing( - uuid = currentState.type.uuid, - query = currentState.query, - page = page, - pageSize = pageSize - ) - } - }, - scope = scope, - mapper = { it.toUi() }, - config = state.value.paging.config - ) - - override fun process(action: Action) { - when (action) { - is Action.Init -> actionInit(action) - is Action.Load -> actionLoad() - is Action.OnUserClick -> actionUserClick(action) - is Action.QueryChanged -> actionQueryChanged(action) - Action.Refresh -> actionRefresh() - Action.Retry -> actionRetry() - } - } - - private fun actionInit(action: Action.Init) { - updateState { state -> - state.copy(type = action.args) - } - - pager.state.launch { pagerState -> - updateState { currentState -> - currentState.copy( - paging = pagerState.toUi(currentState.paging.config) - ) - } - } - - pager.loadState.launch { loadState -> - updateState { currentState -> - currentState.copy( - screen = loadState.toUi() - ) - } - } - - pager.loadEvents.launch { - sendEvent( - Event.ShowSnackbar(Snackbar.Error("error load matches")) - ) - } - - state - .map { it.query } - .distinctUntilChanged() - .launch( - onError = ::showError - ) { - if (pager.loadState.value is PagerLoadState.Initial) { - pager.initialLoad() - } else { - pager.refresh(isForceLoad = false) - } - } - } - - private fun actionLoad() { - pager.load() - } - - private fun actionUserClick(action: Action.OnUserClick) { - // todo ("navigate to user") - } - - private fun actionQueryChanged(action: Action.QueryChanged) { - updateState { currentState -> - currentState.copy( - query = action.query - ) - } - } - - private fun actionRefresh() { - pager.refresh(isForceLoad = true) - } - - private fun actionRetry() { - pager.retry() - } - - private fun showError(error: Throwable) { - val appError = error.mapToAppError("error logout") - if (state.value.screen is FollowerScreenState.Content) { - sendEvent( - Event.ShowSnackbar(Snackbar.Error(appError.message)) - ) - } else { - updateState { currentState -> - currentState.copy( - screen = FollowerScreenState.Error(appError) - ) - } - } - } -} diff --git a/feature/match/build.gradle.kts b/feature/match/build.gradle.kts index cc86f459..131c76a5 100644 --- a/feature/match/build.gradle.kts +++ b/feature/match/build.gradle.kts @@ -14,8 +14,6 @@ kotlin { } } - jvm("desktop") - iosX64() iosArm64() iosSimulatorArm64() 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 65a67e28..03caa1cd 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 @@ -1,5 +1,6 @@ package com.stslex.feature.match.di +import com.stslex.core.ui.mvi.storeDefinition import com.stslex.feature.match.data.repository.MatchRepository import com.stslex.feature.match.data.repository.MatchRepositoryImpl import com.stslex.feature.match.domain.interactor.MatchInteractor @@ -7,7 +8,6 @@ import com.stslex.feature.match.domain.interactor.MatchInteractorImpl import com.stslex.feature.match.navigation.MatchRouter import com.stslex.feature.match.navigation.MatchRouterImpl import com.stslex.feature.match.ui.store.MatchStore -import com.stslex.feature.match.ui.store.MatchStoreImpl import org.koin.dsl.module val featureMatchModule = module { @@ -24,8 +24,8 @@ val featureMatchModule = module { ) } factory { MatchRouterImpl(navigator = get()) } - factory { - MatchStoreImpl( + storeDefinition { + MatchStore( interactor = get(), router = get(), appDispatcher = get(), diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/navigation/MatchRouter.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/navigation/MatchRouter.kt index b660c953..ef65b251 100644 --- a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/navigation/MatchRouter.kt +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/navigation/MatchRouter.kt @@ -1,6 +1,6 @@ package com.stslex.feature.match.navigation import com.stslex.core.ui.mvi.Router -import com.stslex.feature.match.ui.store.MatchStore.Navigation +import com.stslex.feature.match.ui.store.MatchStoreComponent.Navigation interface MatchRouter : Router \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/navigation/MatchRouterImpl.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/navigation/MatchRouterImpl.kt index 51ff7bf5..f6940252 100644 --- a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/navigation/MatchRouterImpl.kt +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/navigation/MatchRouterImpl.kt @@ -2,7 +2,7 @@ package com.stslex.feature.match.navigation import com.stslex.core.ui.navigation.AppNavigator import com.stslex.core.ui.navigation.AppScreen -import com.stslex.feature.match.ui.store.MatchStore.Navigation +import com.stslex.feature.match.ui.store.MatchStoreComponent.Navigation class MatchRouterImpl( private val navigator: AppNavigator diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/MatchScreen.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/MatchScreen.kt index 18c88e65..1ec40d08 100644 --- a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/MatchScreen.kt +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/MatchScreen.kt @@ -11,7 +11,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import cafe.adriel.voyager.core.screen.Screen import com.stslex.core.ui.components.AppSnackbarHost -import com.stslex.core.ui.mvi.getStoreTest +import com.stslex.core.ui.mvi.getStore import com.stslex.core.ui.navigation.args.MatchScreenArgs import com.stslex.feature.match.ui.components.MatchScreenContent import com.stslex.feature.match.ui.components.MatchScreenEmpty @@ -19,9 +19,9 @@ import com.stslex.feature.match.ui.components.MatchScreenError import com.stslex.feature.match.ui.components.MatchScreenShimmer import com.stslex.feature.match.ui.store.MatchScreenState import com.stslex.feature.match.ui.store.MatchStore -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.State +import com.stslex.feature.match.ui.store.MatchStoreComponent.Action +import com.stslex.feature.match.ui.store.MatchStoreComponent.Event +import com.stslex.feature.match.ui.store.MatchStoreComponent.State data class MatchScreen( private val args: MatchScreenArgs @@ -29,7 +29,7 @@ data class MatchScreen( @Composable override fun Content() { - val store = getStoreTest() + val store = getStore() LaunchedEffect(Unit) { store.sendAction(Action.Init(args = args)) } diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/components/MatchScreenContent.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/components/MatchScreenContent.kt index d2b639a3..cfeb26a0 100644 --- a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/components/MatchScreenContent.kt +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/components/MatchScreenContent.kt @@ -23,7 +23,7 @@ import com.stslex.core.ui.base.shimmerLoadingAnimation import com.stslex.core.ui.theme.AppDimension import com.stslex.feature.match.ui.model.MatchUiModel import com.stslex.feature.match.ui.store.MatchScreenState -import com.stslex.feature.match.ui.store.MatchStore.Action +import com.stslex.feature.match.ui.store.MatchStoreComponent.Action @OptIn(ExperimentalMaterialApi::class) @Composable 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 a297dfc3..47054206 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 @@ -1,79 +1,176 @@ package com.stslex.feature.match.ui.store -import androidx.compose.runtime.Stable -import com.stslex.core.ui.base.paging.PagingConfig -import com.stslex.core.ui.base.paging.PagingUiState +import com.stslex.core.core.AppDispatcher +import com.stslex.core.core.Logger +import com.stslex.core.database.store.UserStore +import com.stslex.core.ui.base.mapToAppError +import com.stslex.core.ui.base.paging.toUi import com.stslex.core.ui.mvi.Store -import com.stslex.core.ui.mvi.Store.Event.Snackbar -import com.stslex.core.ui.navigation.args.MatchScreenArgs +import com.stslex.core.ui.mvi.StoreComponent.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.match.domain.interactor.MatchInteractor +import com.stslex.feature.match.navigation.MatchRouter import com.stslex.feature.match.ui.model.MatchUiModel -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.State - -interface MatchStore : Store { - - @Stable - data class State( - val screen: MatchScreenState, - val uuid: String, - val isSelf: Boolean, - val query: String, - val paging: PagingUiState - ) : Store.State { - - companion object { - - val INITIAL = State( - screen = MatchScreenState.Shimmer, - paging = PagingUiState.default(PagingConfig.DEFAULT), - uuid = "", - isSelf = false, - query = "" +import com.stslex.feature.match.ui.model.toUi +import com.stslex.feature.match.ui.store.MatchStoreComponent.Action +import com.stslex.feature.match.ui.store.MatchStoreComponent.Event +import com.stslex.feature.match.ui.store.MatchStoreComponent.Navigation +import com.stslex.feature.match.ui.store.MatchStoreComponent.State +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map + +class MatchStore( + appDispatcher: AppDispatcher, + router: MatchRouter, + pagerFactory: StorePagerFactory, + private val interactor: MatchInteractor, + private val userStore: UserStore, +) : Store( + appDispatcher = appDispatcher, + router = router, + initialState = State.INITIAL +) { + + private val pager: StorePager = pagerFactory.create( + request = { page, pageSize -> + interactor.getMatches( + uuid = state.value.uuid, + query = state.value.query, + page = page, + pageSize = pageSize ) + }, + scope = scope, + mapper = { it.toUi() }, + config = state.value.paging.config + ) + + override fun process(action: Action) { + Logger.debug("process: $action", TAG) + when (action) { + is Action.Init -> actionInit(action) + is Action.LoadMore -> actionLoadMore() + is Action.OnMatchClick -> actionOnMatchClick(action) + is Action.OnRetryClick -> actionRetryClick() + is Action.Refresh -> actionRefresh() + is Action.Logout -> actionLogout() + is Action.RepeatLastAction -> actionRepeatLastAction() + is Action.OnQueryChanged -> actionOnQueryChanged(action) } } - @Stable - sealed interface Event : Store.Event { - - data class ShowSnackbar( - val snackbar: Snackbar - ) : Event - } + private fun actionInit(action: Action.Init) { + pager.state.launch { pagerState -> + updateState { currentState -> + currentState.copy( + paging = pagerState.toUi(currentState.paging.config) + ) + } + } - @Stable - sealed interface Action : Store.Action { + pager.loadState.launch { loadState -> + updateState { currentState -> + currentState.copy( + screen = loadState.toUi() + ) + } + } - data class Init( - val args: MatchScreenArgs - ) : Action + pager.loadEvents.launch { + sendEvent( + Event.ShowSnackbar(Snackbar.Error("error load matches")) + ) + } - data object Refresh : Action + updateState { currentState -> + currentState.copy( + isSelf = action.args.isSelf, + uuid = action.args.uuid ?: userStore.uuid, + ) + } - data object LoadMore : Action + state + .map { it.query } + .distinctUntilChanged() + .launch( + onError = ::showError + ) { + if (pager.loadState.value is PagerLoadState.Initial) { + pager.initialLoad() + } else { + pager.refresh(isForceLoad = false) + } + } + } - data class OnMatchClick( - val matchUuid: String - ) : Action + private fun actionLoadMore() { + pager.load() + } - data object OnRetryClick : Action + private fun actionOnMatchClick(action: Action.OnMatchClick) { + consumeNavigation(Navigation.MatchDetails(action.matchUuid)) + } - data object Logout : Action + private fun actionRetryClick() { + pager.retry() + } - data object RepeatLastAction : Action, Store.Action.RepeatLastAction + private fun actionRefresh() { + pager.refresh(isForceLoad = true) + } - data class OnQueryChanged( - val query: String - ) : Action + private fun actionLogout() { + launch( + action = { + interactor.logout() + }, + onSuccess = { + consumeNavigation(Navigation.LogOut) + }, + onError = ::showError + ) } - @Stable - sealed interface Navigation : Store.Navigation { + private fun actionRepeatLastAction() { + val lastAction = lastAction ?: return + updateState { currentState -> + val screen = when (currentState.screen) { + is MatchScreenState.Content -> MatchScreenState.Content.Refresh + is MatchScreenState.Error, + is MatchScreenState.Shimmer, + is MatchScreenState.Empty -> MatchScreenState.Shimmer + } + currentState.copy(screen = screen) + } + process(lastAction) + } - data class MatchDetails(val matchUuid: String) : Navigation + private fun actionOnQueryChanged(action: Action.OnQueryChanged) { + updateState { currentState -> + currentState.copy( + query = action.query + ) + } + } - data object LogOut : Navigation + 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) + ) + } + } } -} + companion object { + private const val TAG = "MatchStore" + } +} \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchStoreComponent.kt b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchStoreComponent.kt new file mode 100644 index 00000000..e2556045 --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchStoreComponent.kt @@ -0,0 +1,76 @@ +package com.stslex.feature.match.ui.store + +import androidx.compose.runtime.Stable +import com.stslex.core.ui.base.paging.PagingConfig +import com.stslex.core.ui.base.paging.PagingUiState +import com.stslex.core.ui.mvi.StoreComponent +import com.stslex.core.ui.mvi.StoreComponent.Event.Snackbar +import com.stslex.core.ui.navigation.args.MatchScreenArgs +import com.stslex.feature.match.ui.model.MatchUiModel + +interface MatchStoreComponent : StoreComponent { + + @Stable + data class State( + val screen: MatchScreenState, + val uuid: String, + val isSelf: Boolean, + val query: String, + val paging: PagingUiState + ) : StoreComponent.State { + + companion object { + + val INITIAL = State( + screen = MatchScreenState.Shimmer, + paging = PagingUiState.default(PagingConfig.DEFAULT), + uuid = "", + isSelf = false, + query = "" + ) + } + } + + @Stable + sealed interface Event : StoreComponent.Event { + + data class ShowSnackbar( + val snackbar: Snackbar + ) : Event + } + + @Stable + sealed interface Action : StoreComponent.Action { + + data class Init( + val args: MatchScreenArgs + ) : Action + + data object Refresh : Action + + data object LoadMore : Action + + data class OnMatchClick( + val matchUuid: String + ) : Action + + data object OnRetryClick : Action + + data object Logout : Action + + data object RepeatLastAction : Action, StoreComponent.Action.RepeatLastAction + + data class OnQueryChanged( + val query: String + ) : Action + } + + @Stable + sealed interface Navigation : StoreComponent.Navigation { + + data class MatchDetails(val matchUuid: String) : Navigation + + data object LogOut : Navigation + } +} + 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 deleted file mode 100644 index c242f140..00000000 --- a/feature/match/src/commonMain/kotlin/com/stslex/feature/match/ui/store/MatchStoreImpl.kt +++ /dev/null @@ -1,176 +0,0 @@ -package com.stslex.feature.match.ui.store - -import com.stslex.core.core.AppDispatcher -import com.stslex.core.core.Logger -import com.stslex.core.database.store.UserStore -import com.stslex.core.ui.base.mapToAppError -import com.stslex.core.ui.base.paging.toUi -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.match.domain.interactor.MatchInteractor -import com.stslex.feature.match.navigation.MatchRouter -import com.stslex.feature.match.ui.model.MatchUiModel -import com.stslex.feature.match.ui.model.toUi -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 - -class MatchStoreImpl( - appDispatcher: AppDispatcher, - router: MatchRouter, - pagerFactory: StorePagerFactory, - private val interactor: MatchInteractor, - private val userStore: UserStore, -) : BaseStore( - appDispatcher = appDispatcher, - router = router, - initialState = State.INITIAL -), MatchStore { - - private val pager: StorePager = pagerFactory.create( - request = { page, pageSize -> - interactor.getMatches( - uuid = state.value.uuid, - query = state.value.query, - page = page, - pageSize = pageSize - ) - }, - scope = scope, - mapper = { it.toUi() }, - config = state.value.paging.config - ) - - override fun process(action: Action) { - Logger.debug("process: $action", TAG) - when (action) { - is Action.Init -> actionInit(action) - is Action.LoadMore -> actionLoadMore() - is Action.OnMatchClick -> actionOnMatchClick(action) - is Action.OnRetryClick -> actionRetryClick() - is Action.Refresh -> actionRefresh() - is Action.Logout -> actionLogout() - is Action.RepeatLastAction -> actionRepeatLastAction() - is Action.OnQueryChanged -> actionOnQueryChanged(action) - } - } - - private fun actionInit(action: Action.Init) { - pager.state.launch { pagerState -> - updateState { currentState -> - currentState.copy( - paging = pagerState.toUi(currentState.paging.config) - ) - } - } - - 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( - isSelf = action.args.isSelf, - uuid = action.args.uuid ?: userStore.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 actionLoadMore() { - pager.load() - } - - private fun actionOnMatchClick(action: Action.OnMatchClick) { - navigate(Navigation.MatchDetails(action.matchUuid)) - } - - private fun actionRetryClick() { - pager.retry() - } - - private fun actionRefresh() { - pager.refresh(isForceLoad = true) - } - - private fun actionLogout() { - launch( - action = { - interactor.logout() - }, - onSuccess = { - navigate(Navigation.LogOut) - }, - onError = ::showError - ) - } - - private fun actionRepeatLastAction() { - val lastAction = lastAction ?: return - updateState { currentState -> - val screen = when (currentState.screen) { - is MatchScreenState.Content -> MatchScreenState.Content.Refresh - is MatchScreenState.Error, - is MatchScreenState.Shimmer, - is MatchScreenState.Empty -> MatchScreenState.Shimmer - } - currentState.copy(screen = screen) - } - 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) - ) - } - } - } - - companion object { - private const val TAG = "MatchStore" - } -} \ No newline at end of file diff --git a/feature/match_feed/build.gradle.kts b/feature/match_feed/build.gradle.kts index 860a1298..1ce8b98b 100644 --- a/feature/match_feed/build.gradle.kts +++ b/feature/match_feed/build.gradle.kts @@ -14,8 +14,6 @@ kotlin { } } - jvm("desktop") - iosX64() iosArm64() iosSimulatorArm64() diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/di/MatchFeedModule.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/di/MatchFeedModule.kt index 9d2e9844..25a697e6 100644 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/di/MatchFeedModule.kt +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/di/MatchFeedModule.kt @@ -1,5 +1,6 @@ package com.stslex.feature.match_feed.di +import com.stslex.core.ui.mvi.storeDefinition import com.stslex.feature.match_feed.data.repository.MatchFeedMockRepositoryImpl import com.stslex.feature.match_feed.data.repository.MatchFeedRepository import com.stslex.feature.match_feed.domain.MatchFeedInteractor @@ -7,15 +8,14 @@ import com.stslex.feature.match_feed.domain.MatchFeedInteractorImpl import com.stslex.feature.match_feed.navigation.MatchFeedRouter import com.stslex.feature.match_feed.navigation.MatchFeedRouterImpl import com.stslex.feature.match_feed.ui.store.MatchFeedStore -import com.stslex.feature.match_feed.ui.store.MatchFeedStoreImpl import org.koin.dsl.module val featureMatchFeedModule = module { factory { MatchFeedMockRepositoryImpl(client = get()) } factory { MatchFeedInteractorImpl(repository = get()) } factory { MatchFeedRouterImpl(navigator = get()) } - factory { - MatchFeedStoreImpl( + storeDefinition { + MatchFeedStore( interactor = get(), appDispatcher = get(), router = get() diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/navigation/MatchFeedRouter.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/navigation/MatchFeedRouter.kt index 5fc5657d..88f13662 100644 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/navigation/MatchFeedRouter.kt +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/navigation/MatchFeedRouter.kt @@ -1,7 +1,7 @@ package com.stslex.feature.match_feed.navigation import com.stslex.core.ui.mvi.Router -import com.stslex.feature.match_feed.ui.store.MatchFeedStore.Navigation +import com.stslex.feature.match_feed.ui.store.MatchFeedStoreComponent.Navigation interface MatchFeedRouter : Router diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/navigation/MatchFeedRouterImpl.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/navigation/MatchFeedRouterImpl.kt index a3ad993e..e4cb015c 100644 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/navigation/MatchFeedRouterImpl.kt +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/navigation/MatchFeedRouterImpl.kt @@ -2,7 +2,7 @@ package com.stslex.feature.match_feed.navigation import com.stslex.core.ui.navigation.AppNavigator import com.stslex.core.ui.navigation.AppScreen -import com.stslex.feature.match_feed.ui.store.MatchFeedStore.Navigation +import com.stslex.feature.match_feed.ui.store.MatchFeedStoreComponent.Navigation class MatchFeedRouterImpl( private val navigator: AppNavigator diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/MatchFeedScreen.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/MatchFeedScreen.kt index 5a80f58f..240b53ba 100644 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/MatchFeedScreen.kt +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/MatchFeedScreen.kt @@ -9,21 +9,21 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import cafe.adriel.voyager.core.screen.Screen -import com.stslex.core.ui.mvi.getStoreTest +import com.stslex.core.ui.mvi.getStore import com.stslex.feature.match_feed.ui.components.MatchFeedScreenContent import com.stslex.feature.match_feed.ui.components.MatchFeedScreenError import com.stslex.feature.match_feed.ui.components.MatchFeedScreenLoading import com.stslex.feature.match_feed.ui.store.MatchFeedStore -import com.stslex.feature.match_feed.ui.store.MatchFeedStore.Action -import com.stslex.feature.match_feed.ui.store.MatchFeedStore.Event.ErrorSnackBar -import com.stslex.feature.match_feed.ui.store.MatchFeedStore.State +import com.stslex.feature.match_feed.ui.store.MatchFeedStoreComponent.Action +import com.stslex.feature.match_feed.ui.store.MatchFeedStoreComponent.Event.ErrorSnackBar +import com.stslex.feature.match_feed.ui.store.MatchFeedStoreComponent.State import com.stslex.feature.match_feed.ui.store.ScreenState object MatchFeedScreen : Screen { @Composable override fun Content() { - val store = getStoreTest() + val store = getStore() val state by remember { store.state }.collectAsState() LaunchedEffect(Unit) { diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/store/MatchFeedStore.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/store/MatchFeedStore.kt index fb38f92d..c93249f8 100644 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/store/MatchFeedStore.kt +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/store/MatchFeedStore.kt @@ -1,64 +1,131 @@ package com.stslex.feature.match_feed.ui.store -import androidx.compose.runtime.Stable +import com.stslex.core.core.AppDispatcher +import com.stslex.core.core.Logger import com.stslex.core.ui.mvi.Store -import com.stslex.feature.match_feed.ui.components.SwipeDirection -import com.stslex.feature.match_feed.ui.model.FilmUi -import com.stslex.feature.match_feed.ui.model.MatchUi -import com.stslex.feature.match_feed.ui.store.MatchFeedStore.Action -import com.stslex.feature.match_feed.ui.store.MatchFeedStore.Event -import com.stslex.feature.match_feed.ui.store.MatchFeedStore.State -import kotlinx.collections.immutable.ImmutableList +import com.stslex.feature.match_feed.domain.MatchFeedInteractor +import com.stslex.feature.match_feed.navigation.MatchFeedRouter +import com.stslex.feature.match_feed.ui.model.toUI +import com.stslex.feature.match_feed.ui.store.MatchFeedStoreComponent.Action +import com.stslex.feature.match_feed.ui.store.MatchFeedStoreComponent.Event +import com.stslex.feature.match_feed.ui.store.MatchFeedStoreComponent.Navigation +import com.stslex.feature.match_feed.ui.store.MatchFeedStoreComponent.State import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Job -interface MatchFeedStore : Store { +class MatchFeedStore( + private val interactor: MatchFeedInteractor, + appDispatcher: AppDispatcher, + router: MatchFeedRouter +) : Store( + initialState = State.INITIAL, + appDispatcher = appDispatcher, + router = router +) { - @Stable - data class State( - val films: ImmutableList, - val screen: ScreenState, - val match: MatchUi?, - val currentPage: Int, - val hasNextPage: Boolean - ) : Store.State { - companion object { - val INITIAL = State( - screen = ScreenState.Loading, - films = emptyList().toImmutableList(), - currentPage = 0, - hasNextPage = true, - match = null - ) + private var loadingJob: Job? = null + + override fun process(action: Action) { + when (action) { + Action.Init -> actionInit() + Action.LoadFilms -> actionLoadFilms() + is Action.FilmClick -> actionFilmClick(action) + is Action.FilmSwiped -> actionFilmSwiped(action) } } - sealed interface Event : Store.Event { + private fun actionFilmSwiped(action: Action.FilmSwiped) { + // todo send action to backend + } - @Stable - data class ErrorSnackBar(val message: String) : Event + private fun actionFilmClick(action: Action.FilmClick) { + consumeNavigation(Navigation.Film(action.uuid)) } - sealed interface Navigation : Store.Navigation { + private fun actionInit() { + interactor + .getLatestMatch() + .launch { match -> + updateState { currentState -> + currentState.copy( + screen = ScreenState.Content.Success, + match = match.toUI() + ) + } + loadFilms(match.uuid) + } + } - data class Film(val uuid: String) : Navigation + private fun actionLoadFilms() { + val matchUuid = state.value.match?.uuid + if (matchUuid == null) { + Logger.debug("Match uuid is null") + return + } + loadFilms(matchUuid) } - sealed interface Action : Store.Action { + private fun loadFilms(uuid: String) { + if (loadingJob?.isActive == true) { + Logger.debug("Loading job is active") + return + } + val hasNextPage = state.value.hasNextPage + if (hasNextPage.not()) { + Logger.debug("No more pages") + return + } + val loadScreenState = when (state.value.screen) { + is ScreenState.Content -> ScreenState.Content.AppendLoading + is ScreenState.Loading -> ScreenState.Loading + is ScreenState.Error -> ScreenState.Loading + } + + updateState { currentState -> + currentState.copy( + screen = loadScreenState + ) + } - data object Init : Action + val currentPage = state.value.currentPage + loadingJob = launch( + action = { + interactor.getMatchFilms( + matchUuid = uuid, + page = currentPage.inc(), + pageSize = PAGE_SIZE + ) + }, + onSuccess = { feed -> + val films = feed.films.toUI() + updateState { currentState -> + val currentFilms = currentState.films.toMutableList() + currentFilms.addAll(films) + currentState.copy( + films = currentFilms.toImmutableList(), + screen = ScreenState.Content.Success, + currentPage = currentPage.inc(), + hasNextPage = films.size == PAGE_SIZE + ) + } + }, + onError = { throwable -> + if (state.value.films.isEmpty()) { + updateState { + it.copy( + screen = ScreenState.Error(throwable.message ?: "Unknown error") + ) + } + } else { + sendEvent(Event.ErrorSnackBar(throwable.message ?: "Unknown error")) + } + }, + ) + } - data object LoadFilms : Action - @Stable - data class FilmClick( - val uuid: String - ) : Action + companion object { - @Stable - data class FilmSwiped( - val direction: SwipeDirection, - val uuid: String - ) : Action + private const val PAGE_SIZE = 5 } -} - +} \ No newline at end of file diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/store/MatchFeedStoreComponent.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/store/MatchFeedStoreComponent.kt new file mode 100644 index 00000000..4166a3f5 --- /dev/null +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/store/MatchFeedStoreComponent.kt @@ -0,0 +1,61 @@ +package com.stslex.feature.match_feed.ui.store + +import androidx.compose.runtime.Stable +import com.stslex.core.ui.mvi.StoreComponent +import com.stslex.feature.match_feed.ui.components.SwipeDirection +import com.stslex.feature.match_feed.ui.model.FilmUi +import com.stslex.feature.match_feed.ui.model.MatchUi +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +interface MatchFeedStoreComponent : StoreComponent { + + @Stable + data class State( + val films: ImmutableList, + val screen: ScreenState, + val match: MatchUi?, + val currentPage: Int, + val hasNextPage: Boolean + ) : StoreComponent.State { + companion object { + val INITIAL = State( + screen = ScreenState.Loading, + films = emptyList().toImmutableList(), + currentPage = 0, + hasNextPage = true, + match = null + ) + } + } + + sealed interface Event : StoreComponent.Event { + + @Stable + data class ErrorSnackBar(val message: String) : Event + } + + sealed interface Navigation : StoreComponent.Navigation { + + data class Film(val uuid: String) : Navigation + } + + sealed interface Action : StoreComponent.Action { + + data object Init : Action + + data object LoadFilms : Action + + @Stable + data class FilmClick( + val uuid: String + ) : Action + + @Stable + data class FilmSwiped( + val direction: SwipeDirection, + val uuid: String + ) : Action + } +} + diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/store/MatchFeedStoreImpl.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/store/MatchFeedStoreImpl.kt deleted file mode 100644 index 735ac09e..00000000 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/store/MatchFeedStoreImpl.kt +++ /dev/null @@ -1,130 +0,0 @@ -package com.stslex.feature.match_feed.ui.store - -import com.stslex.core.core.AppDispatcher -import com.stslex.core.core.Logger -import com.stslex.core.ui.mvi.BaseStore -import com.stslex.feature.match_feed.domain.MatchFeedInteractor -import com.stslex.feature.match_feed.navigation.MatchFeedRouter -import com.stslex.feature.match_feed.ui.model.toUI -import com.stslex.feature.match_feed.ui.store.MatchFeedStore.Action -import com.stslex.feature.match_feed.ui.store.MatchFeedStore.Event -import com.stslex.feature.match_feed.ui.store.MatchFeedStore.Navigation -import com.stslex.feature.match_feed.ui.store.MatchFeedStore.State -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.Job - -class MatchFeedStoreImpl( - private val interactor: MatchFeedInteractor, - appDispatcher: AppDispatcher, - router: MatchFeedRouter -) : MatchFeedStore, BaseStore( - initialState = State.INITIAL, - appDispatcher = appDispatcher, - router = router -) { - private var loadingJob: Job? = null - - override fun process(action: Action) { - when (action) { - Action.Init -> actionInit() - Action.LoadFilms -> actionLoadFilms() - is Action.FilmClick -> actionFilmClick(action) - is Action.FilmSwiped -> actionFilmSwiped(action) - } - } - - private fun actionFilmSwiped(action: Action.FilmSwiped) { - // todo send action to backend - } - - private fun actionFilmClick(action: Action.FilmClick) { - navigate(Navigation.Film(action.uuid)) - } - - private fun actionInit() { - interactor - .getLatestMatch() - .launch { match -> - updateState { currentState -> - currentState.copy( - screen = ScreenState.Content.Success, - match = match.toUI() - ) - } - loadFilms(match.uuid) - } - } - - private fun actionLoadFilms() { - val matchUuid = state.value.match?.uuid - if (matchUuid == null) { - Logger.debug("Match uuid is null") - return - } - loadFilms(matchUuid) - } - - private fun loadFilms(uuid: String) { - if (loadingJob?.isActive == true) { - Logger.debug("Loading job is active") - return - } - val hasNextPage = state.value.hasNextPage - if (hasNextPage.not()) { - Logger.debug("No more pages") - return - } - val loadScreenState = when (state.value.screen) { - is ScreenState.Content -> ScreenState.Content.AppendLoading - is ScreenState.Loading -> ScreenState.Loading - is ScreenState.Error -> ScreenState.Loading - } - - updateState { currentState -> - currentState.copy( - screen = loadScreenState - ) - } - - val currentPage = state.value.currentPage - loadingJob = launch( - action = { - interactor.getMatchFilms( - matchUuid = uuid, - page = currentPage.inc(), - pageSize = PAGE_SIZE - ) - }, - onSuccess = { feed -> - val films = feed.films.toUI() - updateState { currentState -> - val currentFilms = currentState.films.toMutableList() - currentFilms.addAll(films) - currentState.copy( - films = currentFilms.toImmutableList(), - screen = ScreenState.Content.Success, - currentPage = currentPage.inc(), - hasNextPage = films.size == PAGE_SIZE - ) - } - }, - onError = { throwable -> - if (state.value.films.isEmpty()) { - updateState { - it.copy( - screen = ScreenState.Error(throwable.message ?: "Unknown error") - ) - } - } else { - sendEvent(Event.ErrorSnackBar(throwable.message ?: "Unknown error")) - } - }, - ) - } - - - companion object { - - private const val PAGE_SIZE = 5 - } -} \ No newline at end of file diff --git a/feature/profile/build.gradle.kts b/feature/profile/build.gradle.kts index 71d1e751..2a7ac91f 100644 --- a/feature/profile/build.gradle.kts +++ b/feature/profile/build.gradle.kts @@ -14,8 +14,6 @@ kotlin { } } - jvm("desktop") - iosX64() iosArm64() iosSimulatorArm64() diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/di/ProfileModule.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/di/ProfileModule.kt index d556174e..6ec73aa6 100644 --- a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/di/ProfileModule.kt +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/di/ProfileModule.kt @@ -1,5 +1,6 @@ package com.stslex.feature.profile.di +import com.stslex.core.ui.mvi.storeDefinition import com.stslex.feature.profile.data.repository.ProfileRepository import com.stslex.feature.profile.data.repository.ProfileRepositoryImpl import com.stslex.feature.profile.domain.interactor.ProfileInteractor @@ -7,12 +8,11 @@ import com.stslex.feature.profile.domain.interactor.ProfileInteractorImpl import com.stslex.feature.profile.navigation.ProfileRouter import com.stslex.feature.profile.navigation.ProfileRouterImpl import com.stslex.feature.profile.ui.store.ProfileStore -import com.stslex.feature.profile.ui.store.ProfileStoreImpl import org.koin.dsl.module val featureProfileModule = module { - factory { - ProfileStoreImpl( + storeDefinition { + ProfileStore( interactor = get(), userStore = get(), appDispatcher = get(), diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouter.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouter.kt index ada545fc..004ac468 100644 --- a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouter.kt +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouter.kt @@ -1,6 +1,6 @@ package com.stslex.feature.profile.navigation import com.stslex.core.ui.mvi.Router -import com.stslex.feature.profile.ui.store.ProfileStore +import com.stslex.feature.profile.ui.store.ProfileStoreComponent -interface ProfileRouter : Router \ No newline at end of file +interface ProfileRouter : Router \ No newline at end of file diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouterImpl.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouterImpl.kt index e45b133b..b472ccf9 100644 --- a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouterImpl.kt +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouterImpl.kt @@ -2,7 +2,7 @@ package com.stslex.feature.profile.navigation import com.stslex.core.ui.navigation.AppNavigator import com.stslex.core.ui.navigation.AppScreen -import com.stslex.feature.profile.ui.store.ProfileStore.Navigation +import com.stslex.feature.profile.ui.store.ProfileStoreComponent.Navigation class ProfileRouterImpl( private val navigator: AppNavigator diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/ProfileScreen.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/ProfileScreen.kt index b6c5d852..a95f4b29 100644 --- a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/ProfileScreen.kt +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/ProfileScreen.kt @@ -22,15 +22,15 @@ import androidx.compose.ui.Modifier import cafe.adriel.voyager.core.screen.Screen import com.stslex.core.network.api.server.model.ErrorRefresh import com.stslex.core.ui.components.AppSnackbarHost -import com.stslex.core.ui.mvi.getStoreTest +import com.stslex.core.ui.mvi.getStore import com.stslex.core.ui.theme.AppDimension import com.stslex.feature.profile.navigation.ProfileScreenArguments import com.stslex.feature.profile.ui.components.ProfileScreenContent import com.stslex.feature.profile.ui.store.ProfileScreenState import com.stslex.feature.profile.ui.store.ProfileStore -import com.stslex.feature.profile.ui.store.ProfileStore.Action -import com.stslex.feature.profile.ui.store.ProfileStore.Event -import com.stslex.feature.profile.ui.store.ProfileStore.State +import com.stslex.feature.profile.ui.store.ProfileStoreComponent.Action +import com.stslex.feature.profile.ui.store.ProfileStoreComponent.Event +import com.stslex.feature.profile.ui.store.ProfileStoreComponent.State data class ProfileScreen( val args: ProfileScreenArguments @@ -38,7 +38,7 @@ data class ProfileScreen( @Composable override fun Content() { - val store = getStoreTest() + val store = getStore() LaunchedEffect(Unit) { store.sendAction(Action.Init(args = args)) } diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/components/ProfileScreenContent.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/components/ProfileScreenContent.kt index ab6084b7..0e567490 100644 --- a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/components/ProfileScreenContent.kt +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/components/ProfileScreenContent.kt @@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.stslex.feature.profile.ui.store.ProfileScreenState -import com.stslex.feature.profile.ui.store.ProfileStore.Action +import com.stslex.feature.profile.ui.store.ProfileStoreComponent.Action @Composable internal fun ProfileScreenContent( diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/components/ProfileScreenToolbar.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/components/ProfileScreenToolbar.kt index 4e33b46c..4918ae75 100644 --- a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/components/ProfileScreenToolbar.kt +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/components/ProfileScreenToolbar.kt @@ -20,7 +20,7 @@ fun ProfileScreenToolbar( AppToolbar( modifier = modifier, title = nickname, - onBackClick = onBackClick, + onBackClick = onBackClick.takeIf { isCurrentUser.not() }, isActionVisible = isCurrentUser, actionIcon = { IconButton( diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStore.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStore.kt index fa4f93e4..d0dd57c8 100644 --- a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStore.kt +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStore.kt @@ -1,80 +1,140 @@ package com.stslex.feature.profile.ui.store -import androidx.compose.runtime.Stable +import androidx.compose.ui.graphics.Color +import com.stslex.core.core.AppDispatcher +import com.stslex.core.database.store.UserStore import com.stslex.core.ui.mvi.Store -import com.stslex.core.ui.mvi.Store.Event.Snackbar -import com.stslex.feature.profile.navigation.ProfileScreenArguments -import com.stslex.feature.profile.ui.store.ProfileStore.Action -import com.stslex.feature.profile.ui.store.ProfileStore.Event -import com.stslex.feature.profile.ui.store.ProfileStore.State - -interface ProfileStore : Store { - - @Stable - data class State( - val uuid: String, - val isSelf: Boolean, - val screen: ProfileScreenState - ) : Store.State { - - companion object { - - val INITIAL = State( - uuid = "", - isSelf = false, - screen = ProfileScreenState.Shimmer - ) +import com.stslex.core.ui.mvi.StoreComponent.Event.Snackbar +import com.stslex.feature.profile.domain.interactor.ProfileInteractor +import com.stslex.feature.profile.navigation.ProfileRouter +import com.stslex.feature.profile.ui.model.ProfileAvatarModel +import com.stslex.feature.profile.ui.model.toUi +import com.stslex.feature.profile.ui.store.ProfileStoreComponent.Action +import com.stslex.feature.profile.ui.store.ProfileStoreComponent.Event +import com.stslex.feature.profile.ui.store.ProfileStoreComponent.Navigation +import com.stslex.feature.profile.ui.store.ProfileStoreComponent.State + +class ProfileStore( + private val interactor: ProfileInteractor, + private val userStore: UserStore, + router: ProfileRouter, + appDispatcher: AppDispatcher, +) : Store( + router = router, + appDispatcher = appDispatcher, + initialState = State.INITIAL, +) { + + override fun process(action: Action) { + when (action) { + is Action.Init -> actionInit(action) + is Action.Logout -> actionLogout() + is Action.RepeatLastAction -> actionRepeatLastAction() + is Action.FavouriteClick -> actionFavouriteClick() + is Action.FollowingClick -> actionFollowingClick() + is Action.FollowersClick -> actionFollowersClick() + is Action.SettingsClick -> actionSettingsClick() + is Action.BackButtonClick -> actionBackClick() } } - @Stable - sealed interface Action : Store.Action { - - @Stable - data class Init( - val args: ProfileScreenArguments - ) : Action - - data object Logout : Action - - data object RepeatLastAction : Action, Store.Action.RepeatLastAction + private fun actionInit(action: Action.Init) { + val uuid = action.args.uuid ?: userStore.uuid - data object FavouriteClick : Action - - data object FollowingClick : Action - - data object FollowersClick : Action + updateState { currentState -> + currentState.copy( + isSelf = action.args.isSelf, + uuid = uuid, + ) + } - data object SettingsClick : Action + interactor.getProfile(uuid) + .launch( + onError = { error -> + updateState { currentState -> + currentState.copy( + screen = ProfileScreenState.Error(error) + ) + } + } + ) { profile -> + val avatar = if (profile.avatarUrl.isBlank()) { + ProfileAvatarModel.Empty( + color = Color.Gray, // TODO replace with random color + symbol = profile.username.firstOrNull()?.lowercase().orEmpty() + ) + } else { + ProfileAvatarModel.Content(profile.avatarUrl) + } + val profileUi = profile.toUi( + avatarModel = avatar + ) + updateState { currentState -> + currentState.copy( + screen = ProfileScreenState.Content.NotLoading(profileUi) + ) + } + } + } - data object BackButtonClick : Action + private fun actionFavouriteClick() { + consumeNavigation(Navigation.Favourite(state.value.uuid)) } - @Stable - sealed interface Event : Store.Event { + private fun actionFollowingClick() { + consumeNavigation(Navigation.Following(state.value.uuid)) + } - @Stable - data class ShowSnackbar(val snackbar: Snackbar) : Event + private fun actionFollowersClick() { + consumeNavigation(Navigation.Followers(state.value.uuid)) } - sealed interface Navigation : Store.Navigation { + private fun actionRepeatLastAction() { + val lastAction = lastAction ?: return + updateState { currentState -> + val screen = when (val screen = currentState.screen) { + is ProfileScreenState.Content -> ProfileScreenState.Content.Loading(screen.data) + is ProfileScreenState.Error, is ProfileScreenState.Shimmer -> ProfileScreenState.Shimmer + } + currentState.copy(screen = screen) + } + process(lastAction) + } - data object LogIn : Navigation + private fun actionLogout() { + val currentScreen = state.value.screen - data class Favourite( - val uuid: String - ) : Navigation + if ( + currentScreen is ProfileScreenState.Content.Loading || + currentScreen is ProfileScreenState.Shimmer + ) { + return + } - data class Following( - val uuid: String - ) : Navigation + updateState { currentState -> + currentState.copy( + screen = ProfileScreenState.Shimmer + ) + } - data class Followers( - val uuid: String - ) : Navigation + launch( + action = { + interactor.logOut() + }, + onSuccess = { + consumeNavigation(Navigation.LogIn) + }, + onError = { error -> + sendEvent(Event.ShowSnackbar(Snackbar.Error(error.message ?: "error logout"))) + } + ) + } - data object Back : Navigation + private fun actionSettingsClick() { + consumeNavigation(Navigation.Settings) + } - data object Settings : Navigation + private fun actionBackClick() { + consumeNavigation(Navigation.Back) } -} \ No newline at end of file +} diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStoreComponent.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStoreComponent.kt new file mode 100644 index 00000000..01f7b241 --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStoreComponent.kt @@ -0,0 +1,77 @@ +package com.stslex.feature.profile.ui.store + +import androidx.compose.runtime.Stable +import com.stslex.core.ui.mvi.StoreComponent +import com.stslex.core.ui.mvi.StoreComponent.Event.Snackbar +import com.stslex.feature.profile.navigation.ProfileScreenArguments + +interface ProfileStoreComponent : StoreComponent { + + @Stable + data class State( + val uuid: String, + val isSelf: Boolean, + val screen: ProfileScreenState + ) : StoreComponent.State { + + companion object { + + val INITIAL = State( + uuid = "", + isSelf = false, + screen = ProfileScreenState.Shimmer + ) + } + } + + @Stable + sealed interface Action : StoreComponent.Action { + + @Stable + data class Init( + val args: ProfileScreenArguments + ) : Action + + data object Logout : Action + + data object RepeatLastAction : Action, StoreComponent.Action.RepeatLastAction + + data object FavouriteClick : Action + + data object FollowingClick : Action + + data object FollowersClick : Action + + data object SettingsClick : Action + + data object BackButtonClick : Action + } + + @Stable + sealed interface Event : StoreComponent.Event { + + @Stable + data class ShowSnackbar(val snackbar: Snackbar) : Event + } + + sealed interface Navigation : StoreComponent.Navigation { + + data object LogIn : Navigation + + data class Favourite( + val uuid: String + ) : Navigation + + data class Following( + val uuid: String + ) : Navigation + + data class Followers( + val uuid: String + ) : Navigation + + data object Back : Navigation + + data object Settings : Navigation + } +} \ No newline at end of file diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStoreImpl.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStoreImpl.kt deleted file mode 100644 index a13a8e2d..00000000 --- a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStoreImpl.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.stslex.feature.profile.ui.store - -import androidx.compose.ui.graphics.Color -import com.stslex.core.core.AppDispatcher -import com.stslex.core.database.store.UserStore -import com.stslex.core.ui.mvi.BaseStore -import com.stslex.core.ui.mvi.Store.Event.Snackbar -import com.stslex.feature.profile.domain.interactor.ProfileInteractor -import com.stslex.feature.profile.navigation.ProfileRouter -import com.stslex.feature.profile.ui.model.ProfileAvatarModel -import com.stslex.feature.profile.ui.model.toUi -import com.stslex.feature.profile.ui.store.ProfileStore.Action -import com.stslex.feature.profile.ui.store.ProfileStore.Event -import com.stslex.feature.profile.ui.store.ProfileStore.Navigation -import com.stslex.feature.profile.ui.store.ProfileStore.State - -class ProfileStoreImpl( - private val interactor: ProfileInteractor, - private val userStore: UserStore, - router: ProfileRouter, - appDispatcher: AppDispatcher, -) : ProfileStore, BaseStore( - router = router, - appDispatcher = appDispatcher, - initialState = State.INITIAL, -) { - - override fun process(action: Action) { - when (action) { - is Action.Init -> actionInit(action) - is Action.Logout -> actionLogout() - is Action.RepeatLastAction -> actionRepeatLastAction() - is Action.FavouriteClick -> actionFavouriteClick() - is Action.FollowingClick -> actionFollowingClick() - is Action.FollowersClick -> actionFollowersClick() - is Action.SettingsClick -> actionSettingsClick() - is Action.BackButtonClick -> actionBackClick() - } - } - - private fun actionInit(action: Action.Init) { - val uuid = action.args.uuid ?: userStore.uuid - - updateState { currentState -> - currentState.copy( - isSelf = action.args.isSelf, - uuid = uuid, - ) - } - - interactor.getProfile(uuid) - .launch( - onError = { error -> - updateState { currentState -> - currentState.copy( - screen = ProfileScreenState.Error(error) - ) - } - } - ) { profile -> - val avatar = if (profile.avatarUrl.isBlank()) { - ProfileAvatarModel.Empty( - color = Color.Gray, // TODO replace with random color - symbol = profile.username.firstOrNull()?.lowercase().orEmpty() - ) - } else { - ProfileAvatarModel.Content(profile.avatarUrl) - } - val profileUi = profile.toUi( - avatarModel = avatar - ) - updateState { currentState -> - currentState.copy( - screen = ProfileScreenState.Content.NotLoading(profileUi) - ) - } - } - } - - private fun actionFavouriteClick() { - navigate(Navigation.Favourite(state.value.uuid)) - } - - private fun actionFollowingClick() { - navigate(Navigation.Following(state.value.uuid)) - } - - private fun actionFollowersClick() { - navigate(Navigation.Followers(state.value.uuid)) - } - - private fun actionRepeatLastAction() { - val lastAction = lastAction ?: return - updateState { currentState -> - val screen = when (val screen = currentState.screen) { - is ProfileScreenState.Content -> ProfileScreenState.Content.Loading(screen.data) - is ProfileScreenState.Error, is ProfileScreenState.Shimmer -> ProfileScreenState.Shimmer - } - currentState.copy(screen = screen) - } - process(lastAction) - } - - private fun actionLogout() { - val currentScreen = state.value.screen - - if ( - currentScreen is ProfileScreenState.Content.Loading || - currentScreen is ProfileScreenState.Shimmer - ) { - return - } - - updateState { currentState -> - currentState.copy( - screen = ProfileScreenState.Shimmer - ) - } - - launch( - action = { - interactor.logOut() - }, - onSuccess = { - navigate(Navigation.LogIn) - }, - onError = { error -> - sendEvent(Event.ShowSnackbar(Snackbar.Error(error.message ?: "error logout"))) - } - ) - } - - private fun actionSettingsClick() { - navigate(Navigation.Settings) - } - - private fun actionBackClick() { - navigate(Navigation.Back) - } -} diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index 2c4126fb..1627d1e4 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -14,8 +14,6 @@ kotlin { } } - jvm("desktop") - iosX64() iosArm64() iosSimulatorArm64() @@ -24,7 +22,8 @@ kotlin { homepage = "Link to the Shared Module homepage" version = "1.0" ios.deploymentTarget = "16.0" - podfile = project.file(project.rootProject.projectDir.path + "/iosApp/FeatureSettingsPodfile") + podfile = + project.file(project.rootProject.projectDir.path + "/iosApp/FeatureSettingsPodfile") framework { baseName = "featureSettings" } diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/di/SettingsModule.kt b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/di/SettingsModule.kt index 02b371fe..29be1ed7 100644 --- a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/di/SettingsModule.kt +++ b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/di/SettingsModule.kt @@ -1,16 +1,16 @@ package com.stslex.feature.settings.di +import com.stslex.core.ui.mvi.storeDefinition import com.stslex.feature.settings.domain.SettingsInteractor import com.stslex.feature.settings.domain.SettingsInteractorImpl import com.stslex.feature.settings.navigation.SettingsRouter import com.stslex.feature.settings.navigation.SettingsRouterImpl import com.stslex.feature.settings.ui.store.SettingsStore -import com.stslex.feature.settings.ui.store.SettingsStoreImpl import org.koin.dsl.module val featureSettingsModule = module { - factory { - SettingsStoreImpl( + storeDefinition { + SettingsStore( interactor = get(), appDispatcher = get(), router = get(), diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/navigation/SettingsRouter.kt b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/navigation/SettingsRouter.kt index f84db80a..9fe9df25 100644 --- a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/navigation/SettingsRouter.kt +++ b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/navigation/SettingsRouter.kt @@ -1,6 +1,6 @@ package com.stslex.feature.settings.navigation import com.stslex.core.ui.mvi.Router -import com.stslex.feature.settings.ui.store.SettingsStore +import com.stslex.feature.settings.ui.store.SettingsStoreComponent -interface SettingsRouter : Router +interface SettingsRouter : Router diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/navigation/SettingsRouterImpl.kt b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/navigation/SettingsRouterImpl.kt index ad7c2ab5..6e7cad0e 100644 --- a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/navigation/SettingsRouterImpl.kt +++ b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/navigation/SettingsRouterImpl.kt @@ -2,7 +2,7 @@ package com.stslex.feature.settings.navigation import com.stslex.core.ui.navigation.AppNavigator import com.stslex.core.ui.navigation.AppScreen -import com.stslex.feature.settings.ui.store.SettingsStore.Navigation +import com.stslex.feature.settings.ui.store.SettingsStoreComponent.Navigation class SettingsRouterImpl( private val navigator: AppNavigator diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/SettingsScreen.kt b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/SettingsScreen.kt index 255bc80e..c4196e61 100644 --- a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/SettingsScreen.kt +++ b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/SettingsScreen.kt @@ -18,19 +18,19 @@ import androidx.compose.ui.Modifier import cafe.adriel.voyager.core.screen.Screen import com.stslex.core.ui.components.AppSnackbarHost import com.stslex.core.ui.components.AppToolbar -import com.stslex.core.ui.mvi.getStoreTest +import com.stslex.core.ui.mvi.getStore import com.stslex.core.ui.theme.AppDimension import com.stslex.feature.settings.ui.components.SettingsContent import com.stslex.feature.settings.ui.store.SettingsStore -import com.stslex.feature.settings.ui.store.SettingsStore.Action -import com.stslex.feature.settings.ui.store.SettingsStore.Event -import com.stslex.feature.settings.ui.store.SettingsStore.State +import com.stslex.feature.settings.ui.store.SettingsStoreComponent.Action +import com.stslex.feature.settings.ui.store.SettingsStoreComponent.Event +import com.stslex.feature.settings.ui.store.SettingsStoreComponent.State object SettingsScreen : Screen { @Composable override fun Content() { - val store = getStoreTest() + val store = getStore() val state by remember { store.state }.collectAsState() val snackbarHostState = remember { SnackbarHostState() } LaunchedEffect(Unit) { diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/store/SettingsStore.kt b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/store/SettingsStore.kt index 09e5564d..7b7ae544 100644 --- a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/store/SettingsStore.kt +++ b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/store/SettingsStore.kt @@ -1,43 +1,61 @@ package com.stslex.feature.settings.ui.store -import androidx.compose.runtime.Stable +import com.stslex.core.core.AppDispatcher import com.stslex.core.ui.mvi.Store -import com.stslex.core.ui.mvi.Store.Event.Snackbar -import com.stslex.feature.settings.ui.store.SettingsStore.Action -import com.stslex.feature.settings.ui.store.SettingsStore.Event -import com.stslex.feature.settings.ui.store.SettingsStore.State - -interface SettingsStore : Store { - - @Stable - data class State( - val isLoading: Boolean - ) : Store.State { - - companion object { - - val INITIAL = State(isLoading = false) +import com.stslex.core.ui.mvi.StoreComponent.Event.Snackbar +import com.stslex.feature.settings.domain.SettingsInteractor +import com.stslex.feature.settings.navigation.SettingsRouter +import com.stslex.feature.settings.ui.store.SettingsStoreComponent.Action +import com.stslex.feature.settings.ui.store.SettingsStoreComponent.Event +import com.stslex.feature.settings.ui.store.SettingsStoreComponent.Navigation +import com.stslex.feature.settings.ui.store.SettingsStoreComponent.State + +class SettingsStore( + private val interactor: SettingsInteractor, + router: SettingsRouter, + appDispatcher: AppDispatcher +) : Store( + router = router, + appDispatcher = appDispatcher, + initialState = State.INITIAL +) { + + override fun process(action: Action) { + when (action) { + Action.LogOut -> actionLogout() + Action.BackButtonClicked -> actionBackClick() } } - @Stable - sealed interface Action : Store.Action { - - data object LogOut : Action - - data object BackButtonClicked : Action + private fun actionBackClick() { + consumeNavigation(Navigation.Back) } - @Stable - sealed interface Event : Store.Event { - - data class ShowSnackbar(val snackbar: Snackbar) : Event - } - - sealed interface Navigation : Store.Navigation { - - data object Back : Navigation + private fun actionLogout() { + if (state.value.isLoading) return + updateState { currentState -> + currentState.copy( + isLoading = true + ) + } - data object LogOut : Navigation + launch( + action = { + interactor.logOut() + }, + onSuccess = { + updateState { currentState -> + currentState.copy( + isLoading = false + ) + } + consumeNavigation(Navigation.LogOut) + }, + onError = { error -> + val message = error.message ?: "Logout error" + val snackbarType = Snackbar.Error(message) + sendEvent(Event.ShowSnackbar(snackbarType)) + } + ) } } \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/store/SettingsStoreComponent.kt b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/store/SettingsStoreComponent.kt new file mode 100644 index 00000000..47dbf856 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/store/SettingsStoreComponent.kt @@ -0,0 +1,40 @@ +package com.stslex.feature.settings.ui.store + +import androidx.compose.runtime.Stable +import com.stslex.core.ui.mvi.StoreComponent +import com.stslex.core.ui.mvi.StoreComponent.Event.Snackbar + +interface SettingsStoreComponent : StoreComponent { + + @Stable + data class State( + val isLoading: Boolean + ) : StoreComponent.State { + + companion object { + + val INITIAL = State(isLoading = false) + } + } + + @Stable + sealed interface Action : StoreComponent.Action { + + data object LogOut : Action + + data object BackButtonClicked : Action + } + + @Stable + sealed interface Event : StoreComponent.Event { + + data class ShowSnackbar(val snackbar: Snackbar) : Event + } + + sealed interface Navigation : StoreComponent.Navigation { + + data object Back : Navigation + + data object LogOut : Navigation + } +} \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/store/SettingsStoreImpl.kt b/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/store/SettingsStoreImpl.kt deleted file mode 100644 index 8299a5a6..00000000 --- a/feature/settings/src/commonMain/kotlin/com/stslex/feature/settings/ui/store/SettingsStoreImpl.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.stslex.feature.settings.ui.store - -import com.stslex.core.core.AppDispatcher -import com.stslex.core.ui.mvi.BaseStore -import com.stslex.core.ui.mvi.Store.Event.Snackbar -import com.stslex.feature.settings.domain.SettingsInteractor -import com.stslex.feature.settings.navigation.SettingsRouter -import com.stslex.feature.settings.ui.store.SettingsStore.Action -import com.stslex.feature.settings.ui.store.SettingsStore.Event -import com.stslex.feature.settings.ui.store.SettingsStore.Navigation -import com.stslex.feature.settings.ui.store.SettingsStore.State - -class SettingsStoreImpl( - private val interactor: SettingsInteractor, - router: SettingsRouter, - appDispatcher: AppDispatcher -) : SettingsStore, BaseStore( - router = router, - appDispatcher = appDispatcher, - initialState = State.INITIAL -) { - - override fun process(action: Action) { - when (action) { - Action.LogOut -> actionLogout() - Action.BackButtonClicked -> actionBackClick() - } - } - - private fun actionBackClick() { - navigate(Navigation.Back) - } - - private fun actionLogout() { - if (state.value.isLoading) return - updateState { currentState -> - currentState.copy( - isLoading = true - ) - } - - launch( - action = { - interactor.logOut() - }, - onSuccess = { - updateState { currentState -> - currentState.copy( - isLoading = false - ) - } - navigate(Navigation.LogOut) - }, - onError = { error -> - val message = error.message ?: "Logout error" - val snackbarType = Snackbar.Error(message) - sendEvent(Event.ShowSnackbar(snackbarType)) - } - ) - } -} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6bbeb730..80e7e3b7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,19 +1,19 @@ [versions] kotlin = "1.9.23" -compose-plugin = "1.6.1" +compose-plugin = "1.6.10-beta01" android-minSdk = "24" android-compileSdk = "34" android-targetSdk = "34" logback = "1.4.11" -androidCompose = "1.6.6" +androidCompose = "1.6.7" compose-compiler = "1.5.4" agp = "8.2.2" androidx-activityCompose = "1.9.0" -androidx-core-ktx = "1.13.0" +androidx-core-ktx = "1.13.1" androidx-appcompat = "1.6.1" -androidx-material = "1.11.0" +androidx-material = "1.12.0" androidx-constraintlayout = "2.1.4" androidx-test-junit = "1.1.5" androidx-espresso-core = "3.5.1" @@ -23,7 +23,7 @@ koin = "3.4.3" koin-compose = "1.0.4" ktor = "2.3.6" immutableCollection = "0.3.5" -coroutines = "1.7.3" +coroutines = "1.8.0" voyagerVersion = "1.0.0-rc10" kermit = "2.0.2" @@ -31,8 +31,8 @@ kamel = "0.9.0" coil = "2.5.0" buildConfig = "4.2.0" -lifecycleRuntimeKtx = "2.7.0" -androixComposeBom = "2024.04.01" +lifecycleRuntimeKtx = "2.8.0" +androixComposeBom = "2024.05.00" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }