Merge pull request #298 from HaliksaR/improvement/core-mvi

improvement/core-mvi small refactoring and update examples
This commit is contained in:
Tornike Khintibidze 2020-11-30 13:33:51 +04:00 committed by GitHub
commit 2e71eb3c3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 573 additions and 169 deletions

View File

@ -7,7 +7,17 @@ java {
targetCompatibility = JavaVersion.VERSION_1_8
}
test {
useJUnitPlatform()
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1"
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.1'
testImplementation 'io.kotest:kotest-runner-junit5:4.3.1'
testImplementation 'io.kotest:kotest-assertions-core:4.3.1'
testImplementation 'io.kotest:kotest-property:4.3.1'
testImplementation "io.mockk:mockk:1.10.2"
}

View File

@ -1,21 +0,0 @@
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

@ -1,55 +0,0 @@
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

@ -1,55 +0,0 @@
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

@ -1,15 +0,0 @@
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

@ -6,7 +6,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
interface MviFlow<State, Action, SideEffect> {
interface MviFlow<State, Action, Modification, SideEffect> {
fun bindView(
view: MviRenderView<State, Action, SideEffect>,
@ -25,7 +25,7 @@ private class MviFlowImpl<State, Action, Modification, SideEffect>(
private val reducer: Reducer<State, Modification>,
private val handler: Handler<State, Action, Modification, SideEffect>,
scope: CoroutineScope,
) : MviFlow<State, Action, SideEffect>,
) : MviFlow<State, Action, Modification, SideEffect>,
CoroutineScope by scope,
Mutex by Mutex() {
@ -70,19 +70,19 @@ private class MviFlowImpl<State, Action, Modification, SideEffect>(
private suspend fun Flow<Action>.proceed() =
collect { action ->
handler.invoke(
MutableSharedFlow<Modification>().setState(),
MutableSharedFlow<Modification>().subscribeState(),
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
}
private fun MutableSharedFlow<Modification>.subscribeState(): MutableSharedFlow<Modification> =
also {
onEach { modification ->
withLock {
reducer.invoke(state, state.value, modification)
}
}.launchIn(this@MviFlowImpl)
}
}
fun <State, Action, SideEffect, Modification> MviFlow(
@ -90,7 +90,7 @@ fun <State, Action, SideEffect, Modification> MviFlow(
reducer: Reducer<State, Modification>,
handler: Handler<State, Action, Modification, SideEffect>,
scope: CoroutineScope
): MviFlow<State, Action, SideEffect> = MviFlowImpl(
): MviFlow<State, Action, Modification, SideEffect> = MviFlowImpl(
initialState = initialState,
reducer = reducer,
handler = handler,

View File

@ -0,0 +1,6 @@
package com.vanced.manager.core.mvi
interface MviFlowStore<State, Action, Modification, SideEffect> {
val store: MviFlow<State, Action, Modification, SideEffect>
}

View File

@ -6,11 +6,11 @@ typealias Handler<State, Action, Modification, SideEffect> =
suspend MutableSharedFlow<Modification>.(
state: State,
action: Action,
sideEffect: MutableSharedFlow<SideEffect>
sideEffectsFlow: MutableSharedFlow<SideEffect>,
) -> Unit
typealias Reducer<State, Modification> =
suspend (
suspend MutableSharedFlow<State>.(
state: State,
modification: Modification
) -> State
) -> Unit

View File

@ -0,0 +1,95 @@
package example
import com.vanced.manager.core.mvi.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch
class ExampleManyStoreFragment {
val lifecycleScope = CoroutineScope(Job())
private val view1 = getView()
private val container1 = getStore()
private val view2 = getView()
private val container2 = getStore()
private fun onCreate() {
lifecycleScope.launch { // bind view for call render
container1.store.bindView(view = view1, scope = this)
}
lifecycleScope.launch { // bind side effects (single events) for catch on view
container1.store.bindSideEffects(view = view1, scope = this)
}
lifecycleScope.launch { // bind view for call render
container2.store.bindView(view = view2, scope = this)
}
lifecycleScope.launch { // bind side effects (single events) for catch on view
container2.store.bindSideEffects(view = view2, scope = this)
}
}
}
// handle actions, generate side effects (single events) and send changes (may be viewModel ane change)
private fun createHandler(): 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\
private fun createReducer(): Reducer<State, Modification> = { _: State, modification: Modification ->
when (modification) {
is Modification.ChangeText -> {
State.Default
}
}
}
private fun ExampleManyStoreFragment.getStore() = object : MviFlowStore<State, Action, Modification, SideEffect> {
override val store: MviFlow<State, Action, Modification, SideEffect>
get() = MviFlow(
initialState = State.Default,
reducer = createReducer(),
handler = createHandler(),
scope = lifecycleScope
)// create "store"
}
private fun getView() =
object : MviRenderView<State, Action, SideEffect> {
override fun render(state: State) {
when (state) {
State.Default -> {
// render view
}
}
}
override fun actionsFlow(): Flow<Action> = callbackFlow {
//generate actions click and other
awaitClose()
}
override fun sideEffects(sideEffect: SideEffect) {
when (sideEffect) {
is SideEffect.ShowToast -> {
// Toast.show
}
}
}
}

View File

@ -0,0 +1,75 @@
package example
import com.vanced.manager.core.mvi.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.callbackFlow
class ExampleStoreFragment :
MviRenderView<State, Action, SideEffect>,
MviFlowStore<State, Action, Modification, SideEffect> {
private val lifecycleScope = CoroutineScope(Job())
// handle actions, generate side effects (single events) and send changes (may be viewModel ane change)
private val handler: Handler<State, Action, Modification, SideEffect> =
{ _: State, action: Action, sideEffectsFlow: MutableSharedFlow<SideEffect> ->
when (action) {
Action.Click -> {
sideEffectsFlow.emit(SideEffect.ShowToast(""))
delay(10)
emit(Modification.ChangeText(""))
}
}
}
// handle modifications and current state and generate new state for view
private val reducer: Reducer<State, Modification> = { _: State, modification: Modification ->
when (modification) {
is Modification.ChangeText -> {
State.Default
}
}
}
override val store: MviFlow<State, Action, Modification, SideEffect> =
MviFlow(
initialState = State.Default,
reducer = reducer,
handler = handler,
scope = lifecycleScope
)// create "store"
private fun onCreate() {
lifecycleScope.launch { // bind view for call render
store.bindView(view = this@ExampleStoreFragment, scope = this)
}
lifecycleScope.launch { // bind side effects (single events) for catch on view
store.bindSideEffects(view = this@ExampleStoreFragment, 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,43 @@
package example
import com.vanced.manager.core.mvi.Handler
import com.vanced.manager.core.mvi.MviFlow
import com.vanced.manager.core.mvi.MviFlowStore
import com.vanced.manager.core.mvi.Reducer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
class ExampleViewModel : MviFlowStore<State, Action, Modification, SideEffect> {
private val viewModelScope = CoroutineScope(Job())
// handle actions, generate side effects (single events) and send changes (may be viewModel ane change)
private val handler: Handler<State, Action, Modification, SideEffect> =
{ _: State, action: Action, sideEffectsFlow: MutableSharedFlow<SideEffect> ->
when (action) {
Action.Click -> {
sideEffectsFlow.emit(SideEffect.ShowToast(""))
delay(10)
emit(Modification.ChangeText(""))
}
}
}
// handle modifications and current state and generate new state for view
private val reducer: Reducer<State, Modification> = { _: State, modification: Modification ->
when (modification) {
is Modification.ChangeText -> {
State.Default
}
}
}
override val store: MviFlow<State, Action, Modification, SideEffect> = MviFlow(
initialState = State.Default,
reducer = reducer,
handler = handler,
scope = viewModelScope
) // create "store"
}

View File

@ -1,9 +1,6 @@
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
@ -13,18 +10,18 @@ import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch
class ExampleFragment : MviRenderView<State, Action, SideEffect> {
class ExampleWithViewModelFragment : MviRenderView<State, Action, SideEffect> {
val lifecycleScope = CoroutineScope(Job())
private val lifecycleScope = CoroutineScope(Job())
val viewModel = ExampleViewModel()
private val viewModel = ExampleViewModel()
private fun onCreate() {
lifecycleScope.launch {
viewModel.mvi.bindView(view = this@ExampleFragment, scope = this)
viewModel.store.bindView(view = this@ExampleWithViewModelFragment, scope = this)
}
lifecycleScope.launch {
viewModel.mvi.bindSideEffects(view = this@ExampleFragment, scope = this)
viewModel.store.bindSideEffects(view = this@ExampleWithViewModelFragment, scope = this)
}
}

View File

@ -0,0 +1,25 @@
package example
// "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()
}

View File

@ -0,0 +1,169 @@
package com.vanced.manager.core.mvi
import com.vanced.manager.core.mvi.subject.*
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.TestCoroutineScope
@ExperimentalCoroutinesApi
class MviFlowSpec : ShouldSpec() {
data class Test(val msg: String)
init {
context("Various events") {
should("return Modifications") {
val testData = "testText"
val store = SubjectStore<String>(
scope = TestCoroutineScope(),
defaultState = State.Default,
testData = testData
)
SubjectView(
scope = TestCoroutineScope(),
store = store,
actions = flow {
emit(Action.Click)
emit(Action.Tap)
emit(Action.Click)
}
)
delay(600)
store.modifications shouldBe listOf(
Modification.ChangeDescription(testData),
Modification.ChangeTitle(testData),
Modification.ChangeDescription(testData),
)
}
should("return States") {
val testData = 4545454545
val store = SubjectStore<Long>(
scope = TestCoroutineScope(),
defaultState = State.Default,
testData = testData
)
val view = SubjectView(
scope = TestCoroutineScope(),
store = store,
actions = flow {
emit(Action.Click)
emit(Action.Tap)
emit(Action.Click)
}
)
delay(600)
view.states shouldBe listOf(
State.Default,
State.SetDescription(testData),
State.Default,
State.SetTitle(testData),
State.Default,
State.SetDescription(testData),
State.Default,
)
}
should("return SideEffects") {
val testData = Test("test")
val store = SubjectStore<Test>(
scope = TestCoroutineScope(),
defaultState = State.Default,
testData = testData
)
val view = SubjectView(
scope = TestCoroutineScope(),
store = store,
actions = flow {
emit(Action.Click)
emit(Action.Tap)
emit(Action.Click)
}
)
delay(600)
view.sideEffects shouldBe listOf(
SideEffect.ShowToast(testData),
SideEffect.ShowToast(testData),
SideEffect.ShowToast(testData),
)
}
}
context("The same event") {
should("return Modifications") {
val testData = Test("test")
val store = SubjectStore(
scope = TestCoroutineScope(),
defaultState = State.Default,
testData = testData
)
SubjectView(
scope = TestCoroutineScope(),
store = store,
actions = flow {
emit(Action.Click)
emit(Action.Click)
emit(Action.Click)
}
)
delay(600)
store.modifications shouldBe listOf(
Modification.ChangeDescription(testData),
Modification.ChangeDescription(testData),
Modification.ChangeDescription(testData),
)
}
should("return States") {
val testData = Test("test")
val store = SubjectStore(
scope = TestCoroutineScope(),
defaultState = State.Default,
testData = testData
)
val view = SubjectView(
scope = TestCoroutineScope(),
store = store,
actions = flow {
emit(Action.Click)
emit(Action.Click)
emit(Action.Click)
}
)
delay(600)
view.states shouldBe listOf(
State.Default,
State.SetDescription(testData),
State.Default,
State.SetDescription(testData),
State.Default,
State.SetDescription(testData),
State.Default
)
}
should("return SideEffects") {
val testData = Test("test")
val store = SubjectStore(
scope = TestCoroutineScope(),
defaultState = State.Default,
testData = testData
)
val view = SubjectView(
scope = TestCoroutineScope(),
store = store,
actions = flow {
emit(Action.Click)
emit(Action.Click)
emit(Action.Click)
}
)
delay(600)
view.sideEffects shouldBe listOf(
SideEffect.ShowToast(testData),
SideEffect.ShowToast(testData),
SideEffect.ShowToast(testData),
)
}
}
}
}

View File

@ -0,0 +1,33 @@
package com.vanced.manager.core.mvi.subject
// "single events"
sealed class SideEffect {
data class ShowToast<testData>(val message: testData) : SideEffect()
}
// view state
sealed class State {
object Default : State()
data class SetTitle<testData>(val text: testData) : State()
data class SetDescription<testData>(val text: testData) : State()
}
// view actions
sealed class Action {
object Click : Action()
object Tap : Action()
}
// Modification for view
sealed class Modification {
data class ChangeTitle<testData>(val text: testData) : Modification()
data class ChangeDescription<testData>(val text: testData) : Modification()
}

View File

@ -0,0 +1,54 @@
package com.vanced.manager.core.mvi.subject
import com.vanced.manager.core.mvi.Handler
import com.vanced.manager.core.mvi.MviFlow
import com.vanced.manager.core.mvi.MviFlowStore
import com.vanced.manager.core.mvi.Reducer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
class SubjectStore<TD>(
scope: CoroutineScope,
defaultState: State,
private val testData: TD
) : MviFlowStore<State, Action, Modification, SideEffect> {
val modifications = mutableListOf<Modification>()
private val handler: Handler<State, Action, Modification, SideEffect> =
{ state: State, action: Action, sideEffectsFlow: MutableSharedFlow<SideEffect> ->
when (action) {
Action.Click -> {
emit(Modification.ChangeDescription(testData))
sideEffectsFlow.emit(SideEffect.ShowToast(testData))
}
Action.Tap -> {
emit(Modification.ChangeTitle(testData))
sideEffectsFlow.emit(SideEffect.ShowToast(testData))
}
}
}
private val reducer: Reducer<State, Modification> =
{ state: State, modification: Modification ->
modifications.add(modification)
when (modification) {
is Modification.ChangeDescription<*> -> {
emit(State.SetDescription(modification.text))
emit(State.Default)
}
is Modification.ChangeTitle<*> -> {
emit(State.SetTitle(modification.text))
emit(State.Default)
}
}
}
override val store: MviFlow<State, Action, Modification, SideEffect> =
MviFlow(
scope = scope,
initialState = defaultState,
handler = handler,
reducer = reducer
)
}

View File

@ -0,0 +1,43 @@
package com.vanced.manager.core.mvi.subject
import com.vanced.manager.core.mvi.MviRenderView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
class SubjectView<TD>(
scope: CoroutineScope,
private val actions: Flow<Action>,
store: SubjectStore<TD>
) : MviRenderView<State, Action, SideEffect> {
val states = mutableListOf<State>()
val sideEffects = mutableListOf<SideEffect>()
init {
scope.launch {
store.store.bindSideEffects(
view = this@SubjectView,
scope = this
)
}
scope.launch {
store.store.bindView(
view = this@SubjectView,
scope = this
)
}
}
override fun render(state: State) {
states.add(state)
}
@ExperimentalCoroutinesApi
override fun actionsFlow(): Flow<Action> = actions
override fun sideEffects(sideEffect: SideEffect) {
sideEffects.add(sideEffect)
}
}