From 8d482ad9edb9bdc88597d41dda88df94b6c1d2b0 Mon Sep 17 00:00:00 2001 From: stslex Date: Sat, 2 Nov 2024 09:22:16 +0300 Subject: [PATCH] refactor all stores --- .../stslex/wizard/core/ui/mvi/BaseStore.kt | 120 +++++++++ .../stslex/wizard/core/ui/mvi/CommonEvents.kt | 53 ++++ .../com/stslex/wizard/core/ui/mvi/Router.kt | 2 +- .../com/stslex/wizard/core/ui/mvi/Store.kt | 137 ++--------- .../wizard/core/ui/mvi/StoreAbstraction.kt | 24 -- .../stslex/wizard/core/ui/mvi/StoreBean.kt | 12 +- .../wizard/core/ui/mvi/StoreComponent.kt | 64 ----- .../com/stslex/wizard/core/ui/mvi/StoreExt.kt | 7 +- .../feature/auth/di/ModuleFeatureAuth.kt | 3 +- .../feature/auth/navigation/AuthGraph.kt | 2 +- .../feature/auth/navigation/AuthRouter.kt | 5 +- .../feature/auth/navigation/AuthRouterImpl.kt | 2 +- .../wizard/feature/auth/ui/AuthScreen.kt | 4 +- .../auth/ui/components/AuthSubmitButton.kt | 2 +- .../feature/auth/ui/components/AuthTitle.kt | 2 +- .../auth/ui/model/screen/AuthScreenState.kt | 8 +- .../screen/text_field/LoginTextFieldState.kt | 2 +- .../text_field/PasswordInputTextFieldState.kt | 4 +- .../PasswordSubmitTextFieldState.kt | 2 +- .../text_field/UsernameTextFieldState.kt | 2 +- .../wizard/feature/auth/ui/store/AuthStore.kt | 230 ++++++------------ .../auth/ui/store/AuthStoreComponent.kt | 95 -------- .../feature/auth/ui/store/AuthStoreImpl.kt | 171 +++++++++++++ .../favourite/di/ModuleFeatureFavourite.kt | 3 +- .../favourite/navigation/FavouriteGraph.kt | 2 +- .../favourite/navigation/FavouriteRouter.kt | 4 +- .../navigation/FavouriteRouterImpl.kt | 5 +- .../feature/favourite/ui/FavouriteScreen.kt | 6 +- .../favourite/ui/store/FavouriteStore.kt | 206 ++++------------ .../ui/store/FavouriteStoreComponent.kt | 71 ------ .../favourite/ui/store/FavouriteStoreImpl.kt | 171 +++++++++++++ .../feature/film/di/ModuleFeatureFilm.kt | 3 +- .../feature/film/navigation/FilmRouter.kt | 4 +- .../feature/film/navigation/FilmRouterImpl.kt | 8 +- .../feature/film/navigation/GraphFilm.kt | 2 +- .../wizard/feature/film/ui/FilmScreen.kt | 4 +- .../wizard/feature/film/ui/store/FilmStore.kt | 102 +++----- .../film/ui/store/FilmStoreComponent.kt | 42 ---- .../feature/film/ui/store/FilmStoreImpl.kt | 84 +++++++ .../feature/film_feed/di/ModuleFeatureFeed.kt | 3 +- .../film_feed/navigation/FeedScreenRouter.kt | 2 +- .../navigation/FeedScreenRouterImpl.kt | 6 +- .../film_feed/navigation/GraphFilmFeed.kt | 4 +- .../wizard/feature/film_feed/ui/FeedScreen.kt | 4 +- .../feature/film_feed/ui/store/FeedStore.kt | 124 +++------- .../film_feed/ui/store/FeedStoreComponent.kt | 50 ---- .../film_feed/ui/store/FeedStoreImpl.kt | 102 ++++++++ .../follower/di/ModuleFeatureFollower.kt | 3 +- .../follower/navigation/FollowerRouter.kt | 2 +- .../follower/navigation/FollowerRouterImpl.kt | 4 +- .../follower/navigation/GraphFollower.kt | 2 +- .../feature/follower/ui/FollowerScreen.kt | 4 +- .../follower/ui/store/FollowerStore.kt | 183 ++++---------- .../ui/store/FollowerStoreComponent.kt | 67 ----- .../follower/ui/store/FollowerStoreImpl.kt | 150 ++++++++++++ .../feature/match/di/ModuleFeatureMatch.kt | 3 +- .../feature/match/navigation/GraphMatch.kt | 4 +- .../feature/match/navigation/MatchRouter.kt | 4 +- .../match/navigation/MatchRouterImpl.kt | 2 +- .../wizard/feature/match/ui/MatchScreen.kt | 4 +- .../match/ui/components/MatchScreenContent.kt | 2 +- .../feature/match/ui/store/MatchStore.kt | 210 +++++----------- .../match/ui/store/MatchStoreComponent.kt | 77 ------ .../feature/match/ui/store/MatchStoreImpl.kt | 176 ++++++++++++++ .../match_feed/di/ModuleFeatureMatchFeed.kt | 3 +- .../match_feed/navigation/GraphMatchFeed.kt | 4 +- .../match_feed/navigation/MatchFeedRouter.kt | 5 +- .../navigation/MatchFeedRouterImpl.kt | 2 +- .../feature/match_feed/ui/MatchFeedScreen.kt | 4 +- .../match_feed/ui/store/MatchFeedStore.kt | 162 ++++-------- .../ui/store/MatchFeedStoreComponent.kt | 61 ----- .../match_feed/ui/store/MatchFeedStoreImpl.kt | 130 ++++++++++ .../profile/di/ModuleFeatureProfile.kt | 3 +- .../profile/navigation/GraphProfile.kt | 8 +- .../profile/navigation/ProfileRouter.kt | 4 +- .../profile/navigation/ProfileRouterImpl.kt | 2 +- .../feature/profile/ui/ProfileScreen.kt | 4 +- .../ui/components/ProfileScreenContent.kt | 2 +- .../feature/profile/ui/store/ProfileStore.kt | 181 +++++--------- .../profile/ui/store/ProfileStoreComponent.kt | 78 ------ .../profile/ui/store/ProfileStoreImpl.kt | 140 +++++++++++ .../settings/di/ModuleFeatureSettings.kt | 4 +- .../settings/navigation/GraphSettings.kt | 6 +- .../settings/navigation/SettingsRouter.kt | 4 +- .../settings/navigation/SettingsRouterImpl.kt | 2 +- .../feature/settings/ui/SettingsScreen.kt | 4 +- .../settings/ui/store/SettingsStore.kt | 82 +++---- .../ui/store/SettingsStoreComponent.kt | 40 --- .../settings/ui/store/SettingsStoreImpl.kt | 60 +++++ 89 files changed, 1935 insertions(+), 1922 deletions(-) create mode 100644 core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/BaseStore.kt create mode 100644 core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/CommonEvents.kt delete mode 100644 core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/StoreAbstraction.kt delete mode 100644 core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/StoreComponent.kt delete mode 100644 feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/store/AuthStoreComponent.kt create mode 100644 feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/store/AuthStoreImpl.kt delete mode 100644 feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/ui/store/FavouriteStoreComponent.kt create mode 100644 feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/ui/store/FavouriteStoreImpl.kt delete mode 100644 feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/ui/store/FilmStoreComponent.kt create mode 100644 feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/ui/store/FilmStoreImpl.kt delete mode 100644 feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/ui/store/FeedStoreComponent.kt create mode 100644 feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/ui/store/FeedStoreImpl.kt delete mode 100644 feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/ui/store/FollowerStoreComponent.kt create mode 100644 feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/ui/store/FollowerStoreImpl.kt delete mode 100644 feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/ui/store/MatchStoreComponent.kt create mode 100644 feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/ui/store/MatchStoreImpl.kt delete mode 100644 feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/ui/store/MatchFeedStoreComponent.kt create mode 100644 feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/ui/store/MatchFeedStoreImpl.kt delete mode 100644 feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/ui/store/ProfileStoreComponent.kt create mode 100644 feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/ui/store/ProfileStoreImpl.kt delete mode 100644 feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/ui/store/SettingsStoreComponent.kt create mode 100644 feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/ui/store/SettingsStoreImpl.kt diff --git a/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/BaseStore.kt b/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/BaseStore.kt new file mode 100644 index 0000000..fecb86f --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/BaseStore.kt @@ -0,0 +1,120 @@ +package com.stslex.wizard.core.ui.mvi + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.stslex.wizard.core.core.AppDispatcher +import com.stslex.wizard.core.core.Logger +import com.stslex.wizard.core.core.coroutine.AppCoroutineScope +import com.stslex.wizard.core.core.coroutine.AppCoroutineScopeImpl +import com.stslex.wizard.core.core.coroutineExceptionHandler +import com.stslex.wizard.core.ui.mvi.Store.Action +import com.stslex.wizard.core.ui.mvi.Store.Event +import com.stslex.wizard.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.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 + +abstract class BaseStore( + private val appDispatcher: AppDispatcher, + initialState: S +) : ViewModel(), Store { + + private val _event: MutableSharedFlow = MutableSharedFlow() + override val event: SharedFlow = _event.asSharedFlow() + + private val _state: MutableStateFlow = MutableStateFlow(initialState) + override val state: StateFlow = _state.asStateFlow() + + protected val scope: AppCoroutineScope = AppCoroutineScopeImpl( + scope = viewModelScope, + appDispatcher = appDispatcher + ) + + private var _lastAction: A? = null + protected val lastAction: A? + get() = _lastAction + + override fun sendAction(action: A) { + if (lastAction != action && action !is Action.RepeatLast) { + _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.e(throwable) + viewModelScope.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) { + _state.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) { + viewModelScope.launch(appDispatcher.default) { + this@BaseStore._event.emit(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/wizard/core/ui/mvi/CommonEvents.kt b/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/CommonEvents.kt new file mode 100644 index 0000000..2be89c4 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/CommonEvents.kt @@ -0,0 +1,53 @@ +package com.stslex.wizard.core.ui.mvi + +import androidx.compose.material3.SnackbarDuration +import androidx.compose.runtime.Stable +import com.stslex.wizard.core.ui.components.SnackbarType + +interface CommonEvents { + + @Stable + sealed class Snackbar( + open val message: String, + open val duration: SnackbarDuration, + open val withDismissAction: Boolean, + val action: String, + ) : CommonEvents { + + @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 + ) + } +} \ No newline at end of file diff --git a/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/Router.kt b/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/Router.kt index 0e82b6a..0b735c3 100644 --- a/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/Router.kt +++ b/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/Router.kt @@ -1,5 +1,5 @@ package com.stslex.wizard.core.ui.mvi -fun interface Router { +fun interface Router { operator fun invoke(event: E) } diff --git a/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/Store.kt b/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/Store.kt index 2702825..1496c78 100644 --- a/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/Store.kt +++ b/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/Store.kt @@ -1,131 +1,36 @@ package com.stslex.wizard.core.ui.mvi -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.stslex.wizard.core.core.AppDispatcher -import com.stslex.wizard.core.core.Logger -import com.stslex.wizard.core.core.coroutine.AppCoroutineScope -import com.stslex.wizard.core.core.coroutine.AppCoroutineScopeImpl -import com.stslex.wizard.core.core.coroutineExceptionHandler -import com.stslex.wizard.core.ui.mvi.StoreComponent.Action -import com.stslex.wizard.core.ui.mvi.StoreComponent.Event -import com.stslex.wizard.core.ui.mvi.StoreComponent.Navigation -import com.stslex.wizard.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 com.stslex.wizard.core.ui.mvi.Store.Action +import com.stslex.wizard.core.ui.mvi.Store.Event +import com.stslex.wizard.core.ui.mvi.Store.State 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 -abstract class Store( - private val router: Router, - private val appDispatcher: AppDispatcher, - initialState: S -) : ViewModel(), StoreAbstraction { +interface Store { - private val _event: MutableSharedFlow = MutableSharedFlow() - override val event: SharedFlow = _event.asSharedFlow() + /** Flow of the state of the screen. */ + val state: StateFlow - private val _state: MutableStateFlow = MutableStateFlow(initialState) - override val state: StateFlow = _state.asStateFlow() + /** Flow of events that are sent to the screen. */ + val event: SharedFlow - protected val scope: AppCoroutineScope = AppCoroutineScopeImpl( - scope = viewModelScope, - 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 + */ + fun sendAction(action: A) - private var _lastAction: A? = null - protected val lastAction: A? - get() = _lastAction + interface State - override fun sendAction(action: A) { - if (lastAction != action && action !is Action.RepeatLastAction) { - _lastAction = action - } - process(action) - } + interface Event - /** Process the action. This method should be overridden in the child class.*/ - protected abstract fun process(action: A) + interface Action { - private fun exceptionHandler( - onError: suspend (cause: Throwable) -> Unit = {}, - ) = CoroutineExceptionHandler { _, throwable -> - Logger.e(throwable) - viewModelScope.launch(appDispatcher.default + coroutineExceptionHandler) { - onError(throwable) - } - } + interface RepeatLast : Action - /** - * Updates the state of the screen. - * @param update - function that updates the state - * */ - protected fun updateState(update: (S) -> S) { - _state.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) { - viewModelScope.launch(appDispatcher.default) { - this@Store._event.emit(event) - } + interface Navigation : 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/wizard/core/ui/mvi/StoreAbstraction.kt b/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/StoreAbstraction.kt deleted file mode 100644 index 45f85cb..0000000 --- a/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/StoreAbstraction.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.stslex.wizard.core.ui.mvi - -import com.stslex.wizard.core.ui.mvi.StoreComponent.Action -import com.stslex.wizard.core.ui.mvi.StoreComponent.Event -import com.stslex.wizard.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/wizard/core/ui/mvi/StoreBean.kt b/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/StoreBean.kt index 512c78c..a0dd897 100644 --- a/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/StoreBean.kt +++ b/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/StoreBean.kt @@ -5,32 +5,32 @@ import org.koin.core.definition.BeanDefinition import org.koin.core.definition.KoinDefinition import org.koin.core.module.Module -inline fun > Module.storeOf( +inline fun > Module.storeOf( crossinline constructor: () -> R, noinline options: (BeanDefinition.() -> Unit)? = null, ): KoinDefinition = viewModelOf(constructor, options) -inline fun , reified T1> Module.storeOf( +inline fun , reified T1> Module.storeOf( crossinline constructor: (T1) -> R, noinline options: (BeanDefinition.() -> Unit)? = null, ): KoinDefinition = viewModelOf(constructor, options) -inline fun , reified T1, reified T2> Module.storeOf( +inline fun , reified T1, reified T2> Module.storeOf( crossinline constructor: (T1, T2) -> R, noinline options: (BeanDefinition.() -> Unit)? = null, ): KoinDefinition = viewModelOf(constructor, options) -inline fun , reified T1, reified T2, reified T3> Module.storeOf( +inline fun , reified T1, reified T2, reified T3> Module.storeOf( crossinline constructor: (T1, T2, T3) -> R, noinline options: (BeanDefinition.() -> Unit)? = null, ): KoinDefinition = viewModelOf(constructor, options) -inline fun , reified T1, reified T2, reified T3, reified T4> Module.storeOf( +inline fun , reified T1, reified T2, reified T3, reified T4> Module.storeOf( crossinline constructor: (T1, T2, T3, T4) -> R, noinline options: (BeanDefinition.() -> Unit)? = null, ): KoinDefinition = viewModelOf(constructor, options) -inline fun , reified T1, reified T2, reified T3, reified T4, reified T5> Module.storeOf( +inline fun , reified T1, reified T2, reified T3, reified T4, reified T5> Module.storeOf( crossinline constructor: (T1, T2, T3, T4, T5) -> R, noinline options: (BeanDefinition.() -> Unit)? = null, ): KoinDefinition = viewModelOf(constructor, options) \ No newline at end of file diff --git a/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/StoreComponent.kt b/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/StoreComponent.kt deleted file mode 100644 index 354c564..0000000 --- a/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/StoreComponent.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.stslex.wizard.core.ui.mvi - -import androidx.compose.material3.SnackbarDuration -import androidx.compose.runtime.Stable -import com.stslex.wizard.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/wizard/core/ui/mvi/StoreExt.kt b/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/StoreExt.kt index b42faa7..d568358 100644 --- a/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/StoreExt.kt +++ b/core/ui/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/StoreExt.kt @@ -14,13 +14,8 @@ expect inline fun Module.viewModelDefinition( noinline definition: Definition ): KoinDefinition -inline fun > Module.storeDefinition( - qualifier: Qualifier? = null, - noinline definition: Definition -): KoinDefinition = viewModelDefinition(qualifier, definition) - @Composable -inline fun > getStore( +inline fun > getStore( qualifier: Qualifier? = null, noinline parameters: ParametersDefinition? = null ): T = koinInject( diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/di/ModuleFeatureAuth.kt b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/di/ModuleFeatureAuth.kt index eef2ec6..c84a3e8 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/di/ModuleFeatureAuth.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/di/ModuleFeatureAuth.kt @@ -9,6 +9,7 @@ import com.stslex.wizard.feature.auth.domain.AuthInteractorImpl import com.stslex.wizard.feature.auth.navigation.AuthRouter import com.stslex.wizard.feature.auth.navigation.AuthRouterImpl import com.stslex.wizard.feature.auth.ui.store.AuthStore +import com.stslex.wizard.feature.auth.ui.store.AuthStoreImpl import org.koin.core.annotation.Module import org.koin.core.module.dsl.bind import org.koin.core.module.dsl.factoryOf @@ -21,6 +22,6 @@ class ModuleFeatureAuth : AppModule() { factoryOf(::AuthRepositoryImpl) { bind() } factoryOf(::AuthInteractorImpl) { bind() } factoryOf(::AuthRouterImpl) { bind() } - storeOf(::AuthStore) + storeOf(::AuthStoreImpl) { bind() } } } diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/navigation/AuthGraph.kt b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/navigation/AuthGraph.kt index cf7dfe3..9eaa870 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/navigation/AuthGraph.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/navigation/AuthGraph.kt @@ -12,7 +12,7 @@ import com.stslex.wizard.core.ui.mvi.getStore import com.stslex.wizard.feature.auth.ui.AuthScreen import com.stslex.wizard.feature.auth.ui.model.screen.rememberAuthScreenState import com.stslex.wizard.feature.auth.ui.store.AuthStore -import com.stslex.wizard.feature.auth.ui.store.AuthStoreComponent.Event +import com.stslex.wizard.feature.auth.ui.store.AuthStore.Event fun NavGraphBuilder.graphAuth() { navScreen { diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/navigation/AuthRouter.kt b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/navigation/AuthRouter.kt index 29ec6f9..db1d673 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/navigation/AuthRouter.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/navigation/AuthRouter.kt @@ -1,7 +1,6 @@ package com.stslex.wizard.feature.auth.navigation import com.stslex.wizard.core.ui.mvi.Router -import com.stslex.wizard.feature.auth.ui.store.AuthStoreComponent.Navigation - -interface AuthRouter : Router +import com.stslex.wizard.feature.auth.ui.store.AuthStore +interface AuthRouter : Router \ No newline at end of file diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/navigation/AuthRouterImpl.kt b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/navigation/AuthRouterImpl.kt index e820b6e..f4a68fd 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/navigation/AuthRouterImpl.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/navigation/AuthRouterImpl.kt @@ -2,7 +2,7 @@ package com.stslex.wizard.feature.auth.navigation import com.stslex.wizard.core.navigation.Screen import com.stslex.wizard.core.navigation.navigator.Navigator -import com.stslex.wizard.feature.auth.ui.store.AuthStoreComponent.Navigation +import com.stslex.wizard.feature.auth.ui.store.AuthStore.Action.Navigation class AuthRouterImpl(private val navigator: Navigator) : AuthRouter { diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/AuthScreen.kt b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/AuthScreen.kt index 8af155d..b556f3a 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/AuthScreen.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/AuthScreen.kt @@ -20,8 +20,8 @@ import com.stslex.wizard.core.ui.theme.toPx import com.stslex.wizard.feature.auth.ui.components.AuthFieldsColumn import com.stslex.wizard.feature.auth.ui.components.AuthTitle import com.stslex.wizard.feature.auth.ui.model.screen.AuthScreenState -import com.stslex.wizard.feature.auth.ui.store.AuthStoreComponent.AuthFieldsState -import com.stslex.wizard.feature.auth.ui.store.AuthStoreComponent.ScreenLoadingState +import com.stslex.wizard.feature.auth.ui.store.AuthStore.AuthFieldsState +import com.stslex.wizard.feature.auth.ui.store.AuthStore.ScreenLoadingState @OptIn(ExperimentalMaterialApi::class) @Composable diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/components/AuthSubmitButton.kt b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/components/AuthSubmitButton.kt index 196d159..8974d2b 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/components/AuthSubmitButton.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/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.wizard.feature.auth.ui.store.AuthStoreComponent.AuthFieldsState +import com.stslex.wizard.feature.auth.ui.store.AuthStore.AuthFieldsState @Composable internal fun AuthSubmitButton( diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/components/AuthTitle.kt b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/components/AuthTitle.kt index e5f90d7..ec6cef9 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/components/AuthTitle.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/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.wizard.core.ui.theme.AppDimension -import com.stslex.wizard.feature.auth.ui.store.AuthStoreComponent.AuthFieldsState +import com.stslex.wizard.feature.auth.ui.store.AuthStore.AuthFieldsState import kotlinx.coroutines.launch @OptIn(ExperimentalMaterialApi::class) diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/model/screen/AuthScreenState.kt b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/model/screen/AuthScreenState.kt index c64b339..98915ec 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/model/screen/AuthScreenState.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/model/screen/AuthScreenState.kt @@ -23,10 +23,10 @@ import com.stslex.wizard.feature.auth.ui.model.screen.text_field.rememberLoginTe import com.stslex.wizard.feature.auth.ui.model.screen.text_field.rememberPasswordInputTextFieldState import com.stslex.wizard.feature.auth.ui.model.screen.text_field.rememberPasswordSubmitTextFieldState import com.stslex.wizard.feature.auth.ui.model.screen.text_field.rememberUsernameTextFieldState -import com.stslex.wizard.feature.auth.ui.store.AuthStoreComponent.Action -import com.stslex.wizard.feature.auth.ui.store.AuthStoreComponent.AuthFieldsState -import com.stslex.wizard.feature.auth.ui.store.AuthStoreComponent.ScreenLoadingState -import com.stslex.wizard.feature.auth.ui.store.AuthStoreComponent.State +import com.stslex.wizard.feature.auth.ui.store.AuthStore.Action +import com.stslex.wizard.feature.auth.ui.store.AuthStore.AuthFieldsState +import com.stslex.wizard.feature.auth.ui.store.AuthStore.ScreenLoadingState +import com.stslex.wizard.feature.auth.ui.store.AuthStore.State @Stable data class AuthScreenState @OptIn(ExperimentalMaterialApi::class) constructor( diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/model/screen/text_field/LoginTextFieldState.kt b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/model/screen/text_field/LoginTextFieldState.kt index 2da76db..c5dbdb4 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/model/screen/text_field/LoginTextFieldState.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/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.wizard.feature.auth.ui.model.screen.text_field.base.AuthTextField -import com.stslex.wizard.feature.auth.ui.store.AuthStoreComponent.Action.InputAction +import com.stslex.wizard.feature.auth.ui.store.AuthStore.Action.InputAction @Stable data class LoginTextFieldState( diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/model/screen/text_field/PasswordInputTextFieldState.kt b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/model/screen/text_field/PasswordInputTextFieldState.kt index 6505760..3eb116a 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/model/screen/text_field/PasswordInputTextFieldState.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/model/screen/text_field/PasswordInputTextFieldState.kt @@ -7,8 +7,8 @@ import androidx.compose.ui.hapticfeedback.HapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback import com.stslex.wizard.feature.auth.ui.model.screen.text_field.base.PasswordTextFieldState import com.stslex.wizard.feature.auth.ui.model.screen.text_field.hidden.HiddenState -import com.stslex.wizard.feature.auth.ui.store.AuthStoreComponent.Action.InputAction -import com.stslex.wizard.feature.auth.ui.store.AuthStoreComponent.AuthFieldsState +import com.stslex.wizard.feature.auth.ui.store.AuthStore.Action.InputAction +import com.stslex.wizard.feature.auth.ui.store.AuthStore.AuthFieldsState @Stable data class PasswordInputTextFieldState( diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/model/screen/text_field/PasswordSubmitTextFieldState.kt b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/model/screen/text_field/PasswordSubmitTextFieldState.kt index 196d8a6..2fdec83 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/model/screen/text_field/PasswordSubmitTextFieldState.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/model/screen/text_field/PasswordSubmitTextFieldState.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.hapticfeedback.HapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback import com.stslex.wizard.feature.auth.ui.model.screen.text_field.base.PasswordTextFieldState import com.stslex.wizard.feature.auth.ui.model.screen.text_field.hidden.HiddenState -import com.stslex.wizard.feature.auth.ui.store.AuthStoreComponent.Action.InputAction.PasswordSubmitInput +import com.stslex.wizard.feature.auth.ui.store.AuthStore.Action.InputAction.PasswordSubmitInput @Stable data class PasswordSubmitTextFieldState( diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/model/screen/text_field/UsernameTextFieldState.kt b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/model/screen/text_field/UsernameTextFieldState.kt index 2737114..81e702b 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/model/screen/text_field/UsernameTextFieldState.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/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.wizard.feature.auth.ui.model.screen.text_field.base.AuthTextField -import com.stslex.wizard.feature.auth.ui.store.AuthStoreComponent.Action.InputAction +import com.stslex.wizard.feature.auth.ui.store.AuthStore.Action.InputAction @Stable data class UsernameTextFieldState( diff --git a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/store/AuthStore.kt b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/store/AuthStore.kt index 21359c7..74c8a1b 100644 --- a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/store/AuthStore.kt +++ b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/store/AuthStore.kt @@ -1,172 +1,100 @@ package com.stslex.wizard.feature.auth.ui.store -import com.stslex.wizard.core.core.AppDispatcher +import com.stslex.wizard.core.ui.mvi.CommonEvents import com.stslex.wizard.core.ui.mvi.Store -import com.stslex.wizard.core.ui.mvi.StoreComponent.Event.Snackbar -import com.stslex.wizard.feature.auth.domain.AuthInteractor -import com.stslex.wizard.feature.auth.navigation.AuthRouter -import com.stslex.wizard.feature.auth.ui.store.AuthStoreComponent.Action -import com.stslex.wizard.feature.auth.ui.store.AuthStoreComponent.AuthFieldsState -import com.stslex.wizard.feature.auth.ui.store.AuthStoreComponent.Event -import com.stslex.wizard.feature.auth.ui.store.AuthStoreComponent.Navigation -import com.stslex.wizard.feature.auth.ui.store.AuthStoreComponent.ScreenLoadingState -import com.stslex.wizard.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) - } - } +import com.stslex.wizard.feature.auth.ui.store.AuthStore.Action +import com.stslex.wizard.feature.auth.ui.store.AuthStore.Event +import com.stslex.wizard.feature.auth.ui.store.AuthStore.State - private fun processLoginInput(action: Action.InputAction.LoginInput) { - updateState { currentValue -> - currentValue.copy( - login = action.value - ) - } - } +interface AuthStore : Store { - private fun processUsernameInput(action: Action.InputAction.UsernameInput) { - updateState { currentValue -> - currentValue.copy( - username = action.value + 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 processPasswordInput(action: Action.InputAction.PasswordInput) { - updateState { currentValue -> - currentValue.copy( - password = action.value - ) - } - } + sealed interface Event : Store.Event { - private fun processPasswordSubmitInput(action: Action.InputAction.PasswordSubmitInput) { - updateState { currentValue -> - currentValue.copy( - passwordSubmit = action.value, - ) - } + data class ShowSnackbar( + val snackbar: CommonEvents.Snackbar + ) : Event } - private fun processAuthFieldChange(action: Action.OnAuthFieldChange) { - updateState { currentValue -> - currentValue.copy( - authFieldsState = action.targetState - ) + 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 processSubmitClicked(action: Action.OnSubmitClicked) { - when (action.state) { - AuthFieldsState.AUTH -> auth() - AuthFieldsState.REGISTER -> register() + sealed interface Navigation : Action, Store.Action.Navigation { + + data object HomeFeature : Navigation } } - 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) - }) + // 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 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) - }) - } + sealed interface ScreenLoadingState { - private fun setLoadingState(screenLoadingState: ScreenLoadingState) { - updateState { state -> - state.copy( - screenLoadingState = 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/wizard/feature/auth/ui/store/AuthStoreComponent.kt b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/store/AuthStoreComponent.kt deleted file mode 100644 index 2e24648..0000000 --- a/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/store/AuthStoreComponent.kt +++ /dev/null @@ -1,95 +0,0 @@ -package com.stslex.wizard.feature.auth.ui.store - -import com.stslex.wizard.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/wizard/feature/auth/ui/store/AuthStoreImpl.kt b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/store/AuthStoreImpl.kt new file mode 100644 index 0000000..8d38d1a --- /dev/null +++ b/feature/auth/src/commonMain/kotlin/com/stslex/wizard/feature/auth/ui/store/AuthStoreImpl.kt @@ -0,0 +1,171 @@ +package com.stslex.wizard.feature.auth.ui.store + +import com.stslex.wizard.core.core.AppDispatcher +import com.stslex.wizard.core.ui.mvi.BaseStore +import com.stslex.wizard.core.ui.mvi.CommonEvents.Snackbar +import com.stslex.wizard.feature.auth.domain.AuthInteractor +import com.stslex.wizard.feature.auth.navigation.AuthRouter +import com.stslex.wizard.feature.auth.ui.store.AuthStore.Action +import com.stslex.wizard.feature.auth.ui.store.AuthStore.AuthFieldsState +import com.stslex.wizard.feature.auth.ui.store.AuthStore.Event +import com.stslex.wizard.feature.auth.ui.store.AuthStore.ScreenLoadingState +import com.stslex.wizard.feature.auth.ui.store.AuthStore.State +import kotlinx.coroutines.delay + +class AuthStoreImpl( + private val interactor: AuthInteractor, + dispatcher: AppDispatcher, + private val router: AuthRouter +) : BaseStore( + appDispatcher = dispatcher, + initialState = State.INITIAL, +), AuthStore { + + 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) + is Action.Navigation -> router(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 + ) + } + sendAction(Action.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 + ) + } + process(Action.Navigation.HomeFeature) + }) + } + + private fun setLoadingState(screenLoadingState: ScreenLoadingState) { + updateState { state -> + state.copy( + screenLoadingState = screenLoadingState + ) + } + } +} + diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/di/ModuleFeatureFavourite.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/di/ModuleFeatureFavourite.kt index 68cbf8d..e3189d1 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/di/ModuleFeatureFavourite.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/di/ModuleFeatureFavourite.kt @@ -9,6 +9,7 @@ import com.stslex.wizard.feature.favourite.domain.interactor.FavouriteInteractor import com.stslex.wizard.feature.favourite.navigation.FavouriteRouter import com.stslex.wizard.feature.favourite.navigation.FavouriteRouterImpl import com.stslex.wizard.feature.favourite.ui.store.FavouriteStore +import com.stslex.wizard.feature.favourite.ui.store.FavouriteStoreImpl import org.koin.core.annotation.Module import org.koin.core.module.dsl.bind import org.koin.core.module.dsl.factoryOf @@ -21,6 +22,6 @@ class ModuleFeatureFavourite : AppModule() { factoryOf(::FavouriteRepositoryImpl) { bind() } factoryOf(::FavouriteInteractorImpl) { bind() } factoryOf(::FavouriteRouterImpl) { bind() } - storeOf(::FavouriteStore) + storeOf(::FavouriteStoreImpl) { bind() } } } diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/navigation/FavouriteGraph.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/navigation/FavouriteGraph.kt index 4b24f96..7289879 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/navigation/FavouriteGraph.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/navigation/FavouriteGraph.kt @@ -10,7 +10,7 @@ import com.stslex.wizard.core.navigation.navScreen import com.stslex.wizard.core.ui.mvi.getStore import com.stslex.wizard.feature.favourite.ui.FavouriteScreen import com.stslex.wizard.feature.favourite.ui.store.FavouriteStore -import com.stslex.wizard.feature.favourite.ui.store.FavouriteStoreComponent.Action +import com.stslex.wizard.feature.favourite.ui.store.FavouriteStore.Action fun NavGraphBuilder.graphFavourite() { navScreen { screen -> diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/navigation/FavouriteRouter.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/navigation/FavouriteRouter.kt index baebe47..812022b 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/navigation/FavouriteRouter.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/navigation/FavouriteRouter.kt @@ -1,6 +1,6 @@ package com.stslex.wizard.feature.favourite.navigation import com.stslex.wizard.core.ui.mvi.Router -import com.stslex.wizard.feature.favourite.ui.store.FavouriteStoreComponent.Navigation +import com.stslex.wizard.feature.favourite.ui.store.FavouriteStore -interface FavouriteRouter : Router +interface FavouriteRouter : Router diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/navigation/FavouriteRouterImpl.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/navigation/FavouriteRouterImpl.kt index cfd7e23..e8e93b5 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/navigation/FavouriteRouterImpl.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/navigation/FavouriteRouterImpl.kt @@ -2,14 +2,13 @@ package com.stslex.wizard.feature.favourite.navigation import com.stslex.wizard.core.navigation.Screen import com.stslex.wizard.core.navigation.navigator.Navigator -import com.stslex.wizard.feature.favourite.ui.store.FavouriteStoreComponent.Navigation -import com.stslex.wizard.feature.favourite.ui.store.FavouriteStoreComponent.Navigation.OpenFilm +import com.stslex.wizard.feature.favourite.ui.store.FavouriteStore.Action.Navigation class FavouriteRouterImpl(private val navigator: Navigator) : FavouriteRouter { override fun invoke(event: Navigation) { when (event) { - is OpenFilm -> navigator.navTo(Screen.Film(id = event.uuid)) + is Navigation.OpenFilm -> navigator.navTo(Screen.Film(id = event.uuid)) } } } \ No newline at end of file diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/ui/FavouriteScreen.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/ui/FavouriteScreen.kt index 2e169f4..6cb67ee 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/ui/FavouriteScreen.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/ui/FavouriteScreen.kt @@ -9,12 +9,12 @@ import com.stslex.wizard.feature.favourite.ui.components.content.FavouriteScreen import com.stslex.wizard.feature.favourite.ui.components.error.FavouriteScreenError import com.stslex.wizard.feature.favourite.ui.components.shimmer.FavouriteScreenShimmer import com.stslex.wizard.feature.favourite.ui.store.FavouriteScreenState -import com.stslex.wizard.feature.favourite.ui.store.FavouriteStoreComponent -import com.stslex.wizard.feature.favourite.ui.store.FavouriteStoreComponent.Action +import com.stslex.wizard.feature.favourite.ui.store.FavouriteStore +import com.stslex.wizard.feature.favourite.ui.store.FavouriteStore.Action @Composable internal fun FavouriteScreen( - state: FavouriteStoreComponent.State, + state: FavouriteStore.State, onAction: (Action) -> Unit ) { Box( diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/ui/store/FavouriteStore.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/ui/store/FavouriteStore.kt index 9a88f9d..3d75f05 100644 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/ui/store/FavouriteStore.kt +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/ui/store/FavouriteStore.kt @@ -1,172 +1,74 @@ package com.stslex.wizard.feature.favourite.ui.store -import com.stslex.wizard.core.core.AppDispatcher -import com.stslex.wizard.core.ui.base.mapToAppError -import com.stslex.wizard.core.ui.base.paging.toUi +import androidx.compose.runtime.Stable +import com.stslex.wizard.core.ui.base.paging.PagingConfig +import com.stslex.wizard.core.ui.base.paging.PagingUiState +import com.stslex.wizard.core.ui.mvi.CommonEvents import com.stslex.wizard.core.ui.mvi.Store -import com.stslex.wizard.core.ui.mvi.StoreComponent.Event.Snackbar -import com.stslex.wizard.core.ui.pager.pager.StorePager -import com.stslex.wizard.core.ui.pager.pager.StorePagerFactory -import com.stslex.wizard.core.ui.pager.states.PagerLoadState -import com.stslex.wizard.feature.favourite.domain.interactor.FavouriteInteractor -import com.stslex.wizard.feature.favourite.navigation.FavouriteRouter import com.stslex.wizard.feature.favourite.ui.model.FavouriteModel -import com.stslex.wizard.feature.favourite.ui.model.toDomain -import com.stslex.wizard.feature.favourite.ui.model.toUI -import com.stslex.wizard.feature.favourite.ui.store.FavouriteStoreComponent.Action -import com.stslex.wizard.feature.favourite.ui.store.FavouriteStoreComponent.Event -import com.stslex.wizard.feature.favourite.ui.store.FavouriteStoreComponent.Navigation -import com.stslex.wizard.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 +import com.stslex.wizard.feature.favourite.ui.store.FavouriteStore.Action +import com.stslex.wizard.feature.favourite.ui.store.FavouriteStore.Event +import com.stslex.wizard.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 ) - }, - 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, - ) - } + @Stable + sealed interface Action : Store.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 class Init( + val uuid: String + ) : Action - updateState { currentState -> - currentState.copy(uuid = action.uuid) - } + @Stable + 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) - } - } - } + @Stable + data object Refresh : Action - private fun actionInputSearch(action: Action.InputSearch) { - updateState { state -> - state.copy(query = action.query) - } - } + @Stable + data object Retry : Action - private fun actionItemClick(action: Action.ItemClick) { - consumeNavigation(Navigation.OpenFilm(action.uuid)) - } + @Stable + data class LikeClick(val uuid: 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 + data class ItemClick(val uuid: String) : Action - private fun actionLoadMore() { - pager.load() - } + @Stable + data class InputSearch(val query: String) : Action - private fun actionRefresh() { - pager.refresh(isForceLoad = true) - } + sealed interface Navigation : Action, Store.Action.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) - ) - } - } + @Stable + sealed interface Event : Store.Event { + + @Stable + data class ShowSnackbar(val snackbar: CommonEvents.Snackbar) : Event } -} +} \ No newline at end of file diff --git a/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/ui/store/FavouriteStoreComponent.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/ui/store/FavouriteStoreComponent.kt deleted file mode 100644 index 970a6a7..0000000 --- a/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/ui/store/FavouriteStoreComponent.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.stslex.wizard.feature.favourite.ui.store - -import androidx.compose.runtime.Stable -import com.stslex.wizard.core.ui.base.paging.PagingConfig -import com.stslex.wizard.core.ui.base.paging.PagingUiState -import com.stslex.wizard.core.ui.mvi.StoreComponent -import com.stslex.wizard.core.ui.mvi.StoreComponent.Event.Snackbar -import com.stslex.wizard.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/wizard/feature/favourite/ui/store/FavouriteStoreImpl.kt b/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/ui/store/FavouriteStoreImpl.kt new file mode 100644 index 0000000..15606ca --- /dev/null +++ b/feature/favourite/src/commonMain/kotlin/com/stslex/wizard/feature/favourite/ui/store/FavouriteStoreImpl.kt @@ -0,0 +1,171 @@ +package com.stslex.wizard.feature.favourite.ui.store + +import com.stslex.wizard.core.core.AppDispatcher +import com.stslex.wizard.core.ui.base.mapToAppError +import com.stslex.wizard.core.ui.base.paging.toUi +import com.stslex.wizard.core.ui.mvi.BaseStore +import com.stslex.wizard.core.ui.mvi.CommonEvents.Snackbar +import com.stslex.wizard.core.ui.pager.pager.StorePager +import com.stslex.wizard.core.ui.pager.pager.StorePagerFactory +import com.stslex.wizard.core.ui.pager.states.PagerLoadState +import com.stslex.wizard.feature.favourite.domain.interactor.FavouriteInteractor +import com.stslex.wizard.feature.favourite.navigation.FavouriteRouter +import com.stslex.wizard.feature.favourite.ui.model.FavouriteModel +import com.stslex.wizard.feature.favourite.ui.model.toDomain +import com.stslex.wizard.feature.favourite.ui.model.toUI +import com.stslex.wizard.feature.favourite.ui.store.FavouriteStore.Action +import com.stslex.wizard.feature.favourite.ui.store.FavouriteStore.Event +import com.stslex.wizard.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, + private val router: FavouriteRouter, + appDispatcher: AppDispatcher, + pagingFactory: StorePagerFactory, +) : BaseStore( + 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() + is Action.Navigation -> router(action) + } + } + + 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) { + sendAction(Action.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/src/commonMain/kotlin/com/stslex/wizard/feature/film/di/ModuleFeatureFilm.kt b/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/di/ModuleFeatureFilm.kt index d1d1f48..c83d316 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/di/ModuleFeatureFilm.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/di/ModuleFeatureFilm.kt @@ -9,6 +9,7 @@ import com.stslex.wizard.feature.film.domain.interactor.FilmInteractorImpl import com.stslex.wizard.feature.film.navigation.FilmRouter import com.stslex.wizard.feature.film.navigation.FilmRouterImpl import com.stslex.wizard.feature.film.ui.store.FilmStore +import com.stslex.wizard.feature.film.ui.store.FilmStoreImpl import org.koin.core.annotation.Module import org.koin.core.module.dsl.bind import org.koin.core.module.dsl.factoryOf @@ -18,7 +19,7 @@ import org.koin.dsl.ModuleDeclaration class ModuleFeatureFilm : AppModule() { override fun declaration(): ModuleDeclaration = { - storeOf(::FilmStore) + storeOf(::FilmStoreImpl) { bind() } factoryOf(::FilmRouterImpl) { bind() } factoryOf(::FilmInteractorImpl) { bind() } factoryOf(::FilmRepositoryImpl) { bind() } diff --git a/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/navigation/FilmRouter.kt b/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/navigation/FilmRouter.kt index e2142b8..9ab014c 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/navigation/FilmRouter.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/navigation/FilmRouter.kt @@ -1,6 +1,6 @@ package com.stslex.wizard.feature.film.navigation import com.stslex.wizard.core.ui.mvi.Router -import com.stslex.wizard.feature.film.ui.store.FilmStoreComponent.Navigation +import com.stslex.wizard.feature.film.ui.store.FilmStore -interface FilmRouter : Router +interface FilmRouter : Router diff --git a/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/navigation/FilmRouterImpl.kt b/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/navigation/FilmRouterImpl.kt index d669578..c03d2cd 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/navigation/FilmRouterImpl.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/navigation/FilmRouterImpl.kt @@ -1,17 +1,15 @@ package com.stslex.wizard.feature.film.navigation import com.stslex.wizard.core.navigation.navigator.Navigator -import com.stslex.wizard.feature.film.ui.store.FilmStoreComponent +import com.stslex.wizard.feature.film.ui.store.FilmStore.Action.Navigation class FilmRouterImpl( private val navigator: Navigator ) : FilmRouter { - override fun invoke( - event: FilmStoreComponent.Navigation - ) { + override fun invoke(event: Navigation) { when (event) { - is FilmStoreComponent.Navigation.Back -> navigator.popBack() + is Navigation.Back -> navigator.popBack() } } } \ No newline at end of file diff --git a/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/navigation/GraphFilm.kt b/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/navigation/GraphFilm.kt index 087fce9..1a775d4 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/navigation/GraphFilm.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/navigation/GraphFilm.kt @@ -10,7 +10,7 @@ import com.stslex.wizard.core.navigation.navScreen import com.stslex.wizard.core.ui.mvi.getStore import com.stslex.wizard.feature.film.ui.FilmScreen import com.stslex.wizard.feature.film.ui.store.FilmStore -import com.stslex.wizard.feature.film.ui.store.FilmStoreComponent.Action +import com.stslex.wizard.feature.film.ui.store.FilmStore.Action fun NavGraphBuilder.graphFilm() { navScreen { screen -> diff --git a/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/ui/FilmScreen.kt b/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/ui/FilmScreen.kt index f341439..8e35e6c 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/ui/FilmScreen.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/ui/FilmScreen.kt @@ -10,8 +10,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.stslex.wizard.feature.film.ui.components.FilmContentScreen import com.stslex.wizard.feature.film.ui.store.FilmScreenState -import com.stslex.wizard.feature.film.ui.store.FilmStoreComponent.Action -import com.stslex.wizard.feature.film.ui.store.FilmStoreComponent.State +import com.stslex.wizard.feature.film.ui.store.FilmStore.Action +import com.stslex.wizard.feature.film.ui.store.FilmStore.State @Composable internal fun FilmScreen( diff --git a/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/ui/store/FilmStore.kt b/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/ui/store/FilmStore.kt index 4c7e4f5..57acd17 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/ui/store/FilmStore.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/ui/store/FilmStore.kt @@ -1,85 +1,45 @@ package com.stslex.wizard.feature.film.ui.store -import com.stslex.wizard.core.core.AppDispatcher +import androidx.compose.runtime.Stable import com.stslex.wizard.core.ui.mvi.Store -import com.stslex.wizard.feature.film.domain.interactor.FilmInteractor -import com.stslex.wizard.feature.film.navigation.FilmRouter -import com.stslex.wizard.feature.film.ui.model.toDomain -import com.stslex.wizard.feature.film.ui.model.toUi -import com.stslex.wizard.feature.film.ui.store.FilmStoreComponent.Action -import com.stslex.wizard.feature.film.ui.store.FilmStoreComponent.Event -import com.stslex.wizard.feature.film.ui.store.FilmStoreComponent.Navigation -import com.stslex.wizard.feature.film.ui.store.FilmStoreComponent.State -import kotlinx.coroutines.Job +import com.stslex.wizard.feature.film.ui.store.FilmStore.Action +import com.stslex.wizard.feature.film.ui.store.FilmStore.Event +import com.stslex.wizard.feature.film.ui.store.FilmStore.State -class FilmStore( - private val interactor: FilmInteractor, - appDispatcher: AppDispatcher, - router: FilmRouter, -) : Store( - router = router, - appDispatcher = appDispatcher, - initialState = State.INITIAL, -) { +interface FilmStore : Store { - private var likeJob: Job? = null + @Stable + data class State( + val filmId: String, + val screenState: FilmScreenState + ) : Store.State { - override fun process(action: Action) { - when (action) { - is Action.Init -> actionInit(action) - is Action.BackButtonClick -> actionBackButtonClick() - is Action.LikeButtonClick -> actionLikeButtonClick() + companion object { + val INITIAL = State( + filmId = "", + screenState = FilmScreenState.Loading + ) } } - 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()) - } + @Stable + sealed interface Action : Store.Action { + + @Stable + data class Init(val id: String) : Action + + data object BackButtonClick : Action + + data object LikeButtonClick : Action + + sealed interface Navigation : Action, Store.Action.Navigation { + + data object Back : Navigation } } - private fun actionBackButtonClick() { - consumeNavigation(Navigation.Back) - } + sealed interface Event : Store.Event { - 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()) - ) - } - } + data class ErrorSnackbar(val throwable: Throwable) : Event } -} +} \ No newline at end of file diff --git a/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/ui/store/FilmStoreComponent.kt b/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/ui/store/FilmStoreComponent.kt deleted file mode 100644 index 6110217..0000000 --- a/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/ui/store/FilmStoreComponent.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.stslex.wizard.feature.film.ui.store - -import androidx.compose.runtime.Stable -import com.stslex.wizard.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/wizard/feature/film/ui/store/FilmStoreImpl.kt b/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/ui/store/FilmStoreImpl.kt new file mode 100644 index 0000000..01292f6 --- /dev/null +++ b/feature/film/src/commonMain/kotlin/com/stslex/wizard/feature/film/ui/store/FilmStoreImpl.kt @@ -0,0 +1,84 @@ +package com.stslex.wizard.feature.film.ui.store + +import com.stslex.wizard.core.core.AppDispatcher +import com.stslex.wizard.core.ui.mvi.BaseStore +import com.stslex.wizard.feature.film.domain.interactor.FilmInteractor +import com.stslex.wizard.feature.film.navigation.FilmRouter +import com.stslex.wizard.feature.film.ui.model.toDomain +import com.stslex.wizard.feature.film.ui.model.toUi +import com.stslex.wizard.feature.film.ui.store.FilmStore.Action +import com.stslex.wizard.feature.film.ui.store.FilmStore.Event +import com.stslex.wizard.feature.film.ui.store.FilmStore.State +import kotlinx.coroutines.Job + +class FilmStoreImpl( + private val interactor: FilmInteractor, + appDispatcher: AppDispatcher, + private val router: FilmRouter, +) : BaseStore( + appDispatcher = appDispatcher, + initialState = State.INITIAL, +), FilmStore { + + 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() + is Action.Navigation -> router(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()) + } + } + } + + private fun actionBackButtonClick() { + sendAction(Action.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/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/di/ModuleFeatureFeed.kt b/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/di/ModuleFeatureFeed.kt index c7a1a6b..7985237 100644 --- a/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/di/ModuleFeatureFeed.kt +++ b/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/di/ModuleFeatureFeed.kt @@ -9,6 +9,7 @@ import com.stslex.wizard.feature.film_feed.domain.interactor.FeedInteractorImpl import com.stslex.wizard.feature.film_feed.navigation.FeedScreenRouter import com.stslex.wizard.feature.film_feed.navigation.FeedScreenRouterImpl import com.stslex.wizard.feature.film_feed.ui.store.FeedStore +import com.stslex.wizard.feature.film_feed.ui.store.FeedStoreImpl import org.koin.core.annotation.Module import org.koin.core.module.dsl.bind import org.koin.core.module.dsl.factoryOf @@ -18,7 +19,7 @@ import org.koin.dsl.ModuleDeclaration class ModuleFeatureFeed : AppModule() { override fun declaration(): ModuleDeclaration = { - storeOf(::FeedStore) + storeOf(::FeedStoreImpl) { bind() } factoryOf(::FeedScreenRouterImpl) { bind() } factoryOf(::FeedInteractorImpl) { bind() } factoryOf(::FeedRepositoryImpl) { bind() } diff --git a/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/navigation/FeedScreenRouter.kt b/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/navigation/FeedScreenRouter.kt index d86a769..b0c42f1 100644 --- a/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/navigation/FeedScreenRouter.kt +++ b/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/navigation/FeedScreenRouter.kt @@ -1,6 +1,6 @@ package com.stslex.wizard.feature.film_feed.navigation import com.stslex.wizard.core.ui.mvi.Router -import com.stslex.wizard.feature.film_feed.ui.store.FeedStoreComponent.Navigation +import com.stslex.wizard.feature.film_feed.ui.store.FeedStore.Action.Navigation interface FeedScreenRouter : Router diff --git a/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/navigation/FeedScreenRouterImpl.kt b/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/navigation/FeedScreenRouterImpl.kt index 22e4ce8..d83950f 100644 --- a/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/navigation/FeedScreenRouterImpl.kt +++ b/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/navigation/FeedScreenRouterImpl.kt @@ -2,15 +2,15 @@ package com.stslex.wizard.feature.film_feed.navigation import com.stslex.wizard.core.navigation.Screen import com.stslex.wizard.core.navigation.navigator.Navigator -import com.stslex.wizard.feature.film_feed.ui.store.FeedStoreComponent +import com.stslex.wizard.feature.film_feed.ui.store.FeedStore.Action.Navigation class FeedScreenRouterImpl( private val navigator: Navigator ) : FeedScreenRouter { - override fun invoke(event: FeedStoreComponent.Navigation) { + override fun invoke(event: Navigation) { when (event) { - is FeedStoreComponent.Navigation.Film -> navigator.navTo(Screen.Film(event.filmId)) + is Navigation.Film -> navigator.navTo(Screen.Film(event.filmId)) } } } \ No newline at end of file diff --git a/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/navigation/GraphFilmFeed.kt b/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/navigation/GraphFilmFeed.kt index e3abc87..78a3f42 100644 --- a/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/navigation/GraphFilmFeed.kt +++ b/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/navigation/GraphFilmFeed.kt @@ -10,8 +10,8 @@ import com.stslex.wizard.core.navigation.navScreen import com.stslex.wizard.core.ui.mvi.getStore import com.stslex.wizard.feature.film_feed.ui.FeedScreen import com.stslex.wizard.feature.film_feed.ui.store.FeedStore -import com.stslex.wizard.feature.film_feed.ui.store.FeedStoreComponent.Action -import com.stslex.wizard.feature.film_feed.ui.store.FeedStoreComponent.Event +import com.stslex.wizard.feature.film_feed.ui.store.FeedStore.Action +import com.stslex.wizard.feature.film_feed.ui.store.FeedStore.Event fun NavGraphBuilder.graphFilmFeed() { navScreen { screen -> diff --git a/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/ui/FeedScreen.kt b/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/ui/FeedScreen.kt index 60df62d..f70edcd 100644 --- a/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/ui/FeedScreen.kt +++ b/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/ui/FeedScreen.kt @@ -9,8 +9,8 @@ import com.stslex.wizard.feature.film_feed.ui.components.FeedScreenContent import com.stslex.wizard.feature.film_feed.ui.components.FeedScreenError import com.stslex.wizard.feature.film_feed.ui.components.FeedScreenLoading import com.stslex.wizard.feature.film_feed.ui.model.ScreenState -import com.stslex.wizard.feature.film_feed.ui.store.FeedStoreComponent.Action -import com.stslex.wizard.feature.film_feed.ui.store.FeedStoreComponent.State +import com.stslex.wizard.feature.film_feed.ui.store.FeedStore.Action +import com.stslex.wizard.feature.film_feed.ui.store.FeedStore.State @Composable internal fun FeedScreen( diff --git a/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/ui/store/FeedStore.kt b/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/ui/store/FeedStore.kt index 26d2fa1..1c1c975 100644 --- a/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/ui/store/FeedStore.kt +++ b/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/ui/store/FeedStore.kt @@ -1,103 +1,53 @@ package com.stslex.wizard.feature.film_feed.ui.store import androidx.compose.runtime.Stable -import com.stslex.wizard.core.core.AppDispatcher -import com.stslex.wizard.core.core.Logger import com.stslex.wizard.core.ui.mvi.Store -import com.stslex.wizard.feature.film_feed.domain.interactor.FeedInteractor -import com.stslex.wizard.feature.film_feed.navigation.FeedScreenRouter +import com.stslex.wizard.feature.film_feed.ui.model.FilmModel import com.stslex.wizard.feature.film_feed.ui.model.ScreenState -import com.stslex.wizard.feature.film_feed.ui.model.toUI -import com.stslex.wizard.feature.film_feed.ui.store.FeedStoreComponent.Action -import com.stslex.wizard.feature.film_feed.ui.store.FeedStoreComponent.Event -import com.stslex.wizard.feature.film_feed.ui.store.FeedStoreComponent.Navigation -import com.stslex.wizard.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) +import com.stslex.wizard.feature.film_feed.ui.store.FeedStore.Action +import com.stslex.wizard.feature.film_feed.ui.store.FeedStore.Event +import com.stslex.wizard.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 + ) } } - private fun actionFilmClick(action: Action.FilmClick) { - consumeNavigation(Navigation.Film(action.filmId)) + sealed interface Event : Store.Event { + + @Stable + data class ErrorSnackBar(val message: String) : Event } - private fun actionLoadFilms() { - if (loadingJob?.isActive == true) { - Logger.d("Loading job is active") - return - } - val hasNextPage = state.value.hasNextPage - if (hasNextPage.not()) { - Logger.d("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 - } + @Stable + sealed interface Action : Store.Action { - updateState { currentState -> - currentState.copy( - screen = loadScreenState - ) - } + data object LoadFilms : 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 + data class FilmClick(val filmId: String) : Action - companion object { + @Stable + sealed interface Navigation : Action, Store.Action.Navigation { - private const val PAGE_SIZE = 15 + data class Film(val filmId: String) : Navigation + } } } \ No newline at end of file diff --git a/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/ui/store/FeedStoreComponent.kt b/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/ui/store/FeedStoreComponent.kt deleted file mode 100644 index 90b35f5..0000000 --- a/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/ui/store/FeedStoreComponent.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.stslex.wizard.feature.film_feed.ui.store - -import androidx.compose.runtime.Stable -import com.stslex.wizard.core.ui.mvi.StoreComponent -import com.stslex.wizard.feature.film_feed.ui.model.FilmModel -import com.stslex.wizard.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/wizard/feature/film_feed/ui/store/FeedStoreImpl.kt b/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/ui/store/FeedStoreImpl.kt new file mode 100644 index 0000000..faa60fb --- /dev/null +++ b/feature/film_feed/src/commonMain/kotlin/com/stslex/wizard/feature/film_feed/ui/store/FeedStoreImpl.kt @@ -0,0 +1,102 @@ +package com.stslex.wizard.feature.film_feed.ui.store + +import androidx.compose.runtime.Stable +import com.stslex.wizard.core.core.AppDispatcher +import com.stslex.wizard.core.core.Logger +import com.stslex.wizard.core.ui.mvi.BaseStore +import com.stslex.wizard.feature.film_feed.domain.interactor.FeedInteractor +import com.stslex.wizard.feature.film_feed.navigation.FeedScreenRouter +import com.stslex.wizard.feature.film_feed.ui.model.ScreenState +import com.stslex.wizard.feature.film_feed.ui.model.toUI +import com.stslex.wizard.feature.film_feed.ui.store.FeedStore.Action +import com.stslex.wizard.feature.film_feed.ui.store.FeedStore.Event +import com.stslex.wizard.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, + private val router: FeedScreenRouter +) : BaseStore( + initialState = State.INITIAL, + appDispatcher = appDispatcher +), FeedStore { + + private var loadingJob: Job? = null + + override fun process(action: Action) { + when (action) { + is Action.LoadFilms -> actionLoadFilms() + is Action.FilmClick -> actionFilmClick(action) + is Action.Navigation -> router(action) + } + } + + private fun actionFilmClick(action: Action.FilmClick) { + sendAction(Action.Navigation.Film(action.filmId)) + } + + private fun actionLoadFilms() { + if (loadingJob?.isActive == true) { + Logger.d("Loading job is active") + return + } + val hasNextPage = state.value.hasNextPage + if (hasNextPage.not()) { + Logger.d("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/src/commonMain/kotlin/com/stslex/wizard/feature/follower/di/ModuleFeatureFollower.kt b/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/di/ModuleFeatureFollower.kt index 41bea48..b4f72dc 100644 --- a/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/di/ModuleFeatureFollower.kt +++ b/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/di/ModuleFeatureFollower.kt @@ -9,6 +9,7 @@ import com.stslex.wizard.feature.follower.domain.interactor.FollowerInteractorIm import com.stslex.wizard.feature.follower.navigation.FollowerRouter import com.stslex.wizard.feature.follower.navigation.FollowerRouterImpl import com.stslex.wizard.feature.follower.ui.store.FollowerStore +import com.stslex.wizard.feature.follower.ui.store.FollowerStoreImpl import org.koin.core.annotation.Module import org.koin.core.module.dsl.bind import org.koin.core.module.dsl.factoryOf @@ -21,6 +22,6 @@ class ModuleFeatureFollower : AppModule() { factoryOf(::FollowerRepositoryImpl) { bind() } factoryOf(::FollowerInteractorImpl) { bind() } factoryOf(::FollowerRouterImpl) { bind() } - storeOf(::FollowerStore) + storeOf(::FollowerStoreImpl) { bind() } } } diff --git a/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/navigation/FollowerRouter.kt b/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/navigation/FollowerRouter.kt index d7bb829..1db8523 100644 --- a/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/navigation/FollowerRouter.kt +++ b/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/navigation/FollowerRouter.kt @@ -1,7 +1,7 @@ package com.stslex.wizard.feature.follower.navigation import com.stslex.wizard.core.ui.mvi.Router -import com.stslex.wizard.feature.follower.ui.store.FollowerStoreComponent.Navigation +import com.stslex.wizard.feature.follower.ui.store.FollowerStore.Action.Navigation interface FollowerRouter : Router diff --git a/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/navigation/FollowerRouterImpl.kt b/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/navigation/FollowerRouterImpl.kt index c40c001..9be49eb 100644 --- a/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/navigation/FollowerRouterImpl.kt +++ b/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/navigation/FollowerRouterImpl.kt @@ -1,11 +1,11 @@ package com.stslex.wizard.feature.follower.navigation import com.stslex.wizard.core.navigation.navigator.Navigator -import com.stslex.wizard.feature.follower.ui.store.FollowerStoreComponent +import com.stslex.wizard.feature.follower.ui.store.FollowerStore.Action.Navigation class FollowerRouterImpl(private val navigator: Navigator) : FollowerRouter { - override fun invoke(event: FollowerStoreComponent.Navigation) { + override fun invoke(event: Navigation) { TODO("Not yet implemented") } } \ No newline at end of file diff --git a/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/navigation/GraphFollower.kt b/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/navigation/GraphFollower.kt index aaa30b5..bbe82f3 100644 --- a/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/navigation/GraphFollower.kt +++ b/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/navigation/GraphFollower.kt @@ -10,7 +10,7 @@ import com.stslex.wizard.core.navigation.navScreen import com.stslex.wizard.core.ui.mvi.getStore import com.stslex.wizard.feature.follower.ui.FollowerScreen import com.stslex.wizard.feature.follower.ui.store.FollowerStore -import com.stslex.wizard.feature.follower.ui.store.FollowerStoreComponent.Action +import com.stslex.wizard.feature.follower.ui.store.FollowerStore.Action fun NavGraphBuilder.graphFollower() { navScreen { screen -> diff --git a/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/ui/FollowerScreen.kt b/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/ui/FollowerScreen.kt index ddfa811..fcbb1a2 100644 --- a/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/ui/FollowerScreen.kt +++ b/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/ui/FollowerScreen.kt @@ -8,8 +8,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.stslex.wizard.core.ui.base.paging.PagingColumn import com.stslex.wizard.feature.follower.ui.store.FollowerScreenState -import com.stslex.wizard.feature.follower.ui.store.FollowerStoreComponent.Action -import com.stslex.wizard.feature.follower.ui.store.FollowerStoreComponent.State +import com.stslex.wizard.feature.follower.ui.store.FollowerStore.Action +import com.stslex.wizard.feature.follower.ui.store.FollowerStore.State @Composable internal fun FollowerScreen( diff --git a/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/ui/store/FollowerStore.kt b/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/ui/store/FollowerStore.kt index 68d1db2..6422dbd 100644 --- a/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/ui/store/FollowerStore.kt +++ b/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/ui/store/FollowerStore.kt @@ -1,151 +1,70 @@ package com.stslex.wizard.feature.follower.ui.store -import com.stslex.wizard.core.core.AppDispatcher -import com.stslex.wizard.core.navigation.Screen.Follower.FollowerType -import com.stslex.wizard.core.ui.base.mapToAppError -import com.stslex.wizard.core.ui.base.paging.toUi +import androidx.compose.runtime.Stable +import com.stslex.wizard.core.navigation.Screen +import com.stslex.wizard.core.ui.base.paging.PagingConfig +import com.stslex.wizard.core.ui.base.paging.PagingUiState +import com.stslex.wizard.core.ui.mvi.CommonEvents import com.stslex.wizard.core.ui.mvi.Store -import com.stslex.wizard.core.ui.mvi.StoreComponent.Event.Snackbar -import com.stslex.wizard.core.ui.pager.pager.StorePager -import com.stslex.wizard.core.ui.pager.pager.StorePagerFactory -import com.stslex.wizard.core.ui.pager.states.PagerLoadState -import com.stslex.wizard.feature.follower.domain.interactor.FollowerInteractor -import com.stslex.wizard.feature.follower.navigation.FollowerRouter import com.stslex.wizard.feature.follower.ui.model.FollowerModel -import com.stslex.wizard.feature.follower.ui.model.toUi -import com.stslex.wizard.feature.follower.ui.store.FollowerStoreComponent.Action -import com.stslex.wizard.feature.follower.ui.store.FollowerStoreComponent.Event -import com.stslex.wizard.feature.follower.ui.store.FollowerStoreComponent.Navigation -import com.stslex.wizard.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) { - FollowerType.FOLLOWER -> interactor.getFollowers( - uuid = currentState.uuid, - query = currentState.query, - page = page, - pageSize = pageSize - ) - - FollowerType.FOLLOWING -> interactor.getFollowing( - uuid = currentState.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() +import com.stslex.wizard.feature.follower.ui.store.FollowerStore.Action +import com.stslex.wizard.feature.follower.ui.store.FollowerStore.Event +import com.stslex.wizard.feature.follower.ui.store.FollowerStore.State + +interface FollowerStore : Store { + + @Stable + data class State( + val type: Screen.Follower.FollowerType, + val uuid: String, + val paging: PagingUiState, + val screen: FollowerScreenState, + val query: String + ) : Store.State { + + companion object { + + val INITIAL = State( + type = Screen.Follower.FollowerType.FOLLOWER, + uuid = "", + paging = PagingUiState.default(PagingConfig.DEFAULT), + screen = FollowerScreenState.Shimmer, + query = "" + ) } } - private fun actionInit(action: Action.Init) { - updateState { state -> - state.copy( - type = action.followerType, - uuid = action.uuid - ) - } + @Stable + sealed interface Action : Store.Action { - pager.state.launch { pagerState -> - updateState { currentState -> - currentState.copy( - paging = pagerState.toUi(currentState.paging.config) - ) - } - } + @Stable + data class Init( + val followerType: Screen.Follower.FollowerType, + val uuid: String + ) : Action - pager.loadState.launch { loadState -> - updateState { currentState -> - currentState.copy( - screen = loadState.toUi() - ) - } - } + @Stable + data object Load : Action - 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") - } - - private fun actionQueryChanged(action: Action.QueryChanged) { - updateState { currentState -> - currentState.copy( - query = action.query - ) - } - } + @Stable + data class OnUserClick(val uuid: String) : Action - private fun actionRefresh() { - pager.refresh(isForceLoad = true) + sealed interface Navigation : Action, Store.Action.Navigation } - private fun actionRetry() { - pager.retry() - } + @Stable + sealed interface Event : Store.Event { - 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) - ) - } - } + @Stable + data class ShowSnackbar(val snackbar: CommonEvents.Snackbar) : Event } } diff --git a/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/ui/store/FollowerStoreComponent.kt b/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/ui/store/FollowerStoreComponent.kt deleted file mode 100644 index 775b71a..0000000 --- a/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/ui/store/FollowerStoreComponent.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.stslex.wizard.feature.follower.ui.store - -import androidx.compose.runtime.Stable -import com.stslex.wizard.core.navigation.Screen -import com.stslex.wizard.core.ui.base.paging.PagingConfig -import com.stslex.wizard.core.ui.base.paging.PagingUiState -import com.stslex.wizard.core.ui.mvi.StoreComponent -import com.stslex.wizard.core.ui.mvi.StoreComponent.Event.Snackbar -import com.stslex.wizard.feature.follower.ui.model.FollowerModel - -interface FollowerStoreComponent : StoreComponent { - - @Stable - data class State( - val type: Screen.Follower.FollowerType, - val uuid: String, - val paging: PagingUiState, - val screen: FollowerScreenState, - val query: String - ) : StoreComponent.State { - - companion object { - - val INITIAL = State( - type = Screen.Follower.FollowerType.FOLLOWER, - uuid = "", - paging = PagingUiState.default(PagingConfig.DEFAULT), - screen = FollowerScreenState.Shimmer, - query = "" - ) - } - } - - @Stable - sealed interface Action : StoreComponent.Action { - - @Stable - data class Init( - val followerType: Screen.Follower.FollowerType, - val uuid: String - ) : 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/wizard/feature/follower/ui/store/FollowerStoreImpl.kt b/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/ui/store/FollowerStoreImpl.kt new file mode 100644 index 0000000..eef3380 --- /dev/null +++ b/feature/follower/src/commonMain/kotlin/com/stslex/wizard/feature/follower/ui/store/FollowerStoreImpl.kt @@ -0,0 +1,150 @@ +package com.stslex.wizard.feature.follower.ui.store + +import com.stslex.wizard.core.core.AppDispatcher +import com.stslex.wizard.core.navigation.Screen.Follower.FollowerType +import com.stslex.wizard.core.ui.base.mapToAppError +import com.stslex.wizard.core.ui.base.paging.toUi +import com.stslex.wizard.core.ui.mvi.BaseStore +import com.stslex.wizard.core.ui.mvi.CommonEvents.Snackbar +import com.stslex.wizard.core.ui.pager.pager.StorePager +import com.stslex.wizard.core.ui.pager.pager.StorePagerFactory +import com.stslex.wizard.core.ui.pager.states.PagerLoadState +import com.stslex.wizard.feature.follower.domain.interactor.FollowerInteractor +import com.stslex.wizard.feature.follower.navigation.FollowerRouter +import com.stslex.wizard.feature.follower.ui.model.FollowerModel +import com.stslex.wizard.feature.follower.ui.model.toUi +import com.stslex.wizard.feature.follower.ui.store.FollowerStore.Action +import com.stslex.wizard.feature.follower.ui.store.FollowerStore.Event +import com.stslex.wizard.feature.follower.ui.store.FollowerStore.State +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map + +class FollowerStoreImpl( + private val interactor: FollowerInteractor, + private val router: FollowerRouter, + appDispatcher: AppDispatcher, + pagerFactory: StorePagerFactory, +) : FollowerStore, BaseStore( + appDispatcher = appDispatcher, + initialState = State.INITIAL, +) { + + private val pager: StorePager = pagerFactory.create( + request = { page, pageSize -> + val currentState = state.value + when (currentState.type) { + FollowerType.FOLLOWER -> interactor.getFollowers( + uuid = currentState.uuid, + query = currentState.query, + page = page, + pageSize = pageSize + ) + + FollowerType.FOLLOWING -> interactor.getFollowing( + uuid = currentState.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) + is Action.Navigation -> router(action) + Action.Refresh -> actionRefresh() + Action.Retry -> actionRetry() + } + } + + private fun actionInit(action: Action.Init) { + updateState { state -> + state.copy( + type = action.followerType, + 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")) + ) + } + + 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/src/commonMain/kotlin/com/stslex/wizard/feature/match/di/ModuleFeatureMatch.kt b/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/di/ModuleFeatureMatch.kt index 20507dd..1f2bc23 100644 --- a/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/di/ModuleFeatureMatch.kt +++ b/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/di/ModuleFeatureMatch.kt @@ -9,6 +9,7 @@ import com.stslex.wizard.feature.match.domain.interactor.MatchInteractorImpl import com.stslex.wizard.feature.match.navigation.MatchRouter import com.stslex.wizard.feature.match.navigation.MatchRouterImpl import com.stslex.wizard.feature.match.ui.store.MatchStore +import com.stslex.wizard.feature.match.ui.store.MatchStoreImpl import org.koin.core.annotation.Module import org.koin.core.module.dsl.bind import org.koin.core.module.dsl.factoryOf @@ -21,6 +22,6 @@ class ModuleFeatureMatch : AppModule() { factoryOf(::MatchRepositoryImpl) { bind() } factoryOf(::MatchInteractorImpl) { bind() } factoryOf(::MatchRouterImpl) { bind() } - storeOf(::MatchStore) + storeOf(::MatchStoreImpl) { bind() } } } diff --git a/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/navigation/GraphMatch.kt b/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/navigation/GraphMatch.kt index 92c11de..f5fa70e 100644 --- a/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/navigation/GraphMatch.kt +++ b/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/navigation/GraphMatch.kt @@ -11,8 +11,8 @@ import com.stslex.wizard.core.navigation.navScreen import com.stslex.wizard.core.ui.mvi.getStore import com.stslex.wizard.feature.match.ui.MatchScreen import com.stslex.wizard.feature.match.ui.store.MatchStore -import com.stslex.wizard.feature.match.ui.store.MatchStoreComponent.Action -import com.stslex.wizard.feature.match.ui.store.MatchStoreComponent.Event +import com.stslex.wizard.feature.match.ui.store.MatchStore.Action +import com.stslex.wizard.feature.match.ui.store.MatchStore.Event fun NavGraphBuilder.graphMatch() { navScreen { screen -> diff --git a/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/navigation/MatchRouter.kt b/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/navigation/MatchRouter.kt index fdc5871..b7487cc 100644 --- a/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/navigation/MatchRouter.kt +++ b/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/navigation/MatchRouter.kt @@ -1,6 +1,6 @@ package com.stslex.wizard.feature.match.navigation import com.stslex.wizard.core.ui.mvi.Router -import com.stslex.wizard.feature.match.ui.store.MatchStoreComponent.Navigation +import com.stslex.wizard.feature.match.ui.store.MatchStore -interface MatchRouter : Router \ No newline at end of file +interface MatchRouter : Router \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/navigation/MatchRouterImpl.kt b/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/navigation/MatchRouterImpl.kt index f421ea7..2edc00d 100644 --- a/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/navigation/MatchRouterImpl.kt +++ b/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/navigation/MatchRouterImpl.kt @@ -2,7 +2,7 @@ package com.stslex.wizard.feature.match.navigation import com.stslex.wizard.core.navigation.Screen import com.stslex.wizard.core.navigation.navigator.Navigator -import com.stslex.wizard.feature.match.ui.store.MatchStoreComponent.Navigation +import com.stslex.wizard.feature.match.ui.store.MatchStore.Action.Navigation class MatchRouterImpl(private val navigator: Navigator) : MatchRouter { diff --git a/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/ui/MatchScreen.kt b/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/ui/MatchScreen.kt index 870545f..66e714a 100644 --- a/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/ui/MatchScreen.kt +++ b/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/ui/MatchScreen.kt @@ -11,8 +11,8 @@ import com.stslex.wizard.feature.match.ui.components.MatchScreenEmpty import com.stslex.wizard.feature.match.ui.components.MatchScreenError import com.stslex.wizard.feature.match.ui.components.MatchScreenShimmer import com.stslex.wizard.feature.match.ui.store.MatchScreenState -import com.stslex.wizard.feature.match.ui.store.MatchStoreComponent.Action -import com.stslex.wizard.feature.match.ui.store.MatchStoreComponent.State +import com.stslex.wizard.feature.match.ui.store.MatchStore.Action +import com.stslex.wizard.feature.match.ui.store.MatchStore.State @Composable internal fun MatchScreen( diff --git a/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/ui/components/MatchScreenContent.kt b/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/ui/components/MatchScreenContent.kt index 5bd2d02..f8d1453 100644 --- a/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/ui/components/MatchScreenContent.kt +++ b/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/ui/components/MatchScreenContent.kt @@ -23,7 +23,7 @@ import com.stslex.wizard.core.ui.base.shimmerLoadingAnimation import com.stslex.wizard.core.ui.theme.AppDimension import com.stslex.wizard.feature.match.ui.model.MatchUiModel import com.stslex.wizard.feature.match.ui.store.MatchScreenState -import com.stslex.wizard.feature.match.ui.store.MatchStoreComponent.Action +import com.stslex.wizard.feature.match.ui.store.MatchStore.Action @OptIn(ExperimentalMaterialApi::class) @Composable diff --git a/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/ui/store/MatchStore.kt b/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/ui/store/MatchStore.kt index afbee4f..0cd711a 100644 --- a/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/ui/store/MatchStore.kt +++ b/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/ui/store/MatchStore.kt @@ -1,177 +1,81 @@ package com.stslex.wizard.feature.match.ui.store -import com.stslex.wizard.core.core.AppDispatcher -import com.stslex.wizard.core.core.Logger -import com.stslex.wizard.core.database.store.UserStore +import androidx.compose.runtime.Stable import com.stslex.wizard.core.navigation.Screen -import com.stslex.wizard.core.ui.base.mapToAppError -import com.stslex.wizard.core.ui.base.paging.toUi +import com.stslex.wizard.core.ui.base.paging.PagingConfig +import com.stslex.wizard.core.ui.base.paging.PagingUiState +import com.stslex.wizard.core.ui.mvi.CommonEvents import com.stslex.wizard.core.ui.mvi.Store -import com.stslex.wizard.core.ui.mvi.StoreComponent.Event.Snackbar -import com.stslex.wizard.core.ui.pager.pager.StorePager -import com.stslex.wizard.core.ui.pager.pager.StorePagerFactory -import com.stslex.wizard.core.ui.pager.states.PagerLoadState -import com.stslex.wizard.feature.match.domain.interactor.MatchInteractor -import com.stslex.wizard.feature.match.navigation.MatchRouter import com.stslex.wizard.feature.match.ui.model.MatchUiModel -import com.stslex.wizard.feature.match.ui.model.toUi -import com.stslex.wizard.feature.match.ui.store.MatchStoreComponent.Action -import com.stslex.wizard.feature.match.ui.store.MatchStoreComponent.Event -import com.stslex.wizard.feature.match.ui.store.MatchStoreComponent.Navigation -import com.stslex.wizard.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 +import com.stslex.wizard.feature.match.ui.store.MatchStore.Action +import com.stslex.wizard.feature.match.ui.store.MatchStore.Event +import com.stslex.wizard.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 = "" ) - }, - scope = scope, - mapper = { it.toUi() }, - config = state.value.paging.config - ) - - override fun process(action: Action) { - Logger.d("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) - ) - } - } + @Stable + sealed interface Event : Store.Event { - pager.loadState.launch { loadState -> - updateState { currentState -> - currentState.copy( - screen = loadState.toUi() - ) - } - } + data class ShowSnackbar( + val snackbar: CommonEvents.Snackbar + ) : Event + } - pager.loadEvents.launch { - sendEvent( - Event.ShowSnackbar(Snackbar.Error("error load matches")) - ) - } + @Stable + sealed interface Action : Store.Action { - updateState { currentState -> - currentState.copy( - isSelf = action.type == Screen.Match.Type.SELF, - uuid = action.uuid.ifBlank { userStore.uuid }, - ) - } + data class Init( + val type: Screen.Match.Type, + val uuid: String + ) : Action - state - .map { it.query } - .distinctUntilChanged() - .launch( - onError = ::showError - ) { - if (pager.loadState.value is PagerLoadState.Initial) { - pager.initialLoad() - } else { - pager.refresh(isForceLoad = false) - } - } - } + data object Refresh : Action - private fun actionLoadMore() { - pager.load() - } + data object LoadMore : Action - private fun actionOnMatchClick(action: Action.OnMatchClick) { - consumeNavigation(Navigation.MatchDetails(action.matchUuid)) - } + data class OnMatchClick( + val matchUuid: String + ) : Action - private fun actionRetryClick() { - pager.retry() - } + data object OnRetryClick : Action - private fun actionRefresh() { - pager.refresh(isForceLoad = true) - } + data object Logout : Action - private fun actionLogout() { - launch( - action = { - interactor.logout() - }, - onSuccess = { - consumeNavigation(Navigation.LogOut) - }, - onError = ::showError - ) - } + data object RepeatLastAction : Action, Store.Action.RepeatLast - 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 OnQueryChanged( + val query: String + ) : Action - 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) - ) - } + @Stable + sealed interface Navigation : Action, Store.Action.Navigation { + + data class MatchDetails(val matchUuid: String) : Navigation + + data object LogOut : Navigation } } +} - companion object { - private const val TAG = "MatchStore" - } -} \ No newline at end of file diff --git a/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/ui/store/MatchStoreComponent.kt b/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/ui/store/MatchStoreComponent.kt deleted file mode 100644 index f3ecaf9..0000000 --- a/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/ui/store/MatchStoreComponent.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.stslex.wizard.feature.match.ui.store - -import androidx.compose.runtime.Stable -import com.stslex.wizard.core.navigation.Screen -import com.stslex.wizard.core.ui.base.paging.PagingConfig -import com.stslex.wizard.core.ui.base.paging.PagingUiState -import com.stslex.wizard.core.ui.mvi.StoreComponent -import com.stslex.wizard.core.ui.mvi.StoreComponent.Event.Snackbar -import com.stslex.wizard.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 type: Screen.Match.Type, - val uuid: String - ) : 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/wizard/feature/match/ui/store/MatchStoreImpl.kt b/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/ui/store/MatchStoreImpl.kt new file mode 100644 index 0000000..2abc500 --- /dev/null +++ b/feature/match/src/commonMain/kotlin/com/stslex/wizard/feature/match/ui/store/MatchStoreImpl.kt @@ -0,0 +1,176 @@ +package com.stslex.wizard.feature.match.ui.store + +import com.stslex.wizard.core.core.AppDispatcher +import com.stslex.wizard.core.core.Logger +import com.stslex.wizard.core.database.store.UserStore +import com.stslex.wizard.core.navigation.Screen +import com.stslex.wizard.core.ui.base.mapToAppError +import com.stslex.wizard.core.ui.base.paging.toUi +import com.stslex.wizard.core.ui.mvi.BaseStore +import com.stslex.wizard.core.ui.mvi.CommonEvents.Snackbar +import com.stslex.wizard.core.ui.pager.pager.StorePager +import com.stslex.wizard.core.ui.pager.pager.StorePagerFactory +import com.stslex.wizard.core.ui.pager.states.PagerLoadState +import com.stslex.wizard.feature.match.domain.interactor.MatchInteractor +import com.stslex.wizard.feature.match.navigation.MatchRouter +import com.stslex.wizard.feature.match.ui.model.MatchUiModel +import com.stslex.wizard.feature.match.ui.model.toUi +import com.stslex.wizard.feature.match.ui.store.MatchStore.Action +import com.stslex.wizard.feature.match.ui.store.MatchStore.Event +import com.stslex.wizard.feature.match.ui.store.MatchStore.State +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map + +class MatchStoreImpl( + appDispatcher: AppDispatcher, + pagerFactory: StorePagerFactory, + private val router: MatchRouter, + private val interactor: MatchInteractor, + private val userStore: UserStore, +) : MatchStore, BaseStore( + appDispatcher = appDispatcher, + 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.d("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) + is Action.Navigation -> router(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.type == Screen.Match.Type.SELF, + uuid = action.uuid.ifBlank { 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) { + sendAction(Action.Navigation.MatchDetails(action.matchUuid)) + } + + private fun actionRetryClick() { + pager.retry() + } + + private fun actionRefresh() { + pager.refresh(isForceLoad = true) + } + + private fun actionLogout() { + launch( + action = { + interactor.logout() + }, + onSuccess = { + sendAction(Action.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/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/di/ModuleFeatureMatchFeed.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/di/ModuleFeatureMatchFeed.kt index bc985f4..5e035a4 100644 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/di/ModuleFeatureMatchFeed.kt +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/di/ModuleFeatureMatchFeed.kt @@ -9,6 +9,7 @@ import com.stslex.wizard.feature.match_feed.domain.MatchFeedInteractorImpl import com.stslex.wizard.feature.match_feed.navigation.MatchFeedRouter import com.stslex.wizard.feature.match_feed.navigation.MatchFeedRouterImpl import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStore +import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStoreImpl import org.koin.core.annotation.Module import org.koin.core.module.dsl.bind import org.koin.core.module.dsl.factoryOf @@ -21,6 +22,6 @@ class ModuleFeatureMatchFeed : AppModule() { factoryOf(::MatchFeedMockRepositoryImpl) { bind() } factoryOf(::MatchFeedInteractorImpl) { bind() } factoryOf(::MatchFeedRouterImpl) { bind() } - storeOf(::MatchFeedStore) + storeOf(::MatchFeedStoreImpl) { bind() } } } diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/navigation/GraphMatchFeed.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/navigation/GraphMatchFeed.kt index dadb39b..4e31727 100644 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/navigation/GraphMatchFeed.kt +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/navigation/GraphMatchFeed.kt @@ -10,8 +10,8 @@ import com.stslex.wizard.core.navigation.navScreen import com.stslex.wizard.core.ui.mvi.getStore import com.stslex.wizard.feature.match_feed.ui.MatchFeedScreen import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStore -import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStoreComponent.Action -import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStoreComponent.Event.ErrorSnackBar +import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStore.Action +import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStore.Event.ErrorSnackBar fun NavGraphBuilder.graphMatchFeed() { navScreen { diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/navigation/MatchFeedRouter.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/navigation/MatchFeedRouter.kt index eee5ad0..7bbd98f 100644 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/navigation/MatchFeedRouter.kt +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/navigation/MatchFeedRouter.kt @@ -1,7 +1,6 @@ package com.stslex.wizard.feature.match_feed.navigation import com.stslex.wizard.core.ui.mvi.Router -import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStoreComponent.Navigation - -interface MatchFeedRouter : Router +import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStore +interface MatchFeedRouter : Router diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/navigation/MatchFeedRouterImpl.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/navigation/MatchFeedRouterImpl.kt index ebdbe2b..59a0e1c 100644 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/navigation/MatchFeedRouterImpl.kt +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/navigation/MatchFeedRouterImpl.kt @@ -2,7 +2,7 @@ package com.stslex.wizard.feature.match_feed.navigation import com.stslex.wizard.core.navigation.Screen import com.stslex.wizard.core.navigation.navigator.Navigator -import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStoreComponent.Navigation +import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStore.Action.Navigation class MatchFeedRouterImpl( private val navigator: Navigator diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/ui/MatchFeedScreen.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/ui/MatchFeedScreen.kt index 2eeecdc..1984480 100644 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/ui/MatchFeedScreen.kt +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/ui/MatchFeedScreen.kt @@ -8,8 +8,8 @@ import androidx.compose.ui.Modifier import com.stslex.wizard.feature.match_feed.ui.components.MatchFeedScreenContent import com.stslex.wizard.feature.match_feed.ui.components.MatchFeedScreenError import com.stslex.wizard.feature.match_feed.ui.components.MatchFeedScreenLoading -import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStoreComponent.Action -import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStoreComponent.State +import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStore.Action +import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStore.State import com.stslex.wizard.feature.match_feed.ui.store.ScreenState @Composable diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/ui/store/MatchFeedStore.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/ui/store/MatchFeedStore.kt index 68216d8..fd27343 100644 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/ui/store/MatchFeedStore.kt +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/ui/store/MatchFeedStore.kt @@ -1,131 +1,67 @@ package com.stslex.wizard.feature.match_feed.ui.store -import com.stslex.wizard.core.core.AppDispatcher -import com.stslex.wizard.core.core.Logger +import androidx.compose.runtime.Stable import com.stslex.wizard.core.ui.mvi.Store -import com.stslex.wizard.feature.match_feed.domain.MatchFeedInteractor -import com.stslex.wizard.feature.match_feed.navigation.MatchFeedRouter -import com.stslex.wizard.feature.match_feed.ui.model.toUI -import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStoreComponent.Action -import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStoreComponent.Event -import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStoreComponent.Navigation -import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStoreComponent.State +import com.stslex.wizard.feature.match_feed.ui.components.SwipeDirection +import com.stslex.wizard.feature.match_feed.ui.model.FilmUi +import com.stslex.wizard.feature.match_feed.ui.model.MatchUi +import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStore.Action +import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStore.Event +import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStore.State +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.Job - -class MatchFeedStore( - private val interactor: MatchFeedInteractor, - appDispatcher: AppDispatcher, - router: MatchFeedRouter -) : Store( - 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) + +interface MatchFeedStore : Store { + + @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 fun actionFilmSwiped(action: Action.FilmSwiped) { - // todo send action to backend - } + sealed interface Event : Store.Event { - private fun actionFilmClick(action: Action.FilmClick) { - consumeNavigation(Navigation.Film(action.uuid)) + @Stable + data class ErrorSnackBar(val message: String) : Event } - private fun actionInit() { - interactor - .getLatestMatch() - .launch { match -> - updateState { currentState -> - currentState.copy( - screen = ScreenState.Content.Success, - match = match.toUI() - ) - } - loadFilms(match.uuid) - } - } + sealed interface Action : Store.Action { - private fun actionLoadFilms() { - val matchUuid = state.value.match?.uuid - if (matchUuid == null) { - Logger.d("Match uuid is null") - return - } - loadFilms(matchUuid) - } + data object Init : Action - private fun loadFilms(uuid: String) { - if (loadingJob?.isActive == true) { - Logger.d("Loading job is active") - return - } - val hasNextPage = state.value.hasNextPage - if (hasNextPage.not()) { - Logger.d("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 uuid: String + ) : Action + + @Stable + data class FilmSwiped( + val direction: SwipeDirection, + val uuid: String + ) : 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")) - } - }, - ) - } + sealed interface Navigation : Action, Store.Action.Navigation { - companion object { + data class Film(val uuid: String) : Navigation + } - private const val PAGE_SIZE = 5 } -} \ No newline at end of file +} + diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/ui/store/MatchFeedStoreComponent.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/ui/store/MatchFeedStoreComponent.kt deleted file mode 100644 index 1122fa5..0000000 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/ui/store/MatchFeedStoreComponent.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.stslex.wizard.feature.match_feed.ui.store - -import androidx.compose.runtime.Stable -import com.stslex.wizard.core.ui.mvi.StoreComponent -import com.stslex.wizard.feature.match_feed.ui.components.SwipeDirection -import com.stslex.wizard.feature.match_feed.ui.model.FilmUi -import com.stslex.wizard.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/wizard/feature/match_feed/ui/store/MatchFeedStoreImpl.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/ui/store/MatchFeedStoreImpl.kt new file mode 100644 index 0000000..f18c2b9 --- /dev/null +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/wizard/feature/match_feed/ui/store/MatchFeedStoreImpl.kt @@ -0,0 +1,130 @@ +package com.stslex.wizard.feature.match_feed.ui.store + +import com.stslex.wizard.core.core.AppDispatcher +import com.stslex.wizard.core.core.Logger +import com.stslex.wizard.core.ui.mvi.BaseStore +import com.stslex.wizard.feature.match_feed.domain.MatchFeedInteractor +import com.stslex.wizard.feature.match_feed.navigation.MatchFeedRouter +import com.stslex.wizard.feature.match_feed.ui.model.toUI +import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStore.Action +import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStore.Event +import com.stslex.wizard.feature.match_feed.ui.store.MatchFeedStore.State +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Job + +class MatchFeedStoreImpl( + private val interactor: MatchFeedInteractor, + appDispatcher: AppDispatcher, + private val router: MatchFeedRouter +) : MatchFeedStore, BaseStore( + initialState = State.INITIAL, + appDispatcher = appDispatcher, +) { + + 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) + is Action.Navigation -> router(action) + } + } + + private fun actionFilmSwiped(action: Action.FilmSwiped) { + // todo send action to backend + } + + private fun actionFilmClick(action: Action.FilmClick) { + sendAction(Action.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.d("Match uuid is null") + return + } + loadFilms(matchUuid) + } + + private fun loadFilms(uuid: String) { + if (loadingJob?.isActive == true) { + Logger.d("Loading job is active") + return + } + val hasNextPage = state.value.hasNextPage + if (hasNextPage.not()) { + Logger.d("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/src/commonMain/kotlin/com/stslex/wizard/feature/profile/di/ModuleFeatureProfile.kt b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/di/ModuleFeatureProfile.kt index b2ea8f8..6ce0163 100644 --- a/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/di/ModuleFeatureProfile.kt +++ b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/di/ModuleFeatureProfile.kt @@ -9,6 +9,7 @@ import com.stslex.wizard.feature.profile.domain.interactor.ProfileInteractorImpl import com.stslex.wizard.feature.profile.navigation.ProfileRouter import com.stslex.wizard.feature.profile.navigation.ProfileRouterImpl import com.stslex.wizard.feature.profile.ui.store.ProfileStore +import com.stslex.wizard.feature.profile.ui.store.ProfileStoreImpl import org.koin.core.annotation.Module import org.koin.core.module.dsl.bind import org.koin.core.module.dsl.factoryOf @@ -18,7 +19,7 @@ import org.koin.dsl.ModuleDeclaration class ModuleFeatureProfile : AppModule() { override fun declaration(): ModuleDeclaration = { - storeOf(::ProfileStore) + storeOf(::ProfileStoreImpl) { bind() } factoryOf(::ProfileRouterImpl) { bind() } factoryOf(::ProfileInteractorImpl) { bind() } factoryOf(::ProfileRepositoryImpl) { bind() } diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/navigation/GraphProfile.kt b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/navigation/GraphProfile.kt index 1cbe158..404f715 100644 --- a/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/navigation/GraphProfile.kt +++ b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/navigation/GraphProfile.kt @@ -10,13 +10,13 @@ import com.stslex.wizard.core.navigation.Screen import com.stslex.wizard.core.navigation.navScreen import com.stslex.wizard.core.ui.mvi.getStore import com.stslex.wizard.feature.profile.ui.ProfileScreen -import com.stslex.wizard.feature.profile.ui.store.ProfileStore -import com.stslex.wizard.feature.profile.ui.store.ProfileStoreComponent.Action -import com.stslex.wizard.feature.profile.ui.store.ProfileStoreComponent.Event +import com.stslex.wizard.feature.profile.ui.store.ProfileStoreImpl +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Action +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Event fun NavGraphBuilder.graphProfile() { navScreen { screen -> - val store = getStore() + val store = getStore() LaunchedEffect(Unit) { store.sendAction( Action.Init( diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/navigation/ProfileRouter.kt b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/navigation/ProfileRouter.kt index 9bbb70e..8f7d899 100644 --- a/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/navigation/ProfileRouter.kt +++ b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/navigation/ProfileRouter.kt @@ -1,6 +1,6 @@ package com.stslex.wizard.feature.profile.navigation import com.stslex.wizard.core.ui.mvi.Router -import com.stslex.wizard.feature.profile.ui.store.ProfileStoreComponent +import com.stslex.wizard.feature.profile.ui.store.ProfileStore -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/wizard/feature/profile/navigation/ProfileRouterImpl.kt b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/navigation/ProfileRouterImpl.kt index e04236a..21fc5ef 100644 --- a/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/navigation/ProfileRouterImpl.kt +++ b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/navigation/ProfileRouterImpl.kt @@ -2,7 +2,7 @@ package com.stslex.wizard.feature.profile.navigation import com.stslex.wizard.core.navigation.Screen import com.stslex.wizard.core.navigation.navigator.Navigator -import com.stslex.wizard.feature.profile.ui.store.ProfileStoreComponent.Navigation +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Action.Navigation class ProfileRouterImpl( private val navigator: Navigator diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/ui/ProfileScreen.kt b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/ui/ProfileScreen.kt index 7e13590..b3f7b08 100644 --- a/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/ui/ProfileScreen.kt +++ b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/ui/ProfileScreen.kt @@ -20,8 +20,8 @@ import com.stslex.wizard.core.ui.components.AppSnackbarHost import com.stslex.wizard.core.ui.theme.AppDimension import com.stslex.wizard.feature.profile.ui.components.ProfileScreenContent import com.stslex.wizard.feature.profile.ui.store.ProfileScreenState -import com.stslex.wizard.feature.profile.ui.store.ProfileStoreComponent.Action -import com.stslex.wizard.feature.profile.ui.store.ProfileStoreComponent.State +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Action +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.State @Composable internal fun ProfileScreen( diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/ui/components/ProfileScreenContent.kt b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/ui/components/ProfileScreenContent.kt index 3882d17..1a8961f 100644 --- a/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/ui/components/ProfileScreenContent.kt +++ b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/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.wizard.feature.profile.ui.store.ProfileScreenState -import com.stslex.wizard.feature.profile.ui.store.ProfileStoreComponent.Action +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Action @Composable internal fun ProfileScreenContent( diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/ui/store/ProfileStore.kt b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/ui/store/ProfileStore.kt index 3dd7d15..fe7e868 100644 --- a/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/ui/store/ProfileStore.kt +++ b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/ui/store/ProfileStore.kt @@ -1,141 +1,82 @@ package com.stslex.wizard.feature.profile.ui.store -import androidx.compose.ui.graphics.Color -import com.stslex.wizard.core.core.AppDispatcher -import com.stslex.wizard.core.database.store.UserStore +import androidx.compose.runtime.Stable import com.stslex.wizard.core.navigation.Screen +import com.stslex.wizard.core.ui.mvi.CommonEvents import com.stslex.wizard.core.ui.mvi.Store -import com.stslex.wizard.core.ui.mvi.StoreComponent.Event.Snackbar -import com.stslex.wizard.feature.profile.domain.interactor.ProfileInteractor -import com.stslex.wizard.feature.profile.navigation.ProfileRouter -import com.stslex.wizard.feature.profile.ui.model.ProfileAvatarModel -import com.stslex.wizard.feature.profile.ui.model.toUi -import com.stslex.wizard.feature.profile.ui.store.ProfileStoreComponent.Action -import com.stslex.wizard.feature.profile.ui.store.ProfileStoreComponent.Event -import com.stslex.wizard.feature.profile.ui.store.ProfileStoreComponent.Navigation -import com.stslex.wizard.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() - } - } +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Action +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Event +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.State + +interface ProfileStore : Store { + + @Stable + data class State( + val uuid: String, + val isSelf: Boolean, + val screen: ProfileScreenState + ) : Store.State { - private fun actionInit(action: Action.Init) { - val uuid = action.uuid.ifBlank { userStore.uuid } + companion object { - updateState { currentState -> - currentState.copy( - isSelf = action.type == Screen.Profile.Type.SELF, - uuid = uuid, + val INITIAL = State( + uuid = "", + isSelf = false, + screen = ProfileScreenState.Shimmer ) } - - 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() { - consumeNavigation(Navigation.Favourite(state.value.uuid)) - } + @Stable + sealed interface Action : Store.Action { - private fun actionFollowingClick() { - consumeNavigation(Navigation.Following(state.value.uuid)) - } + @Stable + data class Init( + val type: Screen.Profile.Type, + val uuid: String + ) : Action - private fun actionFollowersClick() { - consumeNavigation(Navigation.Followers(state.value.uuid)) - } + data object Logout : Action - 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 RepeatLastAction : Action, Store.Action.RepeatLast - private fun actionLogout() { - val currentScreen = state.value.screen + data object FavouriteClick : Action - if ( - currentScreen is ProfileScreenState.Content.Loading || - currentScreen is ProfileScreenState.Shimmer - ) { - return - } + data object FollowingClick : Action - updateState { currentState -> - currentState.copy( - screen = ProfileScreenState.Shimmer - ) - } + data object FollowersClick : Action - launch( - action = { - interactor.logOut() - }, - onSuccess = { - consumeNavigation(Navigation.LogIn) - }, - onError = { error -> - sendEvent(Event.ShowSnackbar(Snackbar.Error(error.message ?: "error logout"))) - } - ) - } + data object SettingsClick : Action + + data object BackButtonClick : Action + + + sealed interface Navigation : Action, Store.Action.Navigation { + + data object LogIn : Navigation - private fun actionSettingsClick() { - consumeNavigation(Navigation.Settings) + 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 + } } - private fun actionBackClick() { - consumeNavigation(Navigation.Back) + @Stable + sealed interface Event : Store.Event { + + @Stable + data class ShowSnackbar(val snackbar: CommonEvents.Snackbar) : Event } -} +} \ No newline at end of file diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/ui/store/ProfileStoreComponent.kt b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/ui/store/ProfileStoreComponent.kt deleted file mode 100644 index e5678ec..0000000 --- a/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/ui/store/ProfileStoreComponent.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.stslex.wizard.feature.profile.ui.store - -import androidx.compose.runtime.Stable -import com.stslex.wizard.core.navigation.Screen -import com.stslex.wizard.core.ui.mvi.StoreComponent -import com.stslex.wizard.core.ui.mvi.StoreComponent.Event.Snackbar - -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 type: Screen.Profile.Type, - val uuid: String - ) : 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/wizard/feature/profile/ui/store/ProfileStoreImpl.kt b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/ui/store/ProfileStoreImpl.kt new file mode 100644 index 0000000..1e58064 --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/ui/store/ProfileStoreImpl.kt @@ -0,0 +1,140 @@ +package com.stslex.wizard.feature.profile.ui.store + +import androidx.compose.ui.graphics.Color +import com.stslex.wizard.core.core.AppDispatcher +import com.stslex.wizard.core.database.store.UserStore +import com.stslex.wizard.core.navigation.Screen +import com.stslex.wizard.core.ui.mvi.BaseStore +import com.stslex.wizard.core.ui.mvi.CommonEvents.Snackbar +import com.stslex.wizard.feature.profile.domain.interactor.ProfileInteractor +import com.stslex.wizard.feature.profile.navigation.ProfileRouter +import com.stslex.wizard.feature.profile.ui.model.ProfileAvatarModel +import com.stslex.wizard.feature.profile.ui.model.toUi +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Action +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Event +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.State + +class ProfileStoreImpl( + private val interactor: ProfileInteractor, + private val userStore: UserStore, + private val router: ProfileRouter, + appDispatcher: AppDispatcher, +) : ProfileStore, BaseStore( + 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() + is Action.Navigation -> router(action) + } + } + + private fun actionInit(action: Action.Init) { + val uuid = action.uuid.ifBlank { userStore.uuid } + + updateState { currentState -> + currentState.copy( + isSelf = action.type == Screen.Profile.Type.SELF, + 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() { + sendAction(Action.Navigation.Favourite(state.value.uuid)) + } + + private fun actionFollowingClick() { + sendAction(Action.Navigation.Following(state.value.uuid)) + } + + private fun actionFollowersClick() { + sendAction(Action.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 = { + sendAction(Action.Navigation.LogIn) + }, + onError = { error -> + sendEvent(Event.ShowSnackbar(Snackbar.Error(error.message ?: "error logout"))) + } + ) + } + + private fun actionSettingsClick() { + sendAction(Action.Navigation.Settings) + } + + private fun actionBackClick() { + sendAction(Action.Navigation.Back) + } +} diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/di/ModuleFeatureSettings.kt b/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/di/ModuleFeatureSettings.kt index 3153cf3..9280760 100644 --- a/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/di/ModuleFeatureSettings.kt +++ b/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/di/ModuleFeatureSettings.kt @@ -6,7 +6,7 @@ import com.stslex.wizard.feature.settings.domain.SettingsInteractor import com.stslex.wizard.feature.settings.domain.SettingsInteractorImpl import com.stslex.wizard.feature.settings.navigation.SettingsRouter import com.stslex.wizard.feature.settings.navigation.SettingsRouterImpl -import com.stslex.wizard.feature.settings.ui.store.SettingsStore +import com.stslex.wizard.feature.settings.ui.store.SettingsStoreImpl import org.koin.core.annotation.Module import org.koin.core.module.dsl.bind import org.koin.core.module.dsl.factoryOf @@ -18,6 +18,6 @@ class ModuleFeatureSettings : AppModule() { override fun declaration(): ModuleDeclaration = { factoryOf(::SettingsInteractorImpl) { bind() } factoryOf(::SettingsRouterImpl) { bind() } - storeOf(::SettingsStore) + storeOf(::SettingsStoreImpl) } } \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/navigation/GraphSettings.kt b/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/navigation/GraphSettings.kt index ea16691..0be69c4 100644 --- a/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/navigation/GraphSettings.kt +++ b/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/navigation/GraphSettings.kt @@ -10,12 +10,12 @@ import com.stslex.wizard.core.navigation.Screen import com.stslex.wizard.core.navigation.navScreen import com.stslex.wizard.core.ui.mvi.getStore import com.stslex.wizard.feature.settings.ui.SettingsScreen -import com.stslex.wizard.feature.settings.ui.store.SettingsStore -import com.stslex.wizard.feature.settings.ui.store.SettingsStoreComponent.Event +import com.stslex.wizard.feature.settings.ui.store.SettingsStoreImpl +import com.stslex.wizard.feature.settings.ui.store.SettingsStore.Event fun NavGraphBuilder.graphSettings() { navScreen { - val store = getStore() + 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/wizard/feature/settings/navigation/SettingsRouter.kt b/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/navigation/SettingsRouter.kt index 6714409..b4157e6 100644 --- a/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/navigation/SettingsRouter.kt +++ b/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/navigation/SettingsRouter.kt @@ -1,6 +1,6 @@ package com.stslex.wizard.feature.settings.navigation import com.stslex.wizard.core.ui.mvi.Router -import com.stslex.wizard.feature.settings.ui.store.SettingsStoreComponent +import com.stslex.wizard.feature.settings.ui.store.SettingsStore -interface SettingsRouter : Router +interface SettingsRouter : Router diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/navigation/SettingsRouterImpl.kt b/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/navigation/SettingsRouterImpl.kt index 421c810..fbbab80 100644 --- a/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/navigation/SettingsRouterImpl.kt +++ b/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/navigation/SettingsRouterImpl.kt @@ -2,7 +2,7 @@ package com.stslex.wizard.feature.settings.navigation import com.stslex.wizard.core.navigation.Screen import com.stslex.wizard.core.navigation.navigator.Navigator -import com.stslex.wizard.feature.settings.ui.store.SettingsStoreComponent.Navigation +import com.stslex.wizard.feature.settings.ui.store.SettingsStore.Action.Navigation class SettingsRouterImpl(private val navigator: Navigator) : SettingsRouter { diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/ui/SettingsScreen.kt b/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/ui/SettingsScreen.kt index 83ae555..7026205 100644 --- a/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/ui/SettingsScreen.kt +++ b/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/ui/SettingsScreen.kt @@ -15,8 +15,8 @@ import com.stslex.wizard.core.ui.components.AppSnackbarHost import com.stslex.wizard.core.ui.components.AppToolbar import com.stslex.wizard.core.ui.theme.AppDimension import com.stslex.wizard.feature.settings.ui.components.SettingsContent -import com.stslex.wizard.feature.settings.ui.store.SettingsStoreComponent.Action -import com.stslex.wizard.feature.settings.ui.store.SettingsStoreComponent.State +import com.stslex.wizard.feature.settings.ui.store.SettingsStore.Action +import com.stslex.wizard.feature.settings.ui.store.SettingsStore.State @Composable internal fun SettingsScreen( diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/ui/store/SettingsStore.kt b/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/ui/store/SettingsStore.kt index 4a7cfa4..b8d1e7a 100644 --- a/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/ui/store/SettingsStore.kt +++ b/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/ui/store/SettingsStore.kt @@ -1,61 +1,43 @@ package com.stslex.wizard.feature.settings.ui.store -import com.stslex.wizard.core.core.AppDispatcher +import androidx.compose.runtime.Stable +import com.stslex.wizard.core.ui.mvi.CommonEvents import com.stslex.wizard.core.ui.mvi.Store -import com.stslex.wizard.core.ui.mvi.StoreComponent.Event.Snackbar -import com.stslex.wizard.feature.settings.domain.SettingsInteractor -import com.stslex.wizard.feature.settings.navigation.SettingsRouter -import com.stslex.wizard.feature.settings.ui.store.SettingsStoreComponent.Action -import com.stslex.wizard.feature.settings.ui.store.SettingsStoreComponent.Event -import com.stslex.wizard.feature.settings.ui.store.SettingsStoreComponent.Navigation -import com.stslex.wizard.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() +import com.stslex.wizard.feature.settings.ui.store.SettingsStore.Action +import com.stslex.wizard.feature.settings.ui.store.SettingsStore.Event +import com.stslex.wizard.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) } } - private fun actionBackClick() { - consumeNavigation(Navigation.Back) - } + @Stable + sealed interface Action : Store.Action { + + data object LogOut : Action - private fun actionLogout() { - if (state.value.isLoading) return - updateState { currentState -> - currentState.copy( - isLoading = true - ) + data object BackButtonClicked : Action + + sealed interface Navigation : Action, Store.Action.Navigation { + + data object Back : Navigation + + data object LogOut : Navigation } + } + + @Stable + sealed interface Event : Store.Event { - 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)) - } - ) + data class ShowSnackbar(val snackbar: CommonEvents.Snackbar) : Event } } \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/ui/store/SettingsStoreComponent.kt b/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/ui/store/SettingsStoreComponent.kt deleted file mode 100644 index 55e6a7d..0000000 --- a/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/ui/store/SettingsStoreComponent.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.stslex.wizard.feature.settings.ui.store - -import androidx.compose.runtime.Stable -import com.stslex.wizard.core.ui.mvi.StoreComponent -import com.stslex.wizard.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/wizard/feature/settings/ui/store/SettingsStoreImpl.kt b/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/ui/store/SettingsStoreImpl.kt new file mode 100644 index 0000000..da72720 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/wizard/feature/settings/ui/store/SettingsStoreImpl.kt @@ -0,0 +1,60 @@ +package com.stslex.wizard.feature.settings.ui.store + +import com.stslex.wizard.core.core.AppDispatcher +import com.stslex.wizard.core.ui.mvi.BaseStore +import com.stslex.wizard.core.ui.mvi.CommonEvents.Snackbar +import com.stslex.wizard.feature.settings.domain.SettingsInteractor +import com.stslex.wizard.feature.settings.navigation.SettingsRouter +import com.stslex.wizard.feature.settings.ui.store.SettingsStore.Action +import com.stslex.wizard.feature.settings.ui.store.SettingsStore.Event +import com.stslex.wizard.feature.settings.ui.store.SettingsStore.State + +class SettingsStoreImpl( + private val interactor: SettingsInteractor, + private val router: SettingsRouter, + appDispatcher: AppDispatcher +) : SettingsStore, BaseStore( + appDispatcher = appDispatcher, + initialState = State.INITIAL +) { + + override fun process(action: Action) { + when (action) { + Action.LogOut -> actionLogout() + Action.BackButtonClicked -> actionBackClick() + is Action.Navigation -> router(action) + } + } + + private fun actionBackClick() { + sendAction(Action.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 + ) + } + sendAction(Action.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