mirror of
https://github.com/YTVanced/VancedManager
synced 2024-11-22 03:05:11 +00:00
improvement/core-mvi create core-mvi
This commit is contained in:
parent
9d72738a24
commit
defa5590a5
11 changed files with 339 additions and 1 deletions
1
core-mvi/.gitignore
vendored
Normal file
1
core-mvi/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/build
|
13
core-mvi/build.gradle
Normal file
13
core-mvi/build.gradle
Normal 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"
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
55
core-mvi/src/main/java/example/Example2Fragment.kt
Normal file
55
core-mvi/src/main/java/example/Example2Fragment.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
55
core-mvi/src/main/java/example/ExampleContainer.kt
Normal file
55
core-mvi/src/main/java/example/ExampleContainer.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
52
core-mvi/src/main/java/example/ExampleFragment.kt
Normal file
52
core-mvi/src/main/java/example/ExampleFragment.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
core-mvi/src/main/java/example/ExampleViewModel.kt
Normal file
15
core-mvi/src/main/java/example/ExampleViewModel.kt
Normal 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"
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
rootProject.name='Vanced Manager'
|
rootProject.name='Vanced Manager'
|
||||||
include ':app'
|
include ':app'
|
||||||
|
|
||||||
include ':core-presentation', ':core-ui'
|
include ':core-presentation', ':core-ui' , ':core-mvi'
|
||||||
|
|
||||||
include ':feature-home'
|
include ':feature-home'
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue