Skip to content

Instantly share code, notes, and snippets.

@hoc081098
Last active April 24, 2023 18:36
Show Gist options
  • Save hoc081098/c54154a6eba4fdbe87e574a9b27d1d64 to your computer and use it in GitHub Desktop.
Save hoc081098/c54154a6eba4fdbe87e574a9b27d1d64 to your computer and use it in GitHub Desktop.
flow_redux.kt
// ----------------------------------------------------------------------------- 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