Skip to content

Commit

Permalink
store base components refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
stslex committed Apr 29, 2024
1 parent 6e18868 commit eb8f4fb
Show file tree
Hide file tree
Showing 10 changed files with 234 additions and 217 deletions.
21 changes: 9 additions & 12 deletions core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/BaseStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,18 @@ abstract class BaseStore<S : State, E : Event, A : Action, N : Navigation>(
private val router: Router<N>,
private val appDispatcher: AppDispatcher,
initialState: S
) : Store, StateScreenModel<S>(initialState) {
) : Store<S, E, A>, StateScreenModel<S>(initialState) {

private var _lastAction: A? = null
protected val lastAction: A?
get() = _lastAction

/**
* Flow of events that are sent to the screen.
* */
private val _event: MutableSharedFlow<E> = MutableSharedFlow()
override val event: SharedFlow<E> = _event.asSharedFlow()

protected val scope: AppCoroutineScope = AppCoroutineScopeImpl(
scope = screenModelScope,
appDispatcher = appDispatcher
Expand All @@ -42,16 +48,14 @@ abstract class BaseStore<S : State, E : Event, A : Action, N : Navigation>(
* The action is then processed.
* @param action - action to be sent
*/
fun sendAction(action: A) {
override fun sendAction(action: A) {
if (lastAction != action && action !is Action.RepeatLastAction) {
_lastAction = action
}
process(action)
}

/**
* Process the action. This method should be overridden in the child class.
*/
/** Process the action. This method should be overridden in the child class.*/
protected abstract fun process(action: A)

private fun exceptionHandler(
Expand All @@ -63,13 +67,6 @@ abstract class BaseStore<S : State, E : Event, A : Action, N : Navigation>(
}
}

private val _event: MutableSharedFlow<E> = MutableSharedFlow()

/**
* Flow of events that are sent to the screen.
* */
val event: SharedFlow<E> = _event.asSharedFlow()

/**
* Updates the state of the screen.
* @param update - function that updates the state
Expand Down
18 changes: 17 additions & 1 deletion core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/Store.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,24 @@ package com.stslex.core.ui.mvi
import androidx.compose.material3.SnackbarDuration
import androidx.compose.runtime.Stable
import com.stslex.core.ui.components.SnackbarType
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow

interface Store {
interface Store<S : Store.State, E : Store.Event, A : Store.Action> {

/** Flow of the state of the screen. */
val state: StateFlow<S>

/** Flow of events that are sent to the screen. */
val event: SharedFlow<E>

/**
* 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)

interface State

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.stslex.feature.match.domain.interactor.MatchInteractor
import com.stslex.feature.match.domain.interactor.MatchInteractorImpl
import com.stslex.feature.match.navigation.MatchRouter
import com.stslex.feature.match.navigation.MatchRouterImpl
import com.stslex.feature.match.ui.store.MatchStore
import com.stslex.feature.match.ui.store.MatchStoreImpl
import org.koin.dsl.module

val featureMatchModule = module {
Expand All @@ -19,7 +19,7 @@ val featureMatchModule = module {
}
factory<MatchRouter> { MatchRouterImpl(navigator = get()) }
factory {
MatchStore(
MatchStoreImpl(
interactor = get(),
router = get(),
appDispatcher = get(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.stslex.feature.match.navigation

import com.stslex.core.ui.mvi.Router
import com.stslex.feature.match.ui.store.MatchStoreComponent.Navigation
import com.stslex.feature.match.ui.store.MatchStore.Navigation

interface MatchRouter : Router<Navigation>
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.stslex.feature.match.navigation

import com.stslex.core.ui.navigation.AppNavigator
import com.stslex.core.ui.navigation.AppScreen
import com.stslex.feature.match.ui.store.MatchStoreComponent.Navigation
import com.stslex.feature.match.ui.store.MatchStore.Navigation

class MatchRouterImpl(
private val navigator: AppNavigator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,18 @@ import com.stslex.feature.match.ui.components.MatchScreenError
import com.stslex.feature.match.ui.components.MatchScreenShimmer
import com.stslex.feature.match.ui.store.MatchScreenState
import com.stslex.feature.match.ui.store.MatchStore
import com.stslex.feature.match.ui.store.MatchStoreComponent.Action
import com.stslex.feature.match.ui.store.MatchStoreComponent.Event
import com.stslex.feature.match.ui.store.MatchStoreComponent.State
import com.stslex.feature.match.ui.store.MatchStore.Action
import com.stslex.feature.match.ui.store.MatchStore.Event
import com.stslex.feature.match.ui.store.MatchStore.State
import com.stslex.feature.match.ui.store.MatchStoreImpl

data class MatchScreen(
private val args: MatchScreenArgs
) : Screen {

@Composable
override fun Content() {
val store = getScreenModel<MatchStore>()
val store: MatchStore = getScreenModel<MatchStoreImpl>()
LaunchedEffect(Unit) {
store.sendAction(Action.Init(args = args))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import com.stslex.core.ui.base.shimmerLoadingAnimation
import com.stslex.core.ui.theme.AppDimension
import com.stslex.feature.match.ui.model.MatchUiModel
import com.stslex.feature.match.ui.store.MatchScreenState
import com.stslex.feature.match.ui.store.MatchStoreComponent.Action
import com.stslex.feature.match.ui.store.MatchStore.Action

@OptIn(ExperimentalMaterialApi::class)
@Composable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,142 +1,73 @@
package com.stslex.feature.match.ui.store

import com.stslex.core.core.AppDispatcher
import com.stslex.core.database.store.UserStore
import com.stslex.core.ui.base.mapToAppError
import com.stslex.core.ui.mvi.BaseStore
import androidx.compose.runtime.Stable
import com.stslex.core.ui.base.paging.PagingState
import com.stslex.core.ui.mvi.Store
import com.stslex.core.ui.mvi.Store.Event.Snackbar
import com.stslex.core.ui.pager.StorePager
import com.stslex.core.ui.pager.StorePagerImpl
import com.stslex.feature.match.domain.interactor.MatchInteractor
import com.stslex.feature.match.navigation.MatchRouter
import com.stslex.core.ui.navigation.args.MatchScreenArgs
import com.stslex.feature.match.ui.model.MatchUiModel
import com.stslex.feature.match.ui.model.toUi
import com.stslex.feature.match.ui.store.MatchStoreComponent.Action
import com.stslex.feature.match.ui.store.MatchStoreComponent.Event
import com.stslex.feature.match.ui.store.MatchStoreComponent.Navigation
import com.stslex.feature.match.ui.store.MatchStoreComponent.State

// todo refactor pager state for UI https://github.com/stslex/Wizard/issues/35
// todo add base store pager binding https://github.com/stslex/Wizard/issues/36
// todo add query support for pager https://github.com/stslex/Wizard/issues/37

class MatchStore(
appDispatcher: AppDispatcher,
router: MatchRouter,
private val interactor: MatchInteractor,
private val userStore: UserStore
) : BaseStore<State, Event, Action, Navigation>(
appDispatcher = appDispatcher,
router = router,
initialState = State.INITIAL
) {

private val pager: StorePager<MatchUiModel> = StorePagerImpl(
request = { page, pageSize ->
interactor.getMatches(
uuid = state.value.uuid,
page = page,
pageSize = pageSize
)
},
scope = scope,
mapper = { it.toUi() }
)

override fun process(action: Action) {
when (action) {
is Action.Init -> actionInit(action)
is Action.LoadMore -> actionLoadMore()
is Action.OnMatchClick -> actionOnMatchClick(action)
is Action.OnRetryClick -> actionRetryClick()
is Action.Refresh -> actionRefresh()
is Action.Logout -> actionLogout()
is Action.RepeatLastAction -> actionRepeatLastAction()
}
}
import com.stslex.feature.match.ui.store.MatchStore.Action
import com.stslex.feature.match.ui.store.MatchStore.Event
import com.stslex.feature.match.ui.store.MatchStore.State

private fun actionInit(action: Action.Init) {
pager.state.launch { pagerState ->
updateState { currentState ->
currentState.copy(
pagingState = pagerState
)
}
}
pager.loadState.launch { loadState ->
updateState { currentState ->
currentState.copy(
screen = loadState.toUi()
)
}
}
pager.loadEvents.launch {
sendEvent(
Event.ShowSnackbar(Snackbar.Error("error load matches"))
)
}
pager.initialLoad()
updateState { currentState ->
currentState.copy(
isSelf = action.args.isSelf,
uuid = action.args.uuid ?: userStore.uuid,
// todo make more representative logic with store components implementation https://github.com/stslex/Wizard/issues/34
interface MatchStore : Store<State, Event, Action> {

@Stable
data class State(
val screen: MatchScreenState,
val uuid: String,
val isSelf: Boolean,
val pagingState: PagingState<MatchUiModel>
) : Store.State {

companion object {

val INITIAL = State(
screen = MatchScreenState.Shimmer,
pagingState = PagingState.default(),
uuid = "",
isSelf = false
)
}
pager.initialLoad()
}

private fun actionLoadMore() {
pager.load()
}
@Stable
sealed interface Event : Store.Event {

private fun actionOnMatchClick(action: Action.OnMatchClick) {
navigate(Navigation.MatchDetails(action.matchUuid))
data class ShowSnackbar(
val snackbar: Snackbar
) : Event
}

private fun actionRetryClick() {
pager.retry()
}
@Stable
sealed interface Action : Store.Action {

private fun actionRefresh() {
pager.refresh()
}
data class Init(
val args: MatchScreenArgs
) : Action

data object Refresh : Action

data object LoadMore : Action

data class OnMatchClick(
val matchUuid: String
) : Action

data object OnRetryClick : Action

private fun actionLogout() {
launch(
action = {
interactor.logout()
},
onSuccess = {
navigate(Navigation.LogOut)
},
onError = { error ->
val appError = error.mapToAppError("error logout")
if (state.value.screen is MatchScreenState.Content) {
sendEvent(
Event.ShowSnackbar(Snackbar.Error(appError.message))
)
} else {
updateState { currentState ->
currentState.copy(
screen = MatchScreenState.Error(appError)
)
}
}
}
)
data object Logout : Action

data object RepeatLastAction : Action, Store.Action.RepeatLastAction
}

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)
@Stable
sealed interface Navigation : Store.Navigation {

data class MatchDetails(val matchUuid: String) : Navigation

data object LogOut : Navigation
}
}
}

Loading

0 comments on commit eb8f4fb

Please sign in to comment.