Last active
April 24, 2023 18:36
-
-
Save hoc081098/c54154a6eba4fdbe87e574a9b27d1d64 to your computer and use it in GitHub Desktop.
flow_redux.kt
This file contains hidden or 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
// ----------------------------------------------------------------------------- Domain | |
interface HomeRepository { | |
suspend fun fetchData(): String | |
suspend fun syncData() | |
fun observeData(): Flow<String> | |
} | |
// -----------------------------------------------------------------------------Demo: Home screen UI | |
sealed interface HomeAction { | |
object FetchData : HomeAction | |
object RetryFetchData : HomeAction | |
} | |
sealed interface HomeState { | |
object Loading : HomeState | |
data class Success( | |
val data: String | |
) : HomeState | |
data class Error( | |
val errorMessage: String | |
) : HomeState | |
} | |
// Fetch | |
private sealed interface FetchDataSideEffectAction : HomeAction { | |
object Loading : FetchDataSideEffectAction | |
data class Success(val data: String) : FetchDataSideEffectAction | |
data class Error(val errorMessage: String) : FetchDataSideEffectAction | |
} | |
// Retry | |
private sealed interface RetryFetchDataSideEffectAction : HomeAction { | |
object Loading : RetryFetchDataSideEffectAction | |
data class Success(val data: String) : RetryFetchDataSideEffectAction | |
data class Error(val errorMessage: String) : RetryFetchDataSideEffectAction | |
} | |
// Another action | |
sealed interface StartXXXSideEffectAction : HomeAction { | |
object Start : StartXXXSideEffectAction | |
data class Tick(val time: Long) : StartXXXSideEffectAction | |
object Stop : StartXXXSideEffectAction | |
} | |
data class DataChangedSideEffectAction(val data: String) : HomeAction | |
class FetchDataSideEffect( | |
private val repository: HomeRepository | |
) : SideEffect<HomeAction, HomeState> { | |
// handle action HomeAction.FetchData | |
// Flow<FetchData> -> Flow<FetchDataSideEffectAction> | |
override fun invoke( | |
actionFlow: Flow<HomeAction>, | |
stateFlow: StateFlow<HomeState>, | |
coroutineScope: CoroutineScope | |
): Flow<HomeAction> = actionFlow | |
.filterIsInstance<HomeAction.FetchData>() | |
.flatMapFirst { | |
flowFromSuspend { repository.fetchData() } | |
.map { FetchDataSideEffectAction.Success(it) } | |
.startWith(FetchDataSideEffectAction.Loading) | |
.catch { emit(FetchDataSideEffectAction.Error(it.message.orEmpty())) } | |
} | |
} | |
class RetryFetchDataSideEffect( | |
private val repository: HomeRepository | |
) : SideEffect<HomeAction, HomeState> { | |
// handle action HomeAction.RetryFetchData | |
// Flow<RetryFetchData> -> Flow<RetryFetchDataSideEffectAction> | |
override fun invoke( | |
actionFlow: Flow<HomeAction>, | |
stateFlow: StateFlow<HomeState>, | |
coroutineScope: CoroutineScope | |
): Flow<HomeAction> = actionFlow | |
.filterIsInstance<HomeAction.RetryFetchData>() | |
.flatMapFirst { | |
defer { | |
// only retry if we are in error state | |
if (stateFlow.value is HomeState.Error) { | |
flowFromSuspend { repository.fetchData() } | |
.map { RetryFetchDataSideEffectAction.Success(it) } | |
.startWith(RetryFetchDataSideEffectAction.Loading) | |
.catch { emit(RetryFetchDataSideEffectAction.Error(it.message.orEmpty())) } | |
} else { | |
emptyFlow() | |
} | |
} | |
} | |
} | |
class DispatchAnotherActionOnRetrySuccessSideEffect( | |
private val repository: HomeRepository | |
) : SideEffect<HomeAction, HomeState> { | |
override fun invoke( | |
actionFlow: Flow<HomeAction>, | |
stateFlow: StateFlow<HomeState>, | |
coroutineScope: CoroutineScope | |
): Flow<HomeAction> { | |
val shared = actionFlow.shareIn(coroutineScope, SharingStarted.WhileSubscribed()) | |
return shared | |
.filterIsInstance<RetryFetchDataSideEffectAction.Success>() | |
.mapTo(StartXXXSideEffectAction.Start) | |
.flatMapLatest { | |
interval(Duration.ZERO, 1.seconds) | |
.map { StartXXXSideEffectAction.Tick(it) } | |
.takeUntil(shared.filterIsInstance<StartXXXSideEffectAction.Stop>()) | |
} | |
} | |
} | |
class ReactToStateSideEffect( | |
) : SideEffect<HomeAction, HomeState> { | |
override fun invoke( | |
actionFlow: Flow<HomeAction>, | |
stateFlow: StateFlow<HomeState>, | |
coroutineScope: CoroutineScope | |
): Flow<HomeAction> { | |
stateFlow | |
.filterIsInstance<HomeState.Error>() | |
.onEach { | |
// log to crashlytics | |
println("Error: ${it.errorMessage}") | |
} | |
.launchIn(coroutineScope) | |
return emptyFlow() | |
} | |
} | |
class CallApiOnCreateSideEffect( | |
private val repository: HomeRepository | |
) : SideEffect<HomeAction, HomeState> { | |
override fun invoke( | |
actionFlow: Flow<HomeAction>, | |
stateFlow: StateFlow<HomeState>, | |
coroutineScope: CoroutineScope | |
): Flow<HomeAction> { | |
coroutineScope.launch { | |
repository.syncData() | |
} | |
return emptyFlow() | |
} | |
} | |
class ObserveDataSideEffect(val fakeRepo: HomeRepository) : | |
SideEffect<HomeAction, HomeState> { | |
override fun invoke( | |
actionFlow: Flow<HomeAction>, | |
stateFlow: StateFlow<HomeState>, | |
coroutineScope: CoroutineScope | |
): Flow<HomeAction> = fakeRepo | |
.observeData() | |
.map(::DataChangedSideEffectAction) | |
} | |
sealed interface HomeSingleEvent { | |
object ShowToast : HomeSingleEvent | |
object NavigateToXXX : HomeSingleEvent | |
} | |
fun main() { | |
val fakeRepo = object : HomeRepository { | |
override suspend fun fetchData(): String { | |
TODO("Not yet implemented") | |
} | |
override suspend fun syncData() { | |
TODO("Not yet implemented") | |
} | |
override fun observeData(): Flow<String> { | |
TODO("Not yet implemented") | |
} | |
} | |
val scope = CoroutineScope(Dispatchers.Default) | |
val (outputSideEffect: SideEffect<HomeAction, HomeState>, receiveChannel: ReceiveChannel<HomeSingleEvent>) = allActionsToOutputChannelSideEffect<HomeAction, HomeState, HomeSingleEvent> { action -> | |
when (action) { | |
is FetchDataSideEffectAction.Error -> HomeSingleEvent.ShowToast | |
is FetchDataSideEffectAction.Success -> HomeSingleEvent.NavigateToXXX | |
else -> null | |
} | |
} | |
val store = scope.createFlowReduxStore<HomeAction, HomeState>( | |
initialState = HomeState.Loading, | |
sideEffects = listOf( | |
// handle user action | |
FetchDataSideEffect(fakeRepo), | |
RetryFetchDataSideEffect(fakeRepo), | |
// react to side effect action | |
DispatchAnotherActionOnRetrySuccessSideEffect(fakeRepo), | |
// react to state flow | |
ReactToStateSideEffect(), | |
// call api on create (not care about actions & state) | |
CallApiOnCreateSideEffect(fakeRepo), | |
// observe data (not care about actions & state) | |
ObserveDataSideEffect(fakeRepo), | |
// handle single event | |
outputSideEffect, | |
), | |
reducer = { state, action -> | |
when (action) { | |
// user action -> returns previous state | |
HomeAction.FetchData -> state | |
HomeAction.RetryFetchData -> state | |
// FetchDataSideEffectAction -> returns new state | |
is FetchDataSideEffectAction.Error -> HomeState.Error(action.errorMessage) | |
FetchDataSideEffectAction.Loading -> HomeState.Loading | |
is FetchDataSideEffectAction.Success -> HomeState.Success(data = action.data) | |
// RetryFetchDataSideEffectAction -> returns new state | |
is RetryFetchDataSideEffectAction.Error -> HomeState.Error(action.errorMessage) | |
RetryFetchDataSideEffectAction.Loading -> HomeState.Loading | |
is RetryFetchDataSideEffectAction.Success -> HomeState.Success(data = action.data) | |
// StartXXXSideEffectAction | |
StartXXXSideEffectAction.Start -> TODO() | |
StartXXXSideEffectAction.Stop -> TODO() | |
is StartXXXSideEffectAction.Tick -> TODO() | |
is DataChangedSideEffectAction -> when (state) { | |
is HomeState.Error -> state | |
HomeState.Loading -> state | |
is HomeState.Success -> state.copy(data = action.data) | |
} | |
} | |
} | |
) | |
store.dispatch(HomeAction.FetchData) | |
store.dispatch(HomeAction.RetryFetchData) | |
val singleEventFlow: Flow<HomeSingleEvent> = receiveChannel.receiveAsFlow() | |
singleEventFlow | |
.onEach { | |
when (it) { | |
HomeSingleEvent.NavigateToXXX -> { | |
} | |
HomeSingleEvent.ShowToast -> { | |
} | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment