Created
February 27, 2024 16:29
-
-
Save electrolobzik/8976a32c31b79ff638e4d7ad6c10eb34 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Created by Roman Chernyak (aka @electrolobzik) on 2024-02-22 | |
*/ | |
class Paginator<Data : Any, Cursor : Any>( | |
coroutineContext: CoroutineContext | |
) : CoroutineScope by CoroutineScope(coroutineContext) { | |
private val _state = MutableStateFlow<State<Data, Cursor>>(State.NoData.Empty()) | |
private val _sideEffects = MutableSharedFlow<SideEffect<Cursor>>() | |
private val inputActions = MutableSharedFlow<Action<Data, Cursor>>() | |
val state: StateFlow<State<Data, Cursor>> = _state | |
val sideEffects: Flow<SideEffect<Cursor>> = _sideEffects | |
init { | |
launch { | |
inputActions.collect { | |
handleAction(it) | |
} | |
} | |
} | |
fun sendAction(action: Action<Data, Cursor>) { | |
launch { | |
inputActions.emit(action) | |
} | |
} | |
private suspend fun handleAction(action: Action<Data, Cursor>) { | |
val currentState = state.value | |
log("==> handleAction: action: ${action::class.simpleName}, current state = ${currentState::class.simpleName}") | |
val newState = reduce(action, currentState) | |
val sideEffect = getSideEffect(action, currentState) | |
log(" New state: $newState") | |
if (newState != state) { | |
_state.emit(newState) | |
} | |
log(" Side effect = $sideEffect") | |
if (sideEffect != null) { | |
_sideEffects.emit(sideEffect) | |
} | |
log("<== handleAction: old state: ${currentState::class.simpleName}, new state = ${newState::class.simpleName}") | |
} | |
sealed interface State<Data, Cursor> { | |
sealed interface NoData<Data, Cursor> : State<Data, Cursor> { | |
class Empty<Data, Cursor> : NoData<Data, Cursor> | |
class EmptyLoading<Data, Cursor> : NoData<Data, Cursor> | |
data class EmptyError<Data, Cursor>(val error: LoadingError) : NoData<Data, Cursor> | |
} | |
sealed interface WithData<Data, Cursor> : State<Data, Cursor> { | |
val lastPage: Cursor | |
val data: List<Data> | |
data class Data<Data, Cursor>(override val lastPage: Cursor, override val data: List<Data>) : WithData<Data, Cursor> | |
data class DataAndRefreshInProgress<Data, Cursor>(override val lastPage: Cursor, override val data: List<Data>) : WithData<Data, Cursor> | |
data class DataAndNewPageInProgress<Data, Cursor>(override val lastPage: Cursor, override val data: List<Data>) : WithData<Data, Cursor> | |
data class FullData<Data, Cursor>(override val lastPage: Cursor, override val data: List<Data>) : WithData<Data, Cursor> | |
data class FullDataAndRefreshInProgress<Data, Cursor>(override val lastPage: Cursor, override val data: List<Data>) : | |
WithData<Data, Cursor> | |
} | |
fun toExternalState(): DataState<Data> = when (this) { | |
is NoData.Empty -> DataState.NoData.Empty() | |
is NoData.EmptyError -> DataState.NoData.EmptyError(error) | |
is NoData.EmptyLoading -> DataState.NoData.EmptyLoading() | |
is WithData.Data -> DataState.WithData.Data(data) | |
is WithData.DataAndNewPageInProgress -> DataState.WithData.DataAndNewPageInProgress(data) | |
is WithData.DataAndRefreshInProgress -> DataState.WithData.DataAndRefreshInProgress(data) | |
is WithData.FullData -> DataState.WithData.FullData(data) | |
is WithData.FullDataAndRefreshInProgress -> DataState.WithData.FullDataAndRefreshInProgress(data) | |
} | |
} | |
sealed interface Action<Data, Cursor> { | |
class InitialLoad<Data, Cursor> : Action<Data, Cursor> | |
class Refresh<Data, Cursor> : Action<Data, Cursor> | |
class Restart<Data, Cursor> : Action<Data, Cursor> | |
class LoadMore<Data, Cursor> : Action<Data, Cursor> | |
data class InitialDataLoaded<Data, Cursor>(val lastPage: Cursor, val items: List<Data>) : Action<Data, Cursor> | |
data class InitialDataLoadingError<Data, Cursor>(val error: LoadingError) : Action<Data, Cursor> | |
data class NewPageLoaded<Data, Cursor>(val lastPage: Cursor, val items: List<Data>) : Action<Data, Cursor> | |
data class NewPageLoadingError<Data, Cursor>(val error: LoadingError) : Action<Data, Cursor> | |
} | |
sealed interface SideEffect<Cursor> { | |
class NeedToPerformInitialLoad<Cursor> : SideEffect<Cursor> | |
data class NeedToPerformRefresh<Cursor>(val lastPage: Cursor?) : SideEffect<Cursor> | |
data class NeedToLoadNewPage<Cursor>(val lastPage: Cursor) : SideEffect<Cursor> | |
data class NewPageLoadErrorEvent<Cursor>(val error: LoadingError) : SideEffect<Cursor> | |
} | |
private fun reduce( | |
action: Action<Data, Cursor>, | |
state: State<Data, Cursor> | |
): State<Data, Cursor> { | |
fun getIllegalStateException(): IllegalStateException = IllegalStateException("Can't perform action $action in state: $state") | |
val resultState: State<Data, Cursor> = when (action) { | |
is Action.InitialLoad -> { | |
when (state) { | |
is State.WithData.Data<Data, Cursor>, | |
is State.WithData.FullData<Data, Cursor>, | |
is State.WithData.DataAndNewPageInProgress<Data, Cursor>, | |
is State.WithData.DataAndRefreshInProgress<Data, Cursor>, | |
is State.WithData.FullDataAndRefreshInProgress<Data, Cursor> -> throw getIllegalStateException() | |
is State.NoData.Empty, | |
is State.NoData.EmptyError<Data, Cursor> -> State.NoData.EmptyLoading() | |
is State.NoData.EmptyLoading<Data, Cursor> -> state | |
} | |
} | |
is Action.InitialDataLoaded<Data, Cursor> -> { | |
when (state) { | |
is State.NoData.EmptyError<Data, Cursor>, | |
is State.WithData.FullData<Data, Cursor>, | |
is State.WithData.DataAndNewPageInProgress<Data, Cursor>, | |
is State.WithData.DataAndRefreshInProgress<Data, Cursor>, | |
is State.WithData.FullDataAndRefreshInProgress<Data, Cursor> -> throw getIllegalStateException() | |
// data can be loaded several times (from cache and from remote) | |
is State.WithData.Data<Data, Cursor>, | |
is State.NoData.Empty<Data, Cursor>, | |
is State.NoData.EmptyLoading<Data, Cursor> -> { | |
if (action.items.isEmpty()) { | |
State.NoData.Empty() | |
} else { | |
State.WithData.Data(lastPage = action.lastPage, data = action.items) | |
} | |
} | |
} | |
} | |
is Action.InitialDataLoadingError -> { | |
when (state) { | |
is State.NoData.Empty, | |
is State.NoData.EmptyError, | |
is State.WithData.Data<Data, Cursor>, | |
is State.WithData.FullData<Data, Cursor>, | |
is State.WithData.DataAndNewPageInProgress<Data, Cursor>, | |
is State.WithData.DataAndRefreshInProgress<Data, Cursor>, | |
is State.WithData.FullDataAndRefreshInProgress<Data, Cursor> -> throw getIllegalStateException() | |
is State.NoData.EmptyLoading -> State.NoData.EmptyError(action.error) | |
} | |
} | |
is Action.Refresh -> { | |
when (state) { | |
is State.NoData.Empty, | |
is State.NoData.EmptyError -> State.NoData.EmptyLoading() | |
is State.NoData.EmptyLoading, | |
is State.WithData.DataAndRefreshInProgress, | |
is State.WithData.FullDataAndRefreshInProgress -> state | |
is State.WithData.Data -> State.WithData.DataAndRefreshInProgress(state.lastPage, state.data) | |
is State.WithData.DataAndNewPageInProgress -> State.WithData.DataAndRefreshInProgress(state.lastPage, state.data) | |
is State.WithData.FullData -> State.WithData.FullDataAndRefreshInProgress(state.lastPage, state.data) | |
} | |
} | |
is Action.Restart -> { | |
when (state) { | |
is State.NoData.Empty, | |
is State.NoData.EmptyError, | |
is State.WithData.Data, | |
is State.WithData.DataAndNewPageInProgress, | |
is State.WithData.FullData -> State.NoData.EmptyLoading() | |
is State.NoData.EmptyLoading -> state | |
is State.WithData.DataAndRefreshInProgress<Data, Cursor>, | |
is State.WithData.FullDataAndRefreshInProgress<Data, Cursor> -> State.NoData.EmptyLoading() | |
} | |
} | |
is Action.LoadMore -> { | |
when (state) { | |
is State.NoData.Empty, | |
is State.NoData.EmptyError, | |
is State.WithData.FullData, | |
is State.NoData.EmptyLoading -> throw getIllegalStateException() | |
is State.WithData.DataAndNewPageInProgress -> state | |
is State.WithData.DataAndRefreshInProgress -> state | |
is State.WithData.FullDataAndRefreshInProgress<Data, Cursor> -> state | |
is State.WithData.Data<Data, Cursor> -> State.WithData.DataAndNewPageInProgress(state.lastPage, state.data) | |
} | |
} | |
is Action.NewPageLoaded<Data, Cursor> -> { | |
val items = action.items | |
when (state) { | |
is State.NoData.Empty, | |
is State.NoData.EmptyError, | |
is State.WithData.FullData, | |
is State.NoData.EmptyLoading, | |
is State.WithData.FullDataAndRefreshInProgress, | |
is State.WithData.Data -> throw getIllegalStateException() | |
is State.WithData.DataAndRefreshInProgress -> { | |
if (items.isEmpty()) { | |
State.WithData.FullDataAndRefreshInProgress(state.lastPage, state.data) | |
} else { | |
State.WithData.DataAndRefreshInProgress(action.lastPage, state.data + items) | |
} | |
} | |
is State.WithData.DataAndNewPageInProgress -> { | |
if (items.isEmpty()) { | |
State.WithData.FullData(state.lastPage, state.data) | |
} else { | |
State.WithData.Data(action.lastPage, state.data + items) | |
} | |
} | |
} | |
} | |
is Action.NewPageLoadingError -> { | |
when (state) { | |
is State.NoData.Empty, | |
is State.NoData.EmptyError, | |
is State.WithData.FullData, | |
is State.NoData.EmptyLoading, | |
is State.WithData.FullDataAndRefreshInProgress, | |
is State.WithData.Data -> throw getIllegalStateException() | |
is State.WithData.DataAndRefreshInProgress -> State.WithData.DataAndRefreshInProgress(state.lastPage, state.data) | |
is State.WithData.DataAndNewPageInProgress -> State.WithData.Data(state.lastPage, state.data) | |
} | |
} | |
} | |
return resultState | |
} | |
private fun getSideEffect( | |
action: Action<Data, Cursor>, | |
state: State<Data, Cursor> | |
): SideEffect<Cursor>? { | |
fun getIllegalStateException(): IllegalStateException = IllegalStateException("Can't perform action $action in state: $state") | |
return when (action) { | |
is Action.InitialLoad -> { | |
when (state) { | |
is State.NoData.Empty, | |
is State.NoData.EmptyError<Data, Cursor> -> SideEffect.NeedToPerformInitialLoad() | |
is State.NoData.EmptyLoading, | |
is State.WithData.Data, | |
is State.WithData.DataAndNewPageInProgress, | |
is State.WithData.DataAndRefreshInProgress, | |
is State.WithData.FullData, | |
is State.WithData.FullDataAndRefreshInProgress -> null | |
} | |
} | |
is Action.InitialDataLoaded<Data, Cursor> -> null | |
is Action.InitialDataLoadingError -> null | |
is Action.Refresh -> { | |
when (state) { | |
is State.NoData.Empty, | |
is State.NoData.EmptyError -> SideEffect.NeedToPerformRefresh(null) | |
is State.NoData.EmptyLoading, | |
is State.WithData.DataAndRefreshInProgress, | |
is State.WithData.FullDataAndRefreshInProgress -> null | |
is State.WithData.Data -> SideEffect.NeedToPerformRefresh(state.lastPage) | |
is State.WithData.DataAndNewPageInProgress -> SideEffect.NeedToPerformRefresh(state.lastPage) | |
is State.WithData.FullData -> SideEffect.NeedToPerformRefresh(state.lastPage) | |
} | |
} | |
is Action.Restart -> { | |
when (state) { | |
is State.NoData.Empty, | |
is State.NoData.EmptyError, | |
is State.WithData.Data, | |
is State.WithData.DataAndNewPageInProgress, | |
is State.WithData.FullData -> SideEffect.NeedToPerformInitialLoad() | |
is State.NoData.EmptyLoading, | |
is State.WithData.DataAndRefreshInProgress<Data, Cursor>, | |
is State.WithData.FullDataAndRefreshInProgress<Data, Cursor> -> null | |
} | |
} | |
is Action.LoadMore -> { | |
when (state) { | |
is State.NoData.Empty, | |
is State.NoData.EmptyError, | |
is State.WithData.FullData, | |
is State.NoData.EmptyLoading, | |
is State.WithData.DataAndNewPageInProgress -> null | |
is State.WithData.DataAndRefreshInProgress -> SideEffect.NeedToLoadNewPage(state.lastPage) | |
is State.WithData.FullDataAndRefreshInProgress<Data, Cursor> -> SideEffect.NeedToLoadNewPage(state.lastPage) | |
is State.WithData.Data<Data, Cursor> -> SideEffect.NeedToLoadNewPage(state.lastPage) | |
} | |
} | |
is Action.NewPageLoaded<Data, Cursor> -> null | |
is Action.NewPageLoadingError -> { | |
when (state) { | |
is State.NoData.Empty, | |
is State.NoData.EmptyError, | |
is State.WithData.FullData, | |
is State.NoData.EmptyLoading, | |
is State.WithData.FullDataAndRefreshInProgress, | |
is State.WithData.Data -> null | |
is State.WithData.DataAndRefreshInProgress -> SideEffect.NewPageLoadErrorEvent(action.error) | |
is State.WithData.DataAndNewPageInProgress -> SideEffect.NewPageLoadErrorEvent(action.error) | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment