diff options
author | Amit Kumar <amitkma@e.email> | 2021-04-29 02:25:01 +0530 |
---|---|---|
committer | Amit Kumar <amitkma@e.email> | 2021-04-29 02:25:01 +0530 |
commit | 449e419dfbafeed9a446e36f8de1903981cd0b02 (patch) | |
tree | c961334c95a6def92420fa57bd640ad2dbbe2959 | |
parent | c00ab1009aa512fe89b8965cb0bee3827c64fd98 (diff) |
Add initial implementation of mvi based on kotlin flow
-rw-r--r-- | app/src/main/java/foundation/e/privacycentralapp/PrivacyCentralApplication.kt (renamed from flow-mvi/src/main/java/foundation/e/flowmvi/MyClass.kt) | 5 | ||||
-rw-r--r-- | app/src/main/java/foundation/e/privacycentralapp/features/HomeFeature.kt | 4 | ||||
-rw-r--r-- | app/src/main/java/foundation/e/privacycentralapp/features/HomeViewModel.kt | 4 | ||||
-rw-r--r-- | app/src/main/java/foundation/e/privacycentralapp/features/MainActivity.kt (renamed from app/src/main/java/foundation/e/privacycentralapp/MainActivity.kt) | 0 | ||||
-rw-r--r-- | flow-mvi/build.gradle | 1 | ||||
-rw-r--r-- | flow-mvi/src/main/java/foundation/e/flowmvi/MVIView.kt | 27 | ||||
-rw-r--r-- | flow-mvi/src/main/java/foundation/e/flowmvi/Store.kt | 24 | ||||
-rw-r--r-- | flow-mvi/src/main/java/foundation/e/flowmvi/Types.kt | 42 | ||||
-rw-r--r-- | flow-mvi/src/main/java/foundation/e/flowmvi/feature/BaseFeature.kt | 160 | ||||
-rw-r--r-- | flow-mvi/src/main/java/foundation/e/flowmvi/feature/Feature.kt | 62 |
10 files changed, 327 insertions, 2 deletions
diff --git a/flow-mvi/src/main/java/foundation/e/flowmvi/MyClass.kt b/app/src/main/java/foundation/e/privacycentralapp/PrivacyCentralApplication.kt index 32b47a1..4f5039a 100644 --- a/flow-mvi/src/main/java/foundation/e/flowmvi/MyClass.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/PrivacyCentralApplication.kt @@ -15,6 +15,7 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -package foundation.e.flowmvi +package foundation.e.privacycentralapp -class MyClass +class PrivacyCentralApplication { +}
\ No newline at end of file diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/HomeFeature.kt b/app/src/main/java/foundation/e/privacycentralapp/features/HomeFeature.kt new file mode 100644 index 0000000..43f29d2 --- /dev/null +++ b/app/src/main/java/foundation/e/privacycentralapp/features/HomeFeature.kt @@ -0,0 +1,4 @@ +package foundation.e.privacycentralapp.features + +class HomeFeature { +}
\ No newline at end of file diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/HomeViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/HomeViewModel.kt new file mode 100644 index 0000000..6cb37c6 --- /dev/null +++ b/app/src/main/java/foundation/e/privacycentralapp/features/HomeViewModel.kt @@ -0,0 +1,4 @@ +package foundation.e.privacycentralapp.features + +class HomeViewModel { +}
\ No newline at end of file diff --git a/app/src/main/java/foundation/e/privacycentralapp/MainActivity.kt b/app/src/main/java/foundation/e/privacycentralapp/features/MainActivity.kt index 3dd2145..3dd2145 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/MainActivity.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/MainActivity.kt diff --git a/flow-mvi/build.gradle b/flow-mvi/build.gradle index 52478d7..a012229 100644 --- a/flow-mvi/build.gradle +++ b/flow-mvi/build.gradle @@ -27,4 +27,5 @@ java { dependencies { implementation Libs.Kotlin.stdlib + implementation Libs.Coroutines.core } diff --git a/flow-mvi/src/main/java/foundation/e/flowmvi/MVIView.kt b/flow-mvi/src/main/java/foundation/e/flowmvi/MVIView.kt new file mode 100644 index 0000000..aa6f624 --- /dev/null +++ b/flow-mvi/src/main/java/foundation/e/flowmvi/MVIView.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2021 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package foundation.e.flowmvi + +import kotlinx.coroutines.flow.Flow + +interface MVIView<State, out Action> { + + fun render(state: State) + + fun actions(): Flow<Action> +} diff --git a/flow-mvi/src/main/java/foundation/e/flowmvi/Store.kt b/flow-mvi/src/main/java/foundation/e/flowmvi/Store.kt new file mode 100644 index 0000000..3040f3f --- /dev/null +++ b/flow-mvi/src/main/java/foundation/e/flowmvi/Store.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2021 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package foundation.e.flowmvi + +import kotlinx.coroutines.flow.StateFlow + +interface Store<State : Any, in Action : Any> { + val state: StateFlow<State> +} diff --git a/flow-mvi/src/main/java/foundation/e/flowmvi/Types.kt b/flow-mvi/src/main/java/foundation/e/flowmvi/Types.kt new file mode 100644 index 0000000..1f22a35 --- /dev/null +++ b/flow-mvi/src/main/java/foundation/e/flowmvi/Types.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2021 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package foundation.e.flowmvi + +import kotlinx.coroutines.flow.Flow + +/** + * Actor is a function that receives the current state and the action which just happened + * and acts on it. + * + * It returns a [Flow] of Effects which then can be used in a reducer to reduce to a new state. + */ +typealias Actor<State, Action, Effect> = (state: State, action: Action) -> Flow<Effect> + +/** + * Reducer is a function that applies the effect to current state and return a new state. + * + * This function should be free from any side-effect and make sure it is idempotent. + */ +typealias Reducer<State, Effect> = (state: State, effect: Effect) -> State + +typealias SingleEventProducer<State, Action, Effect, SingleEvent> = (state: State, action: Action, effect: Effect) -> SingleEvent? + +/** + * Logger is function used for logging + */ +typealias Logger = (String) -> Unit diff --git a/flow-mvi/src/main/java/foundation/e/flowmvi/feature/BaseFeature.kt b/flow-mvi/src/main/java/foundation/e/flowmvi/feature/BaseFeature.kt new file mode 100644 index 0000000..f7236ca --- /dev/null +++ b/flow-mvi/src/main/java/foundation/e/flowmvi/feature/BaseFeature.kt @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2021 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package foundation.e.flowmvi.feature + +import foundation.e.flowmvi.Actor +import foundation.e.flowmvi.Logger +import foundation.e.flowmvi.MVIView +import foundation.e.flowmvi.Reducer +import foundation.e.flowmvi.SingleEventProducer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +open class BaseFeature<State : Any, in Action : Any, in Effect : Any, SingleEvent : Any>( + initialState: State, + private val actor: Actor<State, Action, Effect>, + private val reducer: Reducer<State, Effect>, + private val coroutineScope: CoroutineScope, + private val defaultLogger: Logger = {}, + private val singleEventProducer: SingleEventProducer<State, Action, Effect, SingleEvent>? = null +) : + Feature<State, Action, SingleEvent> { + + private val mutex = Mutex() + + private val _state = MutableStateFlow(initialState) + override val state: StateFlow<State> = _state.asStateFlow() + + private val singleEventChannel = Channel<SingleEvent>() + override val singleEvents: Flow<SingleEvent> = singleEventChannel.receiveAsFlow() + + override fun takeView( + viewCoroutineScope: CoroutineScope, + view: MVIView<State, Action>, + initialActions: List<Action>, + logger: Logger? + ) { + viewCoroutineScope.launch { + sendStateUpdatesIntoView(this, view, logger ?: defaultLogger) + handleViewActions(this, view, initialActions, logger ?: defaultLogger) + } + } + + private fun sendStateUpdatesIntoView( + callerCoroutineScope: CoroutineScope, + view: MVIView<State, Action>, + logger: Logger + ) { + state + .onStart { + logger.invoke("State flow started") + } + .onCompletion { + logger.invoke("State flow completed") + } + .onEach { + logger.invoke("New state: $it") + view.render(it) + } + .launchIn(callerCoroutineScope) + } + + private fun handleViewActions( + coroutineScope: CoroutineScope, + view: MVIView<State, Action>, + initialActions: List<Action>, + logger: Logger + ) { + coroutineScope.launch { + view + .actions() + .onStart { + logger.invoke("View actions flow started") + emitAll(initialActions.asFlow()) + } + .onCompletion { + logger.invoke("View actions flow completed") + } + .collectIntoHandler(this, logger) + } + } + + override fun addExternalActions(actions: Flow<Action>, logger: Logger?) { + coroutineScope.launch { + actions.collectIntoHandler(this, logger ?: defaultLogger) + } + } + + private suspend fun Flow<Action>.collectIntoHandler( + callerCoroutineScope: CoroutineScope, + logger: Logger + ) { + onEach { action -> + callerCoroutineScope.launch { + logger.invoke("Received action $action") + actor.invoke(_state.value, action) + .onEach { effect -> + mutex.withLock { + logger.invoke("Applying effect $effect from action $action") + val newState = reducer.invoke(_state.value, effect) + _state.value = newState + singleEventProducer?.also { + it.invoke(newState, action, effect)?.let { singleEvent -> + singleEventChannel.send( + singleEvent + ) + } + } + } + } + .launchIn(coroutineScope) + } + } + .launchIn(callerCoroutineScope) + } +} + +fun <State : Any, Action : Any, Effect : Any, SingleEvent : Any> feature( + initialState: State, + actor: Actor<State, Action, Effect>, + reducer: Reducer<State, Effect>, + coroutineScope: CoroutineScope, + defaultLogger: Logger = {}, + singleEventProducer: SingleEventProducer<State, Action, Effect, SingleEvent>? = null +) = BaseFeature( + initialState, + actor, + reducer, + coroutineScope, + defaultLogger, + singleEventProducer +) diff --git a/flow-mvi/src/main/java/foundation/e/flowmvi/feature/Feature.kt b/flow-mvi/src/main/java/foundation/e/flowmvi/feature/Feature.kt new file mode 100644 index 0000000..bd9ca16 --- /dev/null +++ b/flow-mvi/src/main/java/foundation/e/flowmvi/feature/Feature.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2021 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package foundation.e.flowmvi.feature + +import foundation.e.flowmvi.Logger +import foundation.e.flowmvi.MVIView +import foundation.e.flowmvi.Store +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow + +interface Feature<State : Any, in Action : Any, SingleEvent : Any> : Store<State, Action> { + val singleEvents: Flow<SingleEvent> + + /** + * Call this method when a new [View] is ready to render the state of this MVFlow object. + * + * @param viewCoroutineScope the scope of the view. This will be used to launch a coroutine which will run listening + * to actions until this scope is cancelled. + * @param view the view that will render the state. + * @param initialActions an optional list of Actions that can be passed to introduce an initial action into the + * screen (for example, to trigger a refresh of data). + * @param logger Optional [Logger] to log events inside this MVFlow object associated with this view (but not + * others). If null, a default logger might be used. + */ + fun takeView( + viewCoroutineScope: CoroutineScope, + view: MVIView<State, Action>, + initialActions: List<Action> = emptyList(), + logger: Logger? = null + ) + + /** + * This method adds an external source of actions into the MVFlow object. + * + * This might be useful if you need to update your state based on things happening outside the [View], such as + * timers, external database updates, push notifications, etc. + * + * @param actions the flow of events. You might want to have a look at + * [kotlinx.coroutines.flow.callbackFlow]. + * @param logger Optional [Logger] to log events inside this MVFlow object associated with this external Flow (but + * not others). If null, a default logger might be used. + */ + fun addExternalActions( + actions: Flow<Action>, + logger: Logger? = null + ) +} |