Skip to content

Instantly share code, notes, and snippets.

@sav007
Created July 23, 2019 04:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sav007/d467a3ca258ecc1cedd1c2b59119a429 to your computer and use it in GitHub Desktop.
Save sav007/d467a3ca258ecc1cedd1c2b59119a429 to your computer and use it in GitHub Desktop.
Kotlin MVI
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()
}
}
}
}
}
}
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