diff options
Diffstat (limited to 'flow-mvi/src/main')
5 files changed, 299 insertions, 0 deletions
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..8dec0c4 --- /dev/null +++ b/flow-mvi/src/main/java/foundation/e/flowmvi/feature/BaseFeature.kt @@ -0,0 +1,144 @@ +/* + * 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 { cause -> + logger.invoke("View actions flow completed: $cause") + } + .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) + } +} 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 + ) +} |