diff --git a/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/BaseStore.kt b/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/BaseStore.kt index 9ed385a..e15a796 100644 --- a/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/BaseStore.kt +++ b/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/BaseStore.kt @@ -22,8 +22,9 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -abstract class BaseStore(initialState: S) : ViewModel(), - Store { +abstract class BaseStore( + initialState: S +) : ViewModel(), Store { private val _event: MutableSharedFlow = MutableSharedFlow() override val event: SharedFlow = _event.asSharedFlow() diff --git a/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/Store.kt b/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/Store.kt index 1496c78..3148cc6 100644 --- a/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/Store.kt +++ b/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/Store.kt @@ -24,7 +24,10 @@ interface Store { interface State - interface Event + interface Event { + + data class Handler(val action: Action) : Event + } interface Action { diff --git a/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/handler/BaseHandler.kt b/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/handler/BaseHandler.kt deleted file mode 100644 index d7370ea..0000000 --- a/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/handler/BaseHandler.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.stslex.wizard.core.ui.mvi.handler - -import com.stslex.wizard.core.ui.mvi.Store -import kotlinx.coroutines.flow.StateFlow - -abstract class BaseHandler( - private val store: Store -) : Handler { - - val state: StateFlow - get() = store.state - - protected fun sendAction(action: A) { - store.sendAction(action) - } - -} diff --git a/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/handler/Handler.kt b/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/handler/Handler.kt deleted file mode 100644 index 9783b90..0000000 --- a/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/handler/Handler.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.stslex.wizard.core.ui.mvi.handler - -import com.stslex.wizard.core.ui.mvi.Store -import com.stslex.wizard.core.ui.mvi.handler.Handler.Event - -fun interface Handler { - - operator fun invoke(event: E) - - interface Event : Store.Event -} diff --git a/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/store_di/StoreBeanV2.kt b/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/store_di/StoreBeanV2.kt new file mode 100644 index 0000000..b082850 --- /dev/null +++ b/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/store_di/StoreBeanV2.kt @@ -0,0 +1,36 @@ +package com.stslex.wizard.core.ui.mvi.store_di + +import com.stslex.wizard.core.ui.mvi.v2.BaseStore +import org.koin.core.definition.BeanDefinition +import org.koin.core.definition.KoinDefinition +import org.koin.core.module.Module + +inline fun > Module.storeOf( + crossinline constructor: () -> R, + noinline options: (BeanDefinition.() -> Unit)? = null, +): KoinDefinition = viewModelOf(constructor, options) + +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( + crossinline constructor: (T1, T2) -> R, + noinline options: (BeanDefinition.() -> Unit)? = null, +): KoinDefinition = viewModelOf(constructor, options) + +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( + 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( + 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/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/v2/BaseStore.kt b/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/v2/BaseStore.kt new file mode 100644 index 0000000..a36f35a --- /dev/null +++ b/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/v2/BaseStore.kt @@ -0,0 +1,115 @@ +package com.stslex.wizard.core.ui.mvi.v2 + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.stslex.wizard.core.core.AppDispatcher +import com.stslex.wizard.core.core.coroutine.AppCoroutineScope +import com.stslex.wizard.core.core.coroutine.AppCoroutineScopeImpl +import com.stslex.wizard.core.ui.mvi.Store +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.CoroutineDispatcher +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 + +open class BaseStore( + initialState: S, + private val handlers: Set> +) : ViewModel(), Store, HandlerStore { + + 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(viewModelScope) + + private var _lastAction: A? = null + override val lastAction: A? + get() = _lastAction + + @Suppress("UNCHECKED_CAST") + override fun sendAction(action: A) { + if (lastAction != action && action !is Action.RepeatLast) { + _lastAction = action + } + val handler = handlers.firstOrNull { it.checkAction(action) } as? Handler + ?: throw IllegalStateException("Handler not found for action: ${action::class.simpleName}") + handler.invoke(this, action) + } + + /** + * Updates the state of the screen. + * @param update - function that updates the state + * */ + override 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 + * */ + override fun sendEvent(event: E) { + viewModelScope.launch { _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 + * */ + override fun launch( + onError: suspend (Throwable) -> Unit, + onSuccess: suspend CoroutineScope.(T) -> Unit, + workDispatcher: CoroutineDispatcher, + eachDispatcher: CoroutineDispatcher, + action: suspend CoroutineScope.() -> T, + ) = scope.launch( + onError = onError, + workDispatcher = workDispatcher, + eachDispatcher = eachDispatcher, + 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 + * */ + override fun Flow.launch( + onError: suspend (cause: Throwable) -> Unit, + workDispatcher: CoroutineDispatcher, + eachDispatcher: CoroutineDispatcher, + each: suspend (T) -> Unit + ): Job = scope.launch( + flow = this, + workDispatcher = workDispatcher, + eachDispatcher = eachDispatcher, + onError = onError, + each = each, + ) + + +} \ No newline at end of file diff --git a/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/v2/BaseStoreImpl.kt b/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/v2/BaseStoreImpl.kt new file mode 100644 index 0000000..6d175c6 --- /dev/null +++ b/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/v2/BaseStoreImpl.kt @@ -0,0 +1,13 @@ +package com.stslex.wizard.core.ui.mvi.v2 + +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 + +internal class BaseStoreImpl( + initialState: S, + handlers: Set> +) : BaseStore( + initialState = initialState, + handlers = handlers +) \ No newline at end of file diff --git a/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/v2/Handler.kt b/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/v2/Handler.kt new file mode 100644 index 0000000..3bf7837 --- /dev/null +++ b/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/v2/Handler.kt @@ -0,0 +1,28 @@ +package com.stslex.wizard.core.ui.mvi.v2 + +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 kotlin.reflect.KClass + +abstract class Handler(val actionKClass: KClass<*>) { + + val handlerName: String = requireNotNull(actionKClass.simpleName) { + "Action class name is null" + } + + inline fun checkAction(action: StoreAction): Boolean = actionKClass.isInstance(action) + + abstract fun HandlerStore.invoke(action: A) + + override fun toString(): String = handlerName + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Handler<*, *, *, *>) return false + return handlerName == other.handlerName + } + + override fun hashCode(): Int = handlerName.hashCode() + +} diff --git a/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/v2/HandlerStore.kt b/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/v2/HandlerStore.kt new file mode 100644 index 0000000..cb29e00 --- /dev/null +++ b/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/v2/HandlerStore.kt @@ -0,0 +1,58 @@ +package com.stslex.wizard.core.ui.mvi.v2 + +import com.stslex.wizard.core.core.AppDispatcher +import com.stslex.wizard.core.core.AppDispatcherImpl +import com.stslex.wizard.core.ui.mvi.Store +import com.stslex.wizard.core.ui.mvi.Store.Event +import com.stslex.wizard.core.ui.mvi.Store.State +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +interface HandlerStore { + + val state: StateFlow + + val lastAction: A? + + fun sendEvent(event: E) + + fun sendAction(action: A) + + fun updateState(update: (S) -> S) + + /** + * 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 + * */ + fun launch( + onError: suspend (Throwable) -> Unit = {}, + onSuccess: suspend CoroutineScope.(T) -> Unit = {}, + workDispatcher: CoroutineDispatcher = AppDispatcherImpl.default, + eachDispatcher: CoroutineDispatcher = AppDispatcherImpl.main.immediate, + action: suspend CoroutineScope.() -> T, + ): Job + + /** + * 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 + * */ + fun Flow.launch( + onError: suspend (cause: Throwable) -> Unit = {}, + workDispatcher: CoroutineDispatcher = AppDispatcherImpl.default, + eachDispatcher: CoroutineDispatcher = AppDispatcherImpl.main.immediate, + each: suspend (T) -> Unit + ): Job +} \ No newline at end of file diff --git a/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/v2/StoreExt.kt b/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/v2/StoreExt.kt new file mode 100644 index 0000000..c6040c4 --- /dev/null +++ b/core/ui/mvi/src/commonMain/kotlin/com/stslex/wizard/core/ui/mvi/v2/StoreExt.kt @@ -0,0 +1,30 @@ +package com.stslex.wizard.core.ui.mvi.v2 + +import com.stslex.wizard.core.ui.mvi.Store +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 + +@Suppress("UNCHECKED_CAST") +fun > store( + initialState: S, + handlers: Set> +): TStore = BaseStoreImpl(initialState, handlers) as TStore + +fun Handler.invoke( + store: HandlerStore, + action: A +) { + with(store) { + this.invoke(action) + } +} + +inline fun handler( + crossinline block: HandlerStore.(action: A) -> Unit +) = object : Handler(A::class) { + + override fun HandlerStore.invoke(action: A) { + block(action) + } +} 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 db98cf3..78fc82e 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 @@ -6,8 +6,6 @@ import com.stslex.wizard.feature.profile.data.repository.ProfileRepository import com.stslex.wizard.feature.profile.data.repository.ProfileRepositoryImpl import com.stslex.wizard.feature.profile.domain.interactor.ProfileInteractor 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 @@ -20,7 +18,6 @@ class ModuleFeatureProfile : AppModule() { override fun declaration(): ModuleDeclaration = { 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/mvi/ClickersHandler.kt b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/mvi/ClickersHandler.kt new file mode 100644 index 0000000..3fc9911 --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/mvi/ClickersHandler.kt @@ -0,0 +1,21 @@ +package com.stslex.wizard.feature.profile.mvi + +import com.stslex.wizard.core.ui.mvi.v2.Handler +import com.stslex.wizard.core.ui.mvi.v2.HandlerStore +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Action +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Action.Click +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Event +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.State + +class ClickersHandler : Handler(Click::class) { + + override fun HandlerStore.invoke(action: Click) { + when (action) { + Click.BackButtonClick -> sendAction(Action.Navigation.Back) + Click.FavouriteClick -> sendAction(Action.Navigation.Favourite(state.value.uuid)) + Click.FollowersClick -> sendAction(Action.Navigation.Followers(state.value.uuid)) + Click.FollowingClick -> sendAction(Action.Navigation.Following(state.value.uuid)) + Click.SettingsClick -> sendAction(Action.Navigation.Settings) + } + } +} \ No newline at end of file diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/mvi/InitStorageHandler.kt b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/mvi/InitStorageHandler.kt new file mode 100644 index 0000000..7a70763 --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/mvi/InitStorageHandler.kt @@ -0,0 +1,60 @@ +package com.stslex.wizard.feature.profile.mvi + +import androidx.compose.ui.graphics.Color +import com.stslex.wizard.core.database.store.UserStore +import com.stslex.wizard.core.navigation.Screen +import com.stslex.wizard.core.ui.mvi.v2.Handler +import com.stslex.wizard.core.ui.mvi.v2.HandlerStore +import com.stslex.wizard.feature.profile.domain.interactor.ProfileInteractor +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.ProfileScreenState +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Action +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Action.Init +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Event +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.State + +class InitStorageHandler( + private val interactor: ProfileInteractor, + private val userStore: UserStore, +) : Handler(Init::class) { + + override fun HandlerStore.invoke(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) + ) + } + } + } +} \ No newline at end of file diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/mvi/LogoutHandler.kt b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/mvi/LogoutHandler.kt new file mode 100644 index 0000000..3a6d81e --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/mvi/LogoutHandler.kt @@ -0,0 +1,44 @@ +package com.stslex.wizard.feature.profile.mvi + +import com.stslex.wizard.core.ui.mvi.CommonEvents.Snackbar +import com.stslex.wizard.core.ui.mvi.v2.Handler +import com.stslex.wizard.core.ui.mvi.v2.HandlerStore +import com.stslex.wizard.feature.profile.domain.interactor.ProfileInteractor +import com.stslex.wizard.feature.profile.ui.store.ProfileScreenState +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 LogoutHandler( + private val interactor: ProfileInteractor, +) : Handler(Action.Logout::class) { + + override fun HandlerStore.invoke(action: Action.Logout) { + 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"))) + } + ) + } +} \ No newline at end of file diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/mvi/NavigationHandler.kt b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/mvi/NavigationHandler.kt new file mode 100644 index 0000000..231be6c --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/mvi/NavigationHandler.kt @@ -0,0 +1,29 @@ +package com.stslex.wizard.feature.profile.mvi + +import com.stslex.wizard.core.navigation.Screen +import com.stslex.wizard.core.navigation.Screen.Follower.FollowerType.FOLLOWER +import com.stslex.wizard.core.navigation.Screen.Follower.FollowerType.FOLLOWING +import com.stslex.wizard.core.navigation.navigator.Navigator +import com.stslex.wizard.core.ui.mvi.v2.Handler +import com.stslex.wizard.core.ui.mvi.v2.HandlerStore +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Action +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Action.Navigation +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Event +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.State + +class NavigationHandler( + private val navigator: Navigator +) : Handler(Navigation::class) { + + override fun HandlerStore.invoke(action: Navigation) { + when (action) { + Navigation.LogIn -> navigator.navTo(Screen.Auth) + Navigation.Back -> navigator.popBack() + Navigation.Settings -> navigator.navTo(Screen.Settings) + is Navigation.Favourite -> navigator.navTo(Screen.Favourite(action.uuid)) + is Navigation.Following -> navigator.navTo(Screen.Follower(FOLLOWING, action.uuid)) + is Navigation.Followers -> navigator.navTo(Screen.Follower(FOLLOWER, action.uuid)) + } + } + +} \ No newline at end of file diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/mvi/RepeatLastActionHandler.kt b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/mvi/RepeatLastActionHandler.kt new file mode 100644 index 0000000..54e9dc4 --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/mvi/RepeatLastActionHandler.kt @@ -0,0 +1,26 @@ +package com.stslex.wizard.feature.profile.mvi + + +import com.stslex.wizard.core.ui.mvi.v2.Handler +import com.stslex.wizard.core.ui.mvi.v2.HandlerStore +import com.stslex.wizard.feature.profile.ui.store.ProfileScreenState +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Action +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Action.RepeatLastAction +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.Event +import com.stslex.wizard.feature.profile.ui.store.ProfileStore.State + +class RepeatLastActionHandler : + Handler(RepeatLastAction::class) { + + override fun HandlerStore.invoke(action: RepeatLastAction) { + 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) + } + sendAction(lastAction) + } +} \ No newline at end of file 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 296e7ac..26c200b 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.store_di.getStore import com.stslex.wizard.feature.profile.ui.ProfileScreen -import com.stslex.wizard.feature.profile.ui.store.ProfileStoreImpl +import com.stslex.wizard.feature.profile.ui.store.ProfileStore 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 deleted file mode 100644 index 8f7d899..0000000 --- a/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/navigation/ProfileRouter.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.stslex.wizard.feature.profile.navigation - -import com.stslex.wizard.core.ui.mvi.Router -import com.stslex.wizard.feature.profile.ui.store.ProfileStore - -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 deleted file mode 100644 index 21fc5ef..0000000 --- a/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/navigation/ProfileRouterImpl.kt +++ /dev/null @@ -1,39 +0,0 @@ -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.ProfileStore.Action.Navigation - -class ProfileRouterImpl( - private val navigator: Navigator -) : ProfileRouter { - - override fun invoke(event: Navigation) { - when (event) { - Navigation.LogIn -> navigator.navTo(Screen.Auth) - Navigation.Back -> navigator.popBack() - Navigation.Settings -> navigator.navTo(Screen.Settings) - is Navigation.Favourite -> navigator.navTo(Screen.Favourite(uuid = event.uuid)) - is Navigation.Following -> navToFollowing(event) - is Navigation.Followers -> navToFollower(event) - } - } - - private fun navToFollower(event: Navigation.Followers) { - navigator.navTo( - Screen.Follower( - type = Screen.Follower.FollowerType.FOLLOWER, - uuid = event.uuid - ) - ) - } - - private fun navToFollowing(event: Navigation.Following) { - navigator.navTo( - Screen.Follower( - type = Screen.Follower.FollowerType.FOLLOWING, - uuid = event.uuid - ) - ) - } -} \ No newline at end of file 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 c3b7828..05e69ed 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 @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.SnackbarHostState @@ -31,7 +32,9 @@ internal fun ProfileScreen( modifier: Modifier = Modifier, ) { BoxWithConstraints( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .systemBarsPadding(), ) { when (val screen = state.screen) { is ProfileScreenState.Content -> ProfileScreenContent( 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 1a8961f..65c713c 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 @@ -19,8 +19,8 @@ internal fun ProfileScreenContent( ProfileScreenToolbar( nickname = state.data.username, isCurrentUser = state.data.isCurrentUser, - onSettingsClick = { onAction(Action.SettingsClick) }, - onBackClick = { onAction(Action.BackButtonClick) } + onSettingsClick = { onAction(Action.Click.SettingsClick) }, + onBackClick = { onAction(Action.Click.BackButtonClick) } ) ProfileAvatar(avatar = state.data.avatar) @@ -30,9 +30,9 @@ internal fun ProfileScreenContent( favouriteCount = state.data.favouriteCount, followingCount = state.data.following, followersCount = state.data.followers, - onFavouriteClick = { onAction(Action.FavouriteClick) }, - onFollowingClick = { onAction(Action.FollowingClick) }, - onFollowersClick = { onAction(Action.FollowersClick) }, + onFavouriteClick = { onAction(Action.Click.FavouriteClick) }, + onFollowingClick = { onAction(Action.Click.FollowingClick) }, + onFollowersClick = { onAction(Action.Click.FollowersClick) }, ) } 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 fe7e868..d7c88b7 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 @@ -40,18 +40,20 @@ interface ProfileStore : Store { data object RepeatLastAction : Action, Store.Action.RepeatLast - data object FavouriteClick : Action + sealed interface Click : Action { - data object FollowingClick : Action + data object FavouriteClick : Click - data object FollowersClick : Action + data object FollowingClick : Click - data object SettingsClick : Action + data object FollowersClick : Click - data object BackButtonClick : Action + data object SettingsClick : Click + data object BackButtonClick : Click + } - sealed interface Navigation : Action, Store.Action.Navigation { + sealed interface Navigation : Action { data object LogIn : Navigation @@ -78,5 +80,6 @@ interface ProfileStore : Store { @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/ProfileStoreImpl.kt b/feature/profile/src/commonMain/kotlin/com/stslex/wizard/feature/profile/ui/store/ProfileStoreImpl.kt index 09d69e2..0e522a4 100644 --- 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 @@ -1,135 +1,29 @@ package com.stslex.wizard.feature.profile.ui.store -import androidx.compose.ui.graphics.Color 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.core.navigation.navigator.Navigator +import com.stslex.wizard.core.ui.mvi.v2.BaseStore 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.mvi.ClickersHandler +import com.stslex.wizard.feature.profile.mvi.InitStorageHandler +import com.stslex.wizard.feature.profile.mvi.LogoutHandler +import com.stslex.wizard.feature.profile.mvi.NavigationHandler +import com.stslex.wizard.feature.profile.mvi.RepeatLastActionHandler 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, -) : ProfileStore, BaseStore(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) - } -} + interactor: ProfileInteractor, + userStore: UserStore, + navigator: Navigator +) : ProfileStore, BaseStore( + initialState = State.INITIAL, + handlers = setOf( + InitStorageHandler(interactor, userStore), + LogoutHandler(interactor), + RepeatLastActionHandler(), + ClickersHandler(), + NavigationHandler(navigator) + ) +)