From 449e419dfbafeed9a446e36f8de1903981cd0b02 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Thu, 29 Apr 2021 02:25:01 +0530 Subject: Add initial implementation of mvi based on kotlin flow --- .../src/main/java/foundation/e/flowmvi/MVIView.kt | 27 ++++ .../src/main/java/foundation/e/flowmvi/MyClass.kt | 20 --- .../src/main/java/foundation/e/flowmvi/Store.kt | 24 ++++ .../src/main/java/foundation/e/flowmvi/Types.kt | 42 ++++++ .../foundation/e/flowmvi/feature/BaseFeature.kt | 160 +++++++++++++++++++++ .../java/foundation/e/flowmvi/feature/Feature.kt | 62 ++++++++ 6 files changed, 315 insertions(+), 20 deletions(-) create mode 100644 flow-mvi/src/main/java/foundation/e/flowmvi/MVIView.kt delete mode 100644 flow-mvi/src/main/java/foundation/e/flowmvi/MyClass.kt create mode 100644 flow-mvi/src/main/java/foundation/e/flowmvi/Store.kt create mode 100644 flow-mvi/src/main/java/foundation/e/flowmvi/Types.kt create mode 100644 flow-mvi/src/main/java/foundation/e/flowmvi/feature/BaseFeature.kt create mode 100644 flow-mvi/src/main/java/foundation/e/flowmvi/feature/Feature.kt (limited to 'flow-mvi/src/main/java/foundation') 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 . + */ + +package foundation.e.flowmvi + +import kotlinx.coroutines.flow.Flow + +interface MVIView { + + fun render(state: State) + + fun actions(): Flow +} diff --git a/flow-mvi/src/main/java/foundation/e/flowmvi/MyClass.kt b/flow-mvi/src/main/java/foundation/e/flowmvi/MyClass.kt deleted file mode 100644 index 32b47a1..0000000 --- a/flow-mvi/src/main/java/foundation/e/flowmvi/MyClass.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 . - */ - -package foundation.e.flowmvi - -class MyClass 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 . + */ + +package foundation.e.flowmvi + +import kotlinx.coroutines.flow.StateFlow + +interface Store { + val state: StateFlow +} 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 . + */ + +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: State, action: Action) -> Flow + +/** + * 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: State, effect: Effect) -> State + +typealias SingleEventProducer = (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 . + */ + +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( + initialState: State, + private val actor: Actor, + private val reducer: Reducer, + private val coroutineScope: CoroutineScope, + private val defaultLogger: Logger = {}, + private val singleEventProducer: SingleEventProducer? = null +) : + Feature { + + private val mutex = Mutex() + + private val _state = MutableStateFlow(initialState) + override val state: StateFlow = _state.asStateFlow() + + private val singleEventChannel = Channel() + override val singleEvents: Flow = singleEventChannel.receiveAsFlow() + + override fun takeView( + viewCoroutineScope: CoroutineScope, + view: MVIView, + initialActions: List, + logger: Logger? + ) { + viewCoroutineScope.launch { + sendStateUpdatesIntoView(this, view, logger ?: defaultLogger) + handleViewActions(this, view, initialActions, logger ?: defaultLogger) + } + } + + private fun sendStateUpdatesIntoView( + callerCoroutineScope: CoroutineScope, + view: MVIView, + 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, + initialActions: List, + 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, logger: Logger?) { + coroutineScope.launch { + actions.collectIntoHandler(this, logger ?: defaultLogger) + } + } + + private suspend fun Flow.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 feature( + initialState: State, + actor: Actor, + reducer: Reducer, + coroutineScope: CoroutineScope, + defaultLogger: Logger = {}, + singleEventProducer: SingleEventProducer? = 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 . + */ + +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 : Store { + val singleEvents: Flow + + /** + * 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, + initialActions: List = 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, + logger: Logger? = null + ) +} -- cgit v1.2.1