From defa5590a5e0b14ab0d7d8972dd68628a389fe4b Mon Sep 17 00:00:00 2001 From: HaliksaR Date: Sun, 29 Nov 2020 05:22:24 +0700 Subject: [PATCH] improvement/core-mvi create core-mvi --- core-mvi/.gitignore | 1 + core-mvi/build.gradle | 13 +++ .../com/vanced/manager/core/mvi/MviFlow.kt | 98 +++++++++++++++++++ .../manager/core/mvi/MviFlowContainer.kt | 21 ++++ .../vanced/manager/core/mvi/MviRenderView.kt | 12 +++ .../com/vanced/manager/core/mvi/Typealias.kt | 16 +++ .../src/main/java/example/Example2Fragment.kt | 55 +++++++++++ .../src/main/java/example/ExampleContainer.kt | 55 +++++++++++ .../src/main/java/example/ExampleFragment.kt | 52 ++++++++++ .../src/main/java/example/ExampleViewModel.kt | 15 +++ settings.gradle | 2 +- 11 files changed, 339 insertions(+), 1 deletion(-) create mode 100644 core-mvi/.gitignore create mode 100644 core-mvi/build.gradle create mode 100644 core-mvi/src/main/java/com/vanced/manager/core/mvi/MviFlow.kt create mode 100644 core-mvi/src/main/java/com/vanced/manager/core/mvi/MviFlowContainer.kt create mode 100644 core-mvi/src/main/java/com/vanced/manager/core/mvi/MviRenderView.kt create mode 100644 core-mvi/src/main/java/com/vanced/manager/core/mvi/Typealias.kt create mode 100644 core-mvi/src/main/java/example/Example2Fragment.kt create mode 100644 core-mvi/src/main/java/example/ExampleContainer.kt create mode 100644 core-mvi/src/main/java/example/ExampleFragment.kt create mode 100644 core-mvi/src/main/java/example/ExampleViewModel.kt diff --git a/core-mvi/.gitignore b/core-mvi/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core-mvi/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core-mvi/build.gradle b/core-mvi/build.gradle new file mode 100644 index 00000000..0aa50cd2 --- /dev/null +++ b/core-mvi/build.gradle @@ -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" +} \ No newline at end of file diff --git a/core-mvi/src/main/java/com/vanced/manager/core/mvi/MviFlow.kt b/core-mvi/src/main/java/com/vanced/manager/core/mvi/MviFlow.kt new file mode 100644 index 00000000..3c3efe0a --- /dev/null +++ b/core-mvi/src/main/java/com/vanced/manager/core/mvi/MviFlow.kt @@ -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 { + + fun bindView( + view: MviRenderView, + scope: CoroutineScope, + actions: List = listOf() + ) + + fun bindSideEffects( + view: MviRenderView, + scope: CoroutineScope + ) +} + +private class MviFlowImpl( + initialState: State, + private val reducer: Reducer, + private val handler: Handler, + scope: CoroutineScope, +) : MviFlow, + CoroutineScope by scope, + Mutex by Mutex() { + + private val state = MutableStateFlow(initialState) + private val sideEffect = MutableSharedFlow() + + override fun bindSideEffects( + view: MviRenderView, + scope: CoroutineScope + ): Unit = with(scope) { + launch { + sideEffect.collect { view.sideEffects(it) } + } + } + + override fun bindView( + view: MviRenderView, + scope: CoroutineScope, + actions: List + ): Unit = with(scope) { + emitStateForRender(view) + handleActions(view, actions) + } + + private fun CoroutineScope.emitStateForRender( + view: MviRenderView + ) = launch { + state.collect { + view.render(it) + } + } + + private fun CoroutineScope.handleActions( + view: MviRenderView, + actions: List + ) = launch { + view.actionsFlow() + .onStart { emitAll(actions.asFlow()) } + .proceed() + } + + private suspend fun Flow.proceed() = + collect { action -> + handler.invoke( + MutableSharedFlow().setState(), + state.value, action, sideEffect + ) + } + + private fun MutableSharedFlow.setState(): MutableSharedFlow { + onEach { modification -> + withLock { + state.value = reducer.invoke(state.value, modification) + } + }.launchIn(this@MviFlowImpl) + return this + } +} + +fun MviFlow( + initialState: State, + reducer: Reducer, + handler: Handler, + scope: CoroutineScope +): MviFlow = MviFlowImpl( + initialState = initialState, + reducer = reducer, + handler = handler, + scope = scope +) \ No newline at end of file diff --git a/core-mvi/src/main/java/com/vanced/manager/core/mvi/MviFlowContainer.kt b/core-mvi/src/main/java/com/vanced/manager/core/mvi/MviFlowContainer.kt new file mode 100644 index 00000000..751bcd0e --- /dev/null +++ b/core-mvi/src/main/java/com/vanced/manager/core/mvi/MviFlowContainer.kt @@ -0,0 +1,21 @@ +package com.vanced.manager.core.mvi + +import kotlinx.coroutines.CoroutineScope + +abstract class MviFlowContainer { + + protected abstract val handler: Handler + + protected abstract val reducer: Reducer + + fun create( + state: State, + scope: CoroutineScope + ): MviFlow = + MviFlow( + initialState = state, + reducer = reducer, + handler = handler, + scope = scope + ) +} \ No newline at end of file diff --git a/core-mvi/src/main/java/com/vanced/manager/core/mvi/MviRenderView.kt b/core-mvi/src/main/java/com/vanced/manager/core/mvi/MviRenderView.kt new file mode 100644 index 00000000..31a9fb9c --- /dev/null +++ b/core-mvi/src/main/java/com/vanced/manager/core/mvi/MviRenderView.kt @@ -0,0 +1,12 @@ +package com.vanced.manager.core.mvi + +import kotlinx.coroutines.flow.Flow + +interface MviRenderView { + + fun render(state: State) + + fun actionsFlow(): Flow + + fun sideEffects(sideEffect: SideEffect) +} \ No newline at end of file diff --git a/core-mvi/src/main/java/com/vanced/manager/core/mvi/Typealias.kt b/core-mvi/src/main/java/com/vanced/manager/core/mvi/Typealias.kt new file mode 100644 index 00000000..179e8ea3 --- /dev/null +++ b/core-mvi/src/main/java/com/vanced/manager/core/mvi/Typealias.kt @@ -0,0 +1,16 @@ +package com.vanced.manager.core.mvi + +import kotlinx.coroutines.flow.MutableSharedFlow + +typealias Handler = + suspend MutableSharedFlow.( + state: State, + action: Action, + sideEffect: MutableSharedFlow + ) -> Unit + +typealias Reducer = + suspend ( + state: State, + modification: Modification + ) -> State \ No newline at end of file diff --git a/core-mvi/src/main/java/example/Example2Fragment.kt b/core-mvi/src/main/java/example/Example2Fragment.kt new file mode 100644 index 00000000..b75c7f17 --- /dev/null +++ b/core-mvi/src/main/java/example/Example2Fragment.kt @@ -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 { + + 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 = callbackFlow { + //generate actions click and other + awaitClose() + } + + override fun sideEffects(sideEffect: SideEffect) { // single events + when (sideEffect) { + is SideEffect.ShowToast -> { + // Toast.show + } + } + } +} \ No newline at end of file diff --git a/core-mvi/src/main/java/example/ExampleContainer.kt b/core-mvi/src/main/java/example/ExampleContainer.kt new file mode 100644 index 00000000..4ec44734 --- /dev/null +++ b/core-mvi/src/main/java/example/ExampleContainer.kt @@ -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() { + + // "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: Action, sideEffect: MutableSharedFlow -> + 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: Modification -> + when (modification) { + is Modification.ChangeText -> { + State.Default + } + } + } +} \ No newline at end of file diff --git a/core-mvi/src/main/java/example/ExampleFragment.kt b/core-mvi/src/main/java/example/ExampleFragment.kt new file mode 100644 index 00000000..a0835325 --- /dev/null +++ b/core-mvi/src/main/java/example/ExampleFragment.kt @@ -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 { + + 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 = callbackFlow { + //generate actions click and other + awaitClose() + } + + override fun sideEffects(sideEffect: SideEffect) { // single events + when (sideEffect) { + is SideEffect.ShowToast -> { + // Toast.show + } + } + } +} \ No newline at end of file diff --git a/core-mvi/src/main/java/example/ExampleViewModel.kt b/core-mvi/src/main/java/example/ExampleViewModel.kt new file mode 100644 index 00000000..d5a93067 --- /dev/null +++ b/core-mvi/src/main/java/example/ExampleViewModel.kt @@ -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" +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index fb3a2764..d264cef5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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'