improvement/core-mvi create core-mvi

This commit is contained in:
HaliksaR 2020-11-29 05:22:24 +07:00
parent 9d72738a24
commit defa5590a5
11 changed files with 339 additions and 1 deletions

1
core-mvi/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

13
core-mvi/build.gradle Normal file
View File

@ -0,0 +1,13 @@
plugins {
id 'org.jetbrains.kotlin.jvm'
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1"
}

View File

@ -0,0 +1,98 @@
package com.vanced.manager.core.mvi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
interface MviFlow<State, Action, SideEffect> {
fun bindView(
view: MviRenderView<State, Action, SideEffect>,
scope: CoroutineScope,
actions: List<Action> = listOf()
)
fun bindSideEffects(
view: MviRenderView<State, Action, SideEffect>,
scope: CoroutineScope
)
}
private class MviFlowImpl<State, Action, Modification, SideEffect>(
initialState: State,
private val reducer: Reducer<State, Modification>,
private val handler: Handler<State, Action, Modification, SideEffect>,
scope: CoroutineScope,
) : MviFlow<State, Action, SideEffect>,
CoroutineScope by scope,
Mutex by Mutex() {
private val state = MutableStateFlow(initialState)
private val sideEffect = MutableSharedFlow<SideEffect>()
override fun bindSideEffects(
view: MviRenderView<State, Action, SideEffect>,
scope: CoroutineScope
): Unit = with(scope) {
launch {
sideEffect.collect { view.sideEffects(it) }
}
}
override fun bindView(
view: MviRenderView<State, Action, SideEffect>,
scope: CoroutineScope,
actions: List<Action>
): Unit = with(scope) {
emitStateForRender(view)
handleActions(view, actions)
}
private fun CoroutineScope.emitStateForRender(
view: MviRenderView<State, Action, SideEffect>
) = launch {
state.collect {
view.render(it)
}
}
private fun CoroutineScope.handleActions(
view: MviRenderView<State, Action, SideEffect>,
actions: List<Action>
) = launch {
view.actionsFlow()
.onStart { emitAll(actions.asFlow()) }
.proceed()
}
private suspend fun Flow<Action>.proceed() =
collect { action ->
handler.invoke(
MutableSharedFlow<Modification>().setState(),
state.value, action, sideEffect
)
}
private fun MutableSharedFlow<Modification>.setState(): MutableSharedFlow<Modification> {
onEach { modification ->
withLock {
state.value = reducer.invoke(state.value, modification)
}
}.launchIn(this@MviFlowImpl)
return this
}
}
fun <State, Action, SideEffect, Modification> MviFlow(
initialState: State,
reducer: Reducer<State, Modification>,
handler: Handler<State, Action, Modification, SideEffect>,
scope: CoroutineScope
): MviFlow<State, Action, SideEffect> = MviFlowImpl(
initialState = initialState,
reducer = reducer,
handler = handler,
scope = scope
)

View File

@ -0,0 +1,21 @@
package com.vanced.manager.core.mvi
import kotlinx.coroutines.CoroutineScope
abstract class MviFlowContainer<State, Action, Modification, SideEffect> {
protected abstract val handler: Handler<State, Action, Modification, SideEffect>
protected abstract val reducer: Reducer<State, Modification>
fun create(
state: State,
scope: CoroutineScope
): MviFlow<State, Action, SideEffect> =
MviFlow(
initialState = state,
reducer = reducer,
handler = handler,
scope = scope
)
}

View File

@ -0,0 +1,12 @@
package com.vanced.manager.core.mvi
import kotlinx.coroutines.flow.Flow
interface MviRenderView<State, Action, SideEffect> {
fun render(state: State)
fun actionsFlow(): Flow<Action>
fun sideEffects(sideEffect: SideEffect)
}

View File

@ -0,0 +1,16 @@
package com.vanced.manager.core.mvi
import kotlinx.coroutines.flow.MutableSharedFlow
typealias Handler<State, Action, Modification, SideEffect> =
suspend MutableSharedFlow<Modification>.(
state: State,
action: Action,
sideEffect: MutableSharedFlow<SideEffect>
) -> Unit
typealias Reducer<State, Modification> =
suspend (
state: State,
modification: Modification
) -> State

View File

@ -0,0 +1,55 @@
package example
import com.vanced.manager.core.mvi.MviRenderView
import example.ExampleContainer.Action
import example.ExampleContainer.SideEffect
import example.ExampleContainer.State
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch
class Example2Fragment : MviRenderView<State, Action, SideEffect> {
val lifecycleScope = CoroutineScope(Job())
val mvi = ExampleContainer.create(
state = State.Default,
scope = lifecycleScope
) // create "store"
private fun onCreate() {
lifecycleScope.launch { // bind view for call render
mvi.bindView(view = this@Example2Fragment, scope = this)
}
lifecycleScope.launch { // bind side effects (single events) for catch on view
mvi.bindSideEffects(view = this@Example2Fragment, scope = this)
}
}
override fun render(state: State) {
when (state) {
State.Default -> {
// render view
}
}
}
@ExperimentalCoroutinesApi
override fun actionsFlow(): Flow<Action> = callbackFlow {
//generate actions click and other
awaitClose()
}
override fun sideEffects(sideEffect: SideEffect) { // single events
when (sideEffect) {
is SideEffect.ShowToast -> {
// Toast.show
}
}
}
}

View File

@ -0,0 +1,55 @@
package example
import com.vanced.manager.core.mvi.Handler
import com.vanced.manager.core.mvi.MviFlowContainer
import com.vanced.manager.core.mvi.Reducer
import example.ExampleContainer.Action
import example.ExampleContainer.Modification
import example.ExampleContainer.SideEffect
import example.ExampleContainer.State
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
object ExampleContainer : MviFlowContainer<State, Action, Modification, SideEffect>() {
// "single events"
sealed class SideEffect {
data class ShowToast(val message: String) : SideEffect()
}
// view state
sealed class State {
object Default : State()
}
// view actions
sealed class Action {
object Click : Action()
}
// Modification for view
sealed class Modification {
data class ChangeText(val text: String) : Modification()
}
// handle actions, generate side effects (single events) and send changes (may be viewModel ane change)
override val handler: Handler<State, Action, Modification, SideEffect> =
{ _: State, action: Action, sideEffect: MutableSharedFlow<SideEffect> ->
when (action) {
Action.Click -> {
sideEffect.emit(SideEffect.ShowToast(""))
delay(10)
emit(Modification.ChangeText(""))
}
}
}
// handle modifications and current state and generate new state for view
override val reducer: Reducer<State, Modification> = { _: State, modification: Modification ->
when (modification) {
is Modification.ChangeText -> {
State.Default
}
}
}
}

View File

@ -0,0 +1,52 @@
package example
import com.vanced.manager.core.mvi.MviRenderView
import example.ExampleContainer.Action
import example.ExampleContainer.SideEffect
import example.ExampleContainer.State
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch
class ExampleFragment : MviRenderView<State, Action, SideEffect> {
val lifecycleScope = CoroutineScope(Job())
val viewModel = ExampleViewModel()
private fun onCreate() {
lifecycleScope.launch {
viewModel.mvi.bindView(view = this@ExampleFragment, scope = this)
}
lifecycleScope.launch {
viewModel.mvi.bindSideEffects(view = this@ExampleFragment, scope = this)
}
}
override fun render(state: State) {
when (state) {
State.Default -> {
// render view
}
}
}
@ExperimentalCoroutinesApi
override fun actionsFlow(): Flow<Action> = callbackFlow {
//generate actions click and other
awaitClose()
}
override fun sideEffects(sideEffect: SideEffect) { // single events
when (sideEffect) {
is SideEffect.ShowToast -> {
// Toast.show
}
}
}
}

View File

@ -0,0 +1,15 @@
package example
import example.ExampleContainer.State
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
class ExampleViewModel {
val viewModelScope = CoroutineScope(Job())
val mvi = ExampleContainer.create(
state = State.Default,
scope = viewModelScope
) // create "store"
}

View File

@ -1,7 +1,7 @@
rootProject.name='Vanced Manager'
include ':app'
include ':core-presentation', ':core-ui'
include ':core-presentation', ':core-ui' , ':core-mvi'
include ':feature-home'