Created
July 23, 2019 04:40
-
-
Save sav007/d467a3ca258ecc1cedd1c2b59119a429 to your computer and use it in GitHub Desktop.
Kotlin MVI
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
typealias ViewStateReducer<STATE, EVENT> = (STATE, EVENT) -> STATE | |
typealias SideEffect<EVENT, STATE> = suspend (EVENT, STATE) -> EVENT? | |
fun <STATE, EVENT> CoroutineScope.viewStateFlow(reduce: ViewStateReducer<STATE, EVENT>) = ViewStateFlow.Builder( | |
scope = this, | |
reduce = reduce | |
) | |
interface Subscription { | |
fun cancel() | |
} | |
interface ViewStateFlow<STATE, EVENT> { | |
fun send(event: EVENT): Boolean | |
fun subscribe(observe: (STATE) -> Unit): Subscription | |
class Builder<STATE, EVENT>( | |
private val scope: CoroutineScope, | |
private val reduce: ViewStateReducer<STATE, EVENT>, | |
private val sideEffect: SideEffect<EVENT, STATE> = { _, _ -> null } | |
) { | |
fun sideEffect(sideEffect: SideEffect<EVENT, STATE>) = Builder<STATE, EVENT>( | |
scope = scope, | |
reduce = reduce, | |
sideEffect = sideEffect | |
) | |
fun start(initialState: STATE): ViewStateFlow<STATE, EVENT> { | |
val eventChannel = Channel<EVENT>(Channel.CONFLATED) | |
val viewStateChannel = BroadcastChannel<STATE>(Channel.CONFLATED) | |
val sideEffectJob = SupervisorJob(scope.coroutineContext[Job]) | |
scope.launch { | |
var currentState = initialState | |
eventChannel.consumeEach { event -> | |
currentState = reduce(currentState, event) | |
viewStateChannel.send(currentState) | |
launch(sideEffectJob) { | |
sideEffect(event, currentState)?.let { | |
eventChannel.send(it) | |
} | |
} | |
} | |
}.invokeOnCompletion { | |
eventChannel.close() | |
viewStateChannel.close() | |
} | |
return object : ViewStateFlow<STATE, EVENT> { | |
override fun send(event: EVENT) = eventChannel.offer(event) | |
override fun subscribe(observe: (STATE) -> Unit): Subscription { | |
val receiveChannel = viewStateChannel.openSubscription() | |
scope.launch { | |
receiveChannel.consumeEach(observe) | |
} | |
return object : Subscription { | |
override fun cancel() = receiveChannel.cancel() | |
} | |
} | |
} | |
} | |
} | |
} |
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
sealed class ViewState { | |
object Idle : ViewState() | |
object Loading : ViewState() | |
object Error : ViewState() | |
object Complete : ViewState() | |
} | |
sealed class ViewStateEvent { | |
object Load : ViewStateEvent() | |
object Error : ViewStateEvent() | |
object Success : ViewStateEvent() | |
} | |
class UserListViewModel : ViewModel() { | |
private val scope = CoroutineScope(Dispatchers.Main) | |
val stateFlow: ViewStateFlow<ViewState, ViewStateEvent> | |
init { | |
stateFlow = scope.viewStateFlow<ViewState, ViewStateEvent> { state, event -> | |
when (event) { | |
is ViewStateEvent.Load -> ViewState.Loading | |
is ViewStateEvent.Error -> ViewState.Error | |
is ViewStateEvent.Success -> ViewState.Complete | |
} | |
}.sideEffect { event, state -> | |
when (event) { | |
is ViewStateEvent.Load -> scope.async { | |
delay(1000) | |
ViewStateEvent.Error | |
}.await() | |
else -> null | |
} | |
}.start(ViewState.Idle) | |
} | |
override fun onCleared() { | |
scope.cancel() | |
} | |
} | |
fun main() { | |
val viewModel = UserListViewModel() | |
viewModel.stateFlow.subscribe { state -> | |
state.render() | |
} | |
viewModel.stateFlow.send(ViewStateEvent.Load) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment