mirror of
https://github.com/YTVanced/VancedManager
synced 2024-11-30 06:53:01 +00:00
Merge pull request #298 from HaliksaR/improvement/core-mvi
improvement/core-mvi small refactoring and update examples
This commit is contained in:
commit
2e71eb3c3e
18 changed files with 573 additions and 169 deletions
|
@ -7,7 +7,17 @@ java {
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1"
|
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"
|
||||||
}
|
}
|
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
|
@ -6,7 +6,7 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
interface MviFlow<State, Action, SideEffect> {
|
interface MviFlow<State, Action, Modification, SideEffect> {
|
||||||
|
|
||||||
fun bindView(
|
fun bindView(
|
||||||
view: MviRenderView<State, Action, SideEffect>,
|
view: MviRenderView<State, Action, SideEffect>,
|
||||||
|
@ -25,7 +25,7 @@ private class MviFlowImpl<State, Action, Modification, SideEffect>(
|
||||||
private val reducer: Reducer<State, Modification>,
|
private val reducer: Reducer<State, Modification>,
|
||||||
private val handler: Handler<State, Action, Modification, SideEffect>,
|
private val handler: Handler<State, Action, Modification, SideEffect>,
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
) : MviFlow<State, Action, SideEffect>,
|
) : MviFlow<State, Action, Modification, SideEffect>,
|
||||||
CoroutineScope by scope,
|
CoroutineScope by scope,
|
||||||
Mutex by Mutex() {
|
Mutex by Mutex() {
|
||||||
|
|
||||||
|
@ -70,18 +70,18 @@ private class MviFlowImpl<State, Action, Modification, SideEffect>(
|
||||||
private suspend fun Flow<Action>.proceed() =
|
private suspend fun Flow<Action>.proceed() =
|
||||||
collect { action ->
|
collect { action ->
|
||||||
handler.invoke(
|
handler.invoke(
|
||||||
MutableSharedFlow<Modification>().setState(),
|
MutableSharedFlow<Modification>().subscribeState(),
|
||||||
state.value, action, sideEffect
|
state.value, action, sideEffect
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MutableSharedFlow<Modification>.setState(): MutableSharedFlow<Modification> {
|
private fun MutableSharedFlow<Modification>.subscribeState(): MutableSharedFlow<Modification> =
|
||||||
|
also {
|
||||||
onEach { modification ->
|
onEach { modification ->
|
||||||
withLock {
|
withLock {
|
||||||
state.value = reducer.invoke(state.value, modification)
|
reducer.invoke(state, state.value, modification)
|
||||||
}
|
}
|
||||||
}.launchIn(this@MviFlowImpl)
|
}.launchIn(this@MviFlowImpl)
|
||||||
return this
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ fun <State, Action, SideEffect, Modification> MviFlow(
|
||||||
reducer: Reducer<State, Modification>,
|
reducer: Reducer<State, Modification>,
|
||||||
handler: Handler<State, Action, Modification, SideEffect>,
|
handler: Handler<State, Action, Modification, SideEffect>,
|
||||||
scope: CoroutineScope
|
scope: CoroutineScope
|
||||||
): MviFlow<State, Action, SideEffect> = MviFlowImpl(
|
): MviFlow<State, Action, Modification, SideEffect> = MviFlowImpl(
|
||||||
initialState = initialState,
|
initialState = initialState,
|
||||||
reducer = reducer,
|
reducer = reducer,
|
||||||
handler = handler,
|
handler = handler,
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.vanced.manager.core.mvi
|
||||||
|
|
||||||
|
interface MviFlowStore<State, Action, Modification, SideEffect> {
|
||||||
|
|
||||||
|
val store: MviFlow<State, Action, Modification, SideEffect>
|
||||||
|
}
|
|
@ -6,11 +6,11 @@ typealias Handler<State, Action, Modification, SideEffect> =
|
||||||
suspend MutableSharedFlow<Modification>.(
|
suspend MutableSharedFlow<Modification>.(
|
||||||
state: State,
|
state: State,
|
||||||
action: Action,
|
action: Action,
|
||||||
sideEffect: MutableSharedFlow<SideEffect>
|
sideEffectsFlow: MutableSharedFlow<SideEffect>,
|
||||||
) -> Unit
|
) -> Unit
|
||||||
|
|
||||||
typealias Reducer<State, Modification> =
|
typealias Reducer<State, Modification> =
|
||||||
suspend (
|
suspend MutableSharedFlow<State>.(
|
||||||
state: State,
|
state: State,
|
||||||
modification: Modification
|
modification: Modification
|
||||||
) -> State
|
) -> Unit
|
95
core-mvi/src/main/kotlin/example/ExampleManyStoreFragment.kt
Normal file
95
core-mvi/src/main/kotlin/example/ExampleManyStoreFragment.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
75
core-mvi/src/main/kotlin/example/ExampleStoreFragment.kt
Normal file
75
core-mvi/src/main/kotlin/example/ExampleStoreFragment.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
core-mvi/src/main/kotlin/example/ExampleViewModel.kt
Normal file
43
core-mvi/src/main/kotlin/example/ExampleViewModel.kt
Normal 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"
|
||||||
|
}
|
|
@ -1,9 +1,6 @@
|
||||||
package example
|
package example
|
||||||
|
|
||||||
import com.vanced.manager.core.mvi.MviRenderView
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
@ -13,18 +10,18 @@ import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.launch
|
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() {
|
private fun onCreate() {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
viewModel.mvi.bindView(view = this@ExampleFragment, scope = this)
|
viewModel.store.bindView(view = this@ExampleWithViewModelFragment, scope = this)
|
||||||
}
|
}
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
viewModel.mvi.bindSideEffects(view = this@ExampleFragment, scope = this)
|
viewModel.store.bindSideEffects(view = this@ExampleWithViewModelFragment, scope = this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
25
core-mvi/src/main/kotlin/example/Mvi.kt
Normal file
25
core-mvi/src/main/kotlin/example/Mvi.kt
Normal 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()
|
||||||
|
}
|
|
@ -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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue