summaryrefslogtreecommitdiff
path: root/flow-mvi/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'flow-mvi/src/main')
-rw-r--r--flow-mvi/src/main/java/foundation/e/flowmvi/MVIView.kt27
-rw-r--r--flow-mvi/src/main/java/foundation/e/flowmvi/Store.kt24
-rw-r--r--flow-mvi/src/main/java/foundation/e/flowmvi/Types.kt42
-rw-r--r--flow-mvi/src/main/java/foundation/e/flowmvi/feature/BaseFeature.kt144
-rw-r--r--flow-mvi/src/main/java/foundation/e/flowmvi/feature/Feature.kt62
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
+ )
+}