├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── jitpack.yml
├── .github
├── sh
│ ├── parse_gradle_property.sh
│ ├── update_release_version.sh
│ ├── validate_has_git_changes.sh
│ ├── update_gradle_property.sh
│ ├── validate_version_format.sh
│ ├── validate_version_update.sh
│ ├── validate_publishing_branch.sh
│ └── validate_version_increased.sh
├── changelogconfig
│ ├── plugin-configuration.json
│ └── configuration.json
└── workflows
│ ├── code-quality.yml
│ ├── publish-to-maven.yml
│ └── release.yml
├── samples
├── coroutines-loader
│ ├── src
│ │ └── main
│ │ │ ├── res
│ │ │ ├── mipmap
│ │ │ │ └── ic_launcher.png
│ │ │ ├── values
│ │ │ │ ├── themes.xml
│ │ │ │ └── strings.xml
│ │ │ ├── layout
│ │ │ │ ├── activity_main.xml
│ │ │ │ └── fragment_main.xml
│ │ │ └── xml
│ │ │ │ └── disable_backup.xml
│ │ │ ├── kotlin
│ │ │ └── money
│ │ │ │ └── vivid
│ │ │ │ └── elmslie
│ │ │ │ └── samples
│ │ │ │ └── coroutines
│ │ │ │ └── timer
│ │ │ │ ├── elm
│ │ │ │ ├── StoreFactory.kt
│ │ │ │ ├── TimerModels.kt
│ │ │ │ ├── TimerActor.kt
│ │ │ │ └── TimerReducer.kt
│ │ │ │ ├── MainActivity.kt
│ │ │ │ └── MainFragment.kt
│ │ │ └── AndroidManifest.xml
│ └── build.gradle.kts
└── kotlin-calculator
│ ├── build.gradle.kts
│ └── src
│ ├── main
│ └── kotlin
│ │ └── money
│ │ └── vivid
│ │ └── elmslie
│ │ └── samples
│ │ └── calculator
│ │ ├── Models.kt
│ │ ├── Calculator.kt
│ │ └── Store.kt
│ └── test
│ └── kotlin
│ └── money
│ └── vivid
│ └── elmslie
│ └── samples
│ └── calculator
│ └── StoreTest.kt
├── elmslie-core
├── src
│ ├── commonMain
│ │ └── kotlin
│ │ │ └── money
│ │ │ └── vivid
│ │ │ └── elmslie
│ │ │ └── core
│ │ │ ├── logger
│ │ │ ├── LogSeverity.kt
│ │ │ ├── strategy
│ │ │ │ ├── IgnoreLog.kt
│ │ │ │ └── LogStrategy.kt
│ │ │ ├── ElmslieLogConfiguration.kt
│ │ │ └── ElmslieLogger.kt
│ │ │ ├── utils
│ │ │ ├── DispatcherProvider.kt
│ │ │ └── ResolveStoreKey.kt
│ │ │ ├── store
│ │ │ ├── NoOpReducer.kt
│ │ │ ├── NoOpActor.kt
│ │ │ ├── dsl
│ │ │ │ ├── OperationsBuilder.kt
│ │ │ │ └── ResultBuilder.kt
│ │ │ ├── StateReducer.kt
│ │ │ ├── StoreListener.kt
│ │ │ ├── Result.kt
│ │ │ ├── ScreenReducer.kt
│ │ │ ├── Store.kt
│ │ │ ├── EffectCachingElmStore.kt
│ │ │ ├── Actor.kt
│ │ │ └── ElmStore.kt
│ │ │ ├── ElmScope.kt
│ │ │ ├── config
│ │ │ └── ElmslieConfig.kt
│ │ │ └── switcher
│ │ │ └── Switcher.kt
│ ├── jvmMain
│ │ └── kotlin
│ │ │ └── money
│ │ │ └── vivid
│ │ │ └── elmslie
│ │ │ └── core
│ │ │ └── utils
│ │ │ ├── DispatcherProvider.kt
│ │ │ └── ResolveStoreKey.kt
│ ├── nativeMain
│ │ └── kotlin
│ │ │ └── money
│ │ │ └── vivid
│ │ │ └── elmslie
│ │ │ └── core
│ │ │ └── utils
│ │ │ ├── DispatcherProvider.kt
│ │ │ └── ResolveStoreKey.kt
│ ├── commonWebMain
│ │ └── kotlin
│ │ │ └── money
│ │ │ └── vivid
│ │ │ └── elmslie
│ │ │ └── core
│ │ │ └── utils
│ │ │ ├── DispatcherProvider.kt
│ │ │ └── ResolveStoreKey.kt
│ └── commonTest
│ │ └── kotlin
│ │ └── money
│ │ └── vivid
│ │ └── elmslie
│ │ └── core
│ │ ├── testutil
│ │ └── model
│ │ │ └── StoreModels.kt
│ │ └── store
│ │ ├── dsl
│ │ ├── Models.kt
│ │ ├── ScreenReducerTest.kt
│ │ └── DslReducerTest.kt
│ │ ├── EffectCachingElmStoreTest.kt
│ │ └── ElmStoreTest.kt
├── build.gradle.kts
└── api
│ └── elmslie-core.api
├── elmslie-android
├── src
│ └── main
│ │ ├── kotlin
│ │ └── money
│ │ │ └── vivid
│ │ │ └── elmslie
│ │ │ └── android
│ │ │ ├── util
│ │ │ └── FastLazy.kt
│ │ │ ├── processdeath
│ │ │ ├── ProcessDeathDetectorInitializer.kt
│ │ │ ├── EmptyActivityLifecycleCallbacks.kt
│ │ │ └── ProcessDeathDetector.kt
│ │ │ ├── logger
│ │ │ ├── DefaultLoggerConfigurations.kt
│ │ │ ├── strategy
│ │ │ │ ├── AndroidLog.kt
│ │ │ │ └── Crash.kt
│ │ │ ├── DefaultLoggerInitializer.kt
│ │ │ └── EmptyContentProvider.kt
│ │ │ ├── renderer
│ │ │ ├── ElmRenderer.kt
│ │ │ └── ElmRendererDelegate.kt
│ │ │ └── ElmStoreLazy.kt
│ │ └── AndroidManifest.xml
├── build.gradle.kts
├── detekt-baseline.xml
└── api
│ └── elmslie-android.api
├── settings.gradle.kts
├── .gitignore
├── gradle.properties
├── README.md
├── gradlew.bat
├── gradlew
├── LICENSE
└── detekt
└── detekt.yml
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vivid-money/elmslie/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/jitpack.yml:
--------------------------------------------------------------------------------
1 | jdk:
2 | - zulu17
3 | before_install:
4 | - sdk install java 17.0.7-zulu
5 | - sdk use java 17.0.7-zulu
--------------------------------------------------------------------------------
/.github/sh/parse_gradle_property.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | PROPERTY="$1"
3 | FILE="gradle.properties"
4 | sed -En "s/^$PROPERTY=([^\n]+)$/\1/p" "$FILE"
5 |
--------------------------------------------------------------------------------
/samples/coroutines-loader/src/main/res/mipmap/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vivid-money/elmslie/HEAD/samples/coroutines-loader/src/main/res/mipmap/ic_launcher.png
--------------------------------------------------------------------------------
/.github/sh/update_release_version.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | KEY=$1
3 | VALUE=$2
4 | ./.github/sh/update_gradle_property.sh "$KEY" "$VALUE"
5 | ./.github/sh/validate_has_git_changes.sh
6 |
--------------------------------------------------------------------------------
/.github/sh/validate_has_git_changes.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Condition checks that git status is empty
3 | if [[ -z $(git status -s) ]]; then
4 | echo "::error ::No git changes"
5 | exit 1
6 | fi
--------------------------------------------------------------------------------
/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/logger/LogSeverity.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.logger
2 |
3 | enum class LogSeverity {
4 | Fatal,
5 | NonFatal,
6 | Debug,
7 | }
8 |
--------------------------------------------------------------------------------
/samples/coroutines-loader/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.github/sh/update_gradle_property.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | PROPERTY=$1
3 | VALUE=$2
4 | sed -E "s/^[#]*\s*$PROPERTY=.*/$PROPERTY=$VALUE/" gradle.properties >gradle.properties.tmp &&
5 | mv gradle.properties.tmp gradle.properties
6 |
--------------------------------------------------------------------------------
/samples/coroutines-loader/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Sample
3 | Start
4 | Stop
5 |
6 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/logger/strategy/IgnoreLog.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.logger.strategy
2 |
3 | /** Ignores all log events */
4 | object IgnoreLog : LogStrategy by LogStrategy({ _, _, _, _ -> })
5 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/utils/DispatcherProvider.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.utils
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 |
5 | internal expect val ElmDispatcher: CoroutineDispatcher
6 |
--------------------------------------------------------------------------------
/.github/sh/validate_version_format.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | VERSION=$1
3 | EXPECTED='^([0-9]+){1,3}\.([0-9]+){1,9}\.([0-9]+){1,9}(-(alpha|beta|rc)[0-9]{2})?$'
4 | if [[ ! $VERSION =~ $EXPECTED ]]; then
5 | echo "::error ::Invalid version format: $VERSION"
6 | exit 1
7 | fi
8 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/utils/ResolveStoreKey.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.utils
2 |
3 | import money.vivid.elmslie.core.store.StateReducer
4 |
5 | internal expect fun resolveStoreKey(reducer: StateReducer<*, *, *, *>): String
6 |
--------------------------------------------------------------------------------
/elmslie-android/src/main/kotlin/money/vivid/elmslie/android/util/FastLazy.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.android.util
2 |
3 | /** Lazy initialization without synchronization */
4 | internal fun fastLazy(initializer: () -> T) = lazy(LazyThreadSafetyMode.NONE) { initializer() }
5 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
4 | networkTimeout=10000
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/.github/sh/validate_version_update.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | VERSION_PROPERTY=$1
3 | NEW_VERSION=$2
4 | OLD_VERSION=$(./.github/sh/parse_gradle_property.sh "$VERSION_PROPERTY")
5 | ./.github/sh/validate_version_format.sh "$NEW_VERSION"
6 | ./.github/sh/validate_version_increased.sh "$OLD_VERSION" "$NEW_VERSION"
7 |
--------------------------------------------------------------------------------
/elmslie-core/src/jvmMain/kotlin/money/vivid/elmslie/core/utils/DispatcherProvider.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.utils
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 | import kotlinx.coroutines.Dispatchers
5 |
6 | internal actual val ElmDispatcher: CoroutineDispatcher = Dispatchers.Default
7 |
--------------------------------------------------------------------------------
/elmslie-core/src/nativeMain/kotlin/money/vivid/elmslie/core/utils/DispatcherProvider.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.utils
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 | import kotlinx.coroutines.Dispatchers
5 |
6 | internal actual val ElmDispatcher: CoroutineDispatcher = Dispatchers.Default
7 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonWebMain/kotlin/money/vivid/elmslie/core/utils/DispatcherProvider.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.utils
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 | import kotlinx.coroutines.Dispatchers
5 |
6 | internal actual val ElmDispatcher: CoroutineDispatcher = Dispatchers.Default
7 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonTest/kotlin/money/vivid/elmslie/core/testutil/model/StoreModels.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.testutil.model
2 |
3 | data class Event(val value: Int = 0)
4 |
5 | data class State(val value: Int = 0)
6 |
7 | data class Effect(val value: Int = 0)
8 |
9 | data class Command(val value: Int = 0)
10 |
--------------------------------------------------------------------------------
/samples/coroutines-loader/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonWebMain/kotlin/money/vivid/elmslie/core/utils/ResolveStoreKey.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.utils
2 |
3 | import money.vivid.elmslie.core.store.StateReducer
4 |
5 | internal actual fun resolveStoreKey(reducer: StateReducer<*, *, *, *>): String =
6 | reducer::class.simpleName.orEmpty().replace("Reducer", "Store")
7 |
--------------------------------------------------------------------------------
/.github/changelogconfig/plugin-configuration.json:
--------------------------------------------------------------------------------
1 | {
2 | "categories": [
3 | {
4 | "title": "### Changelog",
5 | "labels": [
6 | "plugin"
7 | ]
8 | }
9 | ],
10 | "sort": "ASC",
11 | "template": "${{CHANGELOG}}",
12 | "pr_template": "- ${{TITLE}}. See: #${{NUMBER}}",
13 | "empty_template": "- No changes"
14 | }
15 |
--------------------------------------------------------------------------------
/elmslie-core/src/jvmMain/kotlin/money/vivid/elmslie/core/utils/ResolveStoreKey.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.utils
2 |
3 | import money.vivid.elmslie.core.store.StateReducer
4 |
5 | internal actual fun resolveStoreKey(reducer: StateReducer<*, *, *, *>): String =
6 | (reducer::class.qualifiedName ?: reducer::class.simpleName).orEmpty().replace("Reducer", "Store")
7 |
--------------------------------------------------------------------------------
/elmslie-core/src/nativeMain/kotlin/money/vivid/elmslie/core/utils/ResolveStoreKey.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.utils
2 |
3 | import money.vivid.elmslie.core.store.StateReducer
4 |
5 | internal actual fun resolveStoreKey(reducer: StateReducer<*, *, *, *>): String =
6 | (reducer::class.qualifiedName ?: reducer::class.simpleName).orEmpty().replace("Reducer", "Store")
7 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/logger/strategy/LogStrategy.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.logger.strategy
2 |
3 | import money.vivid.elmslie.core.logger.LogSeverity
4 |
5 | /** Allows to provide custom logic for error handling */
6 | fun interface LogStrategy {
7 | fun log(severity: LogSeverity, tag: String?, message: String, throwable: Throwable?)
8 | }
9 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/store/NoOpReducer.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.store
2 |
3 | /** Reducer that doesn't change state, and doesn't emit commands or effects */
4 | class NoOpReducer :
5 | StateReducer() {
6 |
7 | override fun Result.reduce(event: Event) = Unit
8 | }
9 |
--------------------------------------------------------------------------------
/.github/sh/validate_publishing_branch.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if [[ ${GITHUB_REF##*/} == main ]]; then
4 | # Safe to publish from main branch
5 | exit 0
6 | elif [[ ${GITHUB_REF##*/} == release* ]]; then
7 | # Safe to publish from branches that have manually enabled publishing
8 | exit 0
9 | else
10 | echo "::error ::Can only release from main branch or branches that start with 'release'"
11 | exit 1
12 | fi
13 |
--------------------------------------------------------------------------------
/samples/kotlin-calculator/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("org.jetbrains.kotlin.jvm")
3 | id("elmslie.detekt")
4 | id("elmslie.spotless")
5 | id("elmslie.tests-convention")
6 | }
7 |
8 | dependencies {
9 | implementation(projects.elmslieCore)
10 | implementation(libs.kotlinx.coroutinesCore)
11 |
12 | testImplementation(projects.elmslieCore)
13 | testImplementation(libs.kotlinx.coroutinesTest)
14 | }
15 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/store/NoOpActor.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.store
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import kotlinx.coroutines.flow.emptyFlow
5 |
6 | /** Actor that doesn't emit any events after receiving a command */
7 | class NoOpActor : Actor() {
8 |
9 | override fun execute(command: Command): Flow = emptyFlow()
10 | }
11 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/store/dsl/OperationsBuilder.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.store.dsl
2 |
3 | @DslMarker internal annotation class OperationsBuilderDsl
4 |
5 | @OperationsBuilderDsl
6 | class OperationsBuilder {
7 |
8 | private val list = mutableListOf()
9 |
10 | operator fun T?.unaryPlus() {
11 | this?.let(list::add)
12 | }
13 |
14 | internal fun build() = list
15 | }
16 |
--------------------------------------------------------------------------------
/samples/coroutines-loader/src/main/kotlin/money/vivid/elmslie/samples/coroutines/timer/elm/StoreFactory.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.samples.coroutines.timer.elm
2 |
3 | import money.vivid.elmslie.core.store.ElmStore
4 |
5 | internal fun storeFactory(id: String, generatedId: String?) =
6 | ElmStore(
7 | initialState = State(id = id, generatedId = generatedId),
8 | reducer = TimerReducer,
9 | actor = TimerActor,
10 | startEvent = Event.Init,
11 | )
12 |
--------------------------------------------------------------------------------
/.github/sh/validate_version_increased.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | OLD_VERSION=$1
3 | NEW_VERSION=$2
4 |
5 | MIN_VERSION=$(printf "%s\n%s" "$OLD_VERSION" "$NEW_VERSION" | sort -V | head -n 1)
6 |
7 | if [[ "$OLD_VERSION" == "$NEW_VERSION" ]]; then
8 | echo "::error ::Can't update to the same version"
9 | exit 1
10 | fi
11 |
12 | if [[ "$OLD_VERSION" != "$MIN_VERSION" ]]; then
13 | echo "::error ::Can't update to an older version. Old: $OLD_VERSION. New: $NEW_VERSION"
14 | exit 1
15 | fi
16 |
--------------------------------------------------------------------------------
/elmslie-android/src/main/kotlin/money/vivid/elmslie/android/processdeath/ProcessDeathDetectorInitializer.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.android.processdeath
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import androidx.startup.Initializer
6 |
7 | class ProcessDeathDetectorInitializer : Initializer {
8 |
9 | override fun create(context: Context) {
10 | ProcessDeathDetector.init(context.applicationContext as Application)
11 | }
12 |
13 | override fun dependencies(): MutableList>> = mutableListOf()
14 | }
15 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/store/StateReducer.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.store
2 |
3 | import money.vivid.elmslie.core.store.dsl.ResultBuilder
4 |
5 | abstract class StateReducer {
6 |
7 | // Needed to type less code
8 | protected inner class Result(state: State) : ResultBuilder(state)
9 |
10 | protected abstract fun Result.reduce(event: Event)
11 |
12 | fun reduce(event: Event, state: State) = Result(state).apply { reduce(event) }.build()
13 | }
14 |
--------------------------------------------------------------------------------
/samples/coroutines-loader/src/main/res/xml/disable_backup.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/elmslie-android/src/main/kotlin/money/vivid/elmslie/android/logger/DefaultLoggerConfigurations.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.android.logger
2 |
3 | import money.vivid.elmslie.android.logger.strategy.AndroidLog
4 | import money.vivid.elmslie.android.logger.strategy.Crash
5 | import money.vivid.elmslie.core.config.ElmslieConfig
6 | import money.vivid.elmslie.core.logger.strategy.IgnoreLog
7 |
8 | fun ElmslieConfig.defaultReleaseLogger() = logger {
9 | fatal(Crash)
10 | nonfatal(IgnoreLog)
11 | debug(IgnoreLog)
12 | }
13 |
14 | fun ElmslieConfig.defaultDebugLogger() = logger {
15 | fatal(Crash)
16 | nonfatal(AndroidLog.E)
17 | debug(AndroidLog.E)
18 | }
19 |
--------------------------------------------------------------------------------
/elmslie-android/src/main/kotlin/money/vivid/elmslie/android/logger/strategy/AndroidLog.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.android.logger.strategy
2 |
3 | import android.util.Log
4 | import money.vivid.elmslie.core.logger.strategy.LogStrategy
5 |
6 | /** Uses default android logging mechanism for reporting */
7 | object AndroidLog {
8 |
9 | val E = log(Log::e)
10 | val W = log(Log::w)
11 | val I = log(Log::i)
12 | val D = log(Log::d)
13 | val V = log(Log::v)
14 |
15 | private fun log(log: (tag: String?, message: String?, throwable: Throwable?) -> Unit) =
16 | LogStrategy { _, tag, message, error ->
17 | log(tag, message, error)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/elmslie-core/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("elmslie.kotlin-multiplatform-lib")
3 | id("elmslie.publishing")
4 | alias(libs.plugins.binaryCompatibilityValidator)
5 | }
6 |
7 | elmsliePublishing {
8 | pom {
9 | name = "Elmslie core"
10 | description = "Elmslie is a minimalistic reactive implementation of TEA/ELM"
11 | }
12 | }
13 |
14 | kotlin {
15 | sourceSets {
16 | val commonMain by getting { dependencies { implementation(libs.kotlinx.coroutinesCore) } }
17 | val commonTest by getting {
18 | dependencies {
19 | implementation(libs.kotlinx.coroutinesTest)
20 | implementation(libs.kotlin.test)
21 | }
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/store/StoreListener.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.store
2 |
3 | interface StoreListener {
4 |
5 | fun onBeforeEvent(key: String, event: Event, currentState: State) {}
6 |
7 | fun onAfterEvent(key: String, newState: State, oldState: State, eventCause: Event) {}
8 |
9 | fun onEffect(key: String, effect: Effect, state: State) {}
10 |
11 | fun onCommand(key: String, command: Command, state: State) {}
12 |
13 | fun onReducerError(key: String, throwable: Throwable, event: Event) {}
14 |
15 | fun onActorError(key: String, throwable: Throwable, command: Command) {}
16 | }
17 |
--------------------------------------------------------------------------------
/elmslie-android/src/main/kotlin/money/vivid/elmslie/android/logger/DefaultLoggerInitializer.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.android.logger
2 |
3 | import android.content.Context
4 | import android.content.pm.ApplicationInfo
5 | import androidx.startup.Initializer
6 | import money.vivid.elmslie.core.config.ElmslieConfig
7 |
8 | class DefaultLoggerInitializer : Initializer {
9 |
10 | override fun create(context: Context) {
11 | val isDebug = 0 != context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
12 | if (isDebug) ElmslieConfig.defaultDebugLogger() else ElmslieConfig.defaultReleaseLogger()
13 | }
14 |
15 | override fun dependencies(): MutableList>> = mutableListOf()
16 | }
17 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/ElmScope.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core
2 |
3 | import kotlinx.coroutines.CoroutineExceptionHandler
4 | import kotlinx.coroutines.CoroutineName
5 | import kotlinx.coroutines.CoroutineScope
6 | import kotlinx.coroutines.SupervisorJob
7 | import money.vivid.elmslie.core.config.ElmslieConfig
8 |
9 | @Suppress("detekt.FunctionNaming")
10 | fun ElmScope(name: String): CoroutineScope =
11 | CoroutineScope(
12 | context =
13 | ElmslieConfig.elmDispatcher +
14 | SupervisorJob() +
15 | CoroutineName(name) +
16 | CoroutineExceptionHandler { _, throwable ->
17 | ElmslieConfig.logger.fatal("Unhandled error: $throwable")
18 | }
19 | )
20 |
--------------------------------------------------------------------------------
/elmslie-android/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("elmslie.android-lib")
3 | id("elmslie.publishing")
4 | alias(libs.plugins.binaryCompatibilityValidator)
5 | }
6 |
7 | android { namespace = "money.vivid.elmslie.android" }
8 |
9 | elmsliePublishing {
10 | pom {
11 | name = "Elmslie Android"
12 | description =
13 | "Elmslie is a minimalistic reactive implementation of TEA/ELM. Android specific. https://github.com/vivid-money/elmslie/"
14 | }
15 | }
16 |
17 | dependencies {
18 | api(projects.elmslieCore)
19 |
20 | implementation(libs.androidx.appcompat)
21 | implementation(libs.androidx.lifecycle.runtime)
22 | implementation(libs.androidx.lifecycle.viewmodel)
23 | implementation(libs.androidx.startup.runtime)
24 | }
25 |
--------------------------------------------------------------------------------
/elmslie-android/src/main/kotlin/money/vivid/elmslie/android/logger/strategy/Crash.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.android.logger.strategy
2 |
3 | import android.os.Handler
4 | import android.os.Looper
5 | import android.os.Message
6 | import money.vivid.elmslie.core.logger.LogSeverity
7 | import money.vivid.elmslie.core.logger.strategy.LogStrategy
8 |
9 | /** Strategy that performs a crash on every log event it receives. Use wisely. */
10 | object Crash : LogStrategy {
11 |
12 | private val errorHandler = Handler(Looper.getMainLooper()) { throw it.obj as Throwable }
13 |
14 | override fun log(severity: LogSeverity, tag: String?, message: String, throwable: Throwable?) {
15 | errorHandler.sendMessage(Message().apply { obj = throwable ?: Exception(message) })
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/samples/coroutines-loader/src/main/kotlin/money/vivid/elmslie/samples/coroutines/timer/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.samples.coroutines.timer
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.fragment.app.commit
6 | import kotlin.random.Random
7 |
8 | internal class MainActivity : AppCompatActivity(R.layout.activity_main) {
9 |
10 | @Suppress("MagicNumber")
11 | override fun onCreate(savedInstanceState: Bundle?) {
12 | super.onCreate(savedInstanceState)
13 | if (savedInstanceState == null) {
14 | supportFragmentManager.commit {
15 | val screenId = Random.nextInt(100).toString()
16 | replace(R.id.container, MainFragment.newInstance(screenId))
17 | }
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/store/Result.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.store
2 |
3 | /** Represents result of reduce function */
4 | data class Result(
5 | val state: State,
6 | val effects: List,
7 | val commands: List,
8 | ) {
9 |
10 | constructor(
11 | state: State,
12 | effect: Effect? = null,
13 | command: Command? = null,
14 | ) : this(state = state, effects = listOfNotNull(effect), commands = listOfNotNull(command))
15 |
16 | constructor(
17 | state: State,
18 | commands: List,
19 | ) : this(state = state, effects = emptyList(), commands = commands)
20 |
21 | constructor(state: State) : this(state = state, effects = emptyList(), commands = emptyList())
22 | }
23 |
--------------------------------------------------------------------------------
/samples/coroutines-loader/src/main/kotlin/money/vivid/elmslie/samples/coroutines/timer/elm/TimerModels.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.samples.coroutines.timer.elm
2 |
3 | internal data class State(
4 | val id: String,
5 | val secondsPassed: Long = 0,
6 | val generatedId: String? = null,
7 | val isStarted: Boolean = false,
8 | )
9 |
10 | internal sealed class Effect {
11 | data class Error(val throwable: Throwable) : Effect()
12 | }
13 |
14 | internal sealed class Command {
15 | object Start : Command()
16 |
17 | object Stop : Command()
18 | }
19 |
20 | internal sealed class Event {
21 | object Init : Event()
22 |
23 | object Start : Event()
24 |
25 | object Stop : Event()
26 |
27 | object OnTimeTick : Event()
28 |
29 | data class OnTimeError(val throwable: Throwable) : Event()
30 | }
31 |
--------------------------------------------------------------------------------
/.github/changelogconfig/configuration.json:
--------------------------------------------------------------------------------
1 | {
2 | "categories": [
3 | {
4 | "title": "### Notable Changes",
5 | "labels": [
6 | "enhancement"
7 | ]
8 | },
9 | {
10 | "title": "### Changelog",
11 | "labels": [
12 | "bug"
13 | ]
14 | },
15 | {
16 | "title": "### Dependency Updates",
17 | "labels": [
18 | "dependencies"
19 | ]
20 | },
21 | {
22 | "title": "### Housekeeping & Refactoring",
23 | "labels": [
24 | "housekeeping"
25 | ]
26 | }
27 | ],
28 | "sort": "ASC",
29 | "template": "${{CHANGELOG}}\n\n\nUncategorized
\n\n${{UNCATEGORIZED}}\n ",
30 | "pr_template": "- ${{TITLE}}. See: #${{NUMBER}}",
31 | "empty_template": "- No changes"
32 | }
33 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage")
2 |
3 | pluginManagement {
4 | includeBuild("build-logic")
5 | repositories {
6 | gradlePluginPortal()
7 | google()
8 | mavenCentral()
9 | }
10 | }
11 |
12 | dependencyResolutionManagement {
13 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
14 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
15 | repositories {
16 | google()
17 | mavenCentral()
18 | }
19 | }
20 |
21 | rootProject.name = "Elmslie"
22 |
23 | include(":elmslie-android")
24 | include(":elmslie-core")
25 |
26 | include(":sample-coroutines-loader")
27 | project(":sample-coroutines-loader").projectDir = file("samples/coroutines-loader")
28 | include(":sample-kotlin-calculator")
29 | project(":sample-kotlin-calculator").projectDir = file("samples/kotlin-calculator")
30 |
31 |
--------------------------------------------------------------------------------
/elmslie-android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
10 |
13 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/elmslie-android/src/main/kotlin/money/vivid/elmslie/android/processdeath/EmptyActivityLifecycleCallbacks.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.android.processdeath
2 |
3 | import android.app.Activity
4 | import android.app.Application
5 | import android.os.Bundle
6 |
7 | internal interface EmptyActivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks {
8 |
9 | override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit
10 |
11 | override fun onActivityStarted(activity: Activity) = Unit
12 |
13 | override fun onActivityResumed(activity: Activity) = Unit
14 |
15 | override fun onActivityPaused(activity: Activity) = Unit
16 |
17 | override fun onActivityStopped(activity: Activity) = Unit
18 |
19 | override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
20 |
21 | override fun onActivityDestroyed(activity: Activity) = Unit
22 | }
23 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonTest/kotlin/money/vivid/elmslie/core/store/dsl/Models.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.store.dsl
2 |
3 | data class TestState(val one: Int, val two: Int)
4 |
5 | sealed class TestScreenEvent {
6 |
7 | sealed class Ui : TestScreenEvent() {
8 | object One : Ui()
9 | }
10 |
11 | sealed class Internal : TestScreenEvent() {
12 | object One : Internal()
13 | }
14 | }
15 |
16 | sealed class TestEvent {
17 | object One : TestEvent()
18 |
19 | object Two : TestEvent()
20 |
21 | object Three : TestEvent()
22 |
23 | data class Four(val flag: Boolean) : TestEvent()
24 |
25 | object Five : TestEvent()
26 |
27 | data class Six(val flag: Boolean) : TestEvent()
28 | }
29 |
30 | sealed class TestEffect {
31 | object One : TestEffect()
32 | }
33 |
34 | sealed class TestCommand {
35 | object One : TestCommand()
36 |
37 | object Two : TestCommand()
38 | }
39 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/store/ScreenReducer.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.store
2 |
3 | import kotlin.reflect.KClass
4 |
5 | abstract class ScreenReducer<
6 | Event : Any,
7 | Ui : Any,
8 | Internal : Any,
9 | State : Any,
10 | Effect : Any,
11 | Command : Any,
12 | >(private val uiEventClass: KClass, private val internalEventClass: KClass) :
13 | StateReducer() {
14 |
15 | protected abstract fun Result.ui(event: Ui)
16 |
17 | protected abstract fun Result.internal(event: Internal)
18 |
19 | override fun Result.reduce(event: Event) {
20 | @Suppress("UNCHECKED_CAST")
21 | when {
22 | uiEventClass.isInstance(event) -> ui(event as Ui)
23 | internalEventClass.isInstance(event) -> internal(event as Internal)
24 | else -> error("Event ${event::class} is neither UI nor Internal")
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/samples/kotlin-calculator/src/main/kotlin/money/vivid/elmslie/samples/calculator/Models.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.samples.calculator
2 |
3 | sealed class Event {
4 | data class EnterDigit(val digit: Char) : Event()
5 |
6 | data class PerformOperation(val operation: Operation) : Event()
7 |
8 | object Evaluate : Event()
9 | }
10 |
11 | sealed class Effect {
12 | data class NotifyError(val text: String) : Effect()
13 |
14 | data class NotifyNewResult(val result: Int) : Effect()
15 | }
16 |
17 | data class State(
18 | val pendingOperation: Operation? =
19 | Operation.PLUS, /* The first value should be added after entering */
20 | val total: Int = 0,
21 | val input: Int = 0,
22 | )
23 |
24 | sealed interface Command
25 |
26 | enum class Operation(op: (Int, Int) -> Int) : (Int, Int) -> Int by op {
27 |
28 | TIMES(Int::times),
29 | PLUS(Int::plus),
30 | MINUS(Int::minus),
31 | DIVIDE(Int::div),
32 | }
33 |
--------------------------------------------------------------------------------
/samples/coroutines-loader/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/samples/coroutines-loader/src/main/kotlin/money/vivid/elmslie/samples/coroutines/timer/elm/TimerActor.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.samples.coroutines.timer.elm
2 |
3 | import kotlinx.coroutines.delay
4 | import kotlinx.coroutines.flow.Flow
5 | import kotlinx.coroutines.flow.flow
6 | import money.vivid.elmslie.core.store.Actor
7 |
8 | internal object TimerActor : Actor() {
9 |
10 | override fun execute(command: Command) =
11 | when (command) {
12 | is Command.Start ->
13 | secondsFlow()
14 | .switch(Command.Start::class)
15 | .mapEvents(eventMapper = { Event.OnTimeTick }, errorMapper = { Event.OnTimeError(it) })
16 |
17 | is Command.Stop -> cancelSwitchFlows(Command.Start::class).mapEvents()
18 | }
19 |
20 | @Suppress("MagicNumber")
21 | private fun secondsFlow(): Flow = flow {
22 | repeat(10) {
23 | delay(1000)
24 | emit(it)
25 | }
26 | error("Test error")
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.github/workflows/code-quality.yml:
--------------------------------------------------------------------------------
1 | name: Code quality
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - main
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout sources
14 | uses: actions/checkout@v2
15 |
16 | - name: Setup Java
17 | uses: actions/setup-java@v3
18 | with:
19 | distribution: zulu
20 | java-version: 17
21 |
22 | - name: Setup Gradle
23 | uses: gradle/gradle-build-action@v2
24 |
25 | - name : Run api compatibility check
26 | run: ./gradlew apiCheck
27 |
28 | - name: Run spotless
29 | run: ./gradlew spotlessCheck
30 |
31 | - name: Run detekt
32 | run: ./gradlew detekt
33 |
34 | - name: Build project
35 | run: ./gradlew assemble
36 |
37 | - name: Run unit tests
38 | run: ./gradlew test
39 |
40 | - name: Run android lint
41 | run: ./gradlew lint
42 |
--------------------------------------------------------------------------------
/samples/coroutines-loader/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | id("org.jetbrains.kotlin.android")
4 | id("elmslie.detekt")
5 | id("elmslie.spotless")
6 | }
7 |
8 | android {
9 | namespace = "money.vivid.elmslie.samples.coroutines.timer"
10 |
11 | compileSdk = 35
12 | buildToolsVersion = "35.0.0"
13 |
14 | buildFeatures { buildConfig = true }
15 |
16 | defaultConfig {
17 | minSdk = 21
18 | targetSdk = 35
19 | }
20 |
21 | compileOptions {
22 | targetCompatibility = JavaVersion.VERSION_11
23 | sourceCompatibility = JavaVersion.VERSION_11
24 | }
25 |
26 | kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() }
27 | }
28 |
29 | dependencies {
30 | implementation(projects.elmslieAndroid)
31 | implementation(projects.elmslieCore)
32 |
33 | implementation(libs.androidx.appcompat)
34 | implementation(libs.androidx.fragmentKtx)
35 | implementation(libs.google.material)
36 | implementation(libs.kotlinx.coroutinesCore)
37 | }
38 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/store/dsl/ResultBuilder.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.store.dsl
2 |
3 | import money.vivid.elmslie.core.store.Result
4 |
5 | open class ResultBuilder(val initialState: State) {
6 |
7 | private var currentState: State = initialState
8 | private val commandsBuilder = OperationsBuilder()
9 | private val effectsBuilder = OperationsBuilder()
10 |
11 | val state: State
12 | get() = currentState
13 |
14 | fun state(update: State.() -> State) {
15 | currentState = currentState.update()
16 | }
17 |
18 | fun commands(update: OperationsBuilder.() -> Unit) {
19 | commandsBuilder.update()
20 | }
21 |
22 | fun effects(update: OperationsBuilder.() -> Unit) {
23 | effectsBuilder.update()
24 | }
25 |
26 | internal fun build(): Result {
27 | return Result(currentState, effectsBuilder.build(), commandsBuilder.build())
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/logger/ElmslieLogConfiguration.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.logger
2 |
3 | import money.vivid.elmslie.core.logger.strategy.LogStrategy
4 |
5 | class ElmslieLogConfiguration {
6 |
7 | private val strategies = mutableMapOf()
8 |
9 | /** Report a certain bug in the client code */
10 | fun fatal(strategy: LogStrategy) = apply { strategies[LogSeverity.Fatal] = strategy }
11 |
12 | /** Report an error in client code which can be identified as bug with certainty */
13 | fun nonfatal(strategy: LogStrategy) = apply { strategies[LogSeverity.NonFatal] = strategy }
14 |
15 | /** Print informational message */
16 | fun debug(strategy: LogStrategy) = apply { strategies[LogSeverity.Debug] = strategy }
17 |
18 | /** Apply the same logging strategy to all log levels */
19 | fun always(strategy: LogStrategy) = apply {
20 | LogSeverity.entries.forEach { strategies[it] = strategy }
21 | }
22 |
23 | internal fun build() = ElmslieLogger(strategies)
24 | }
25 |
--------------------------------------------------------------------------------
/samples/kotlin-calculator/src/main/kotlin/money/vivid/elmslie/samples/calculator/Calculator.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.samples.calculator
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import kotlinx.coroutines.flow.filter
5 | import kotlinx.coroutines.flow.map
6 |
7 | class Calculator {
8 |
9 | private val store = createStore()
10 |
11 | fun digit(digit: Char) = store.accept(Event.EnterDigit(digit))
12 |
13 | fun plus() = operation(Operation.PLUS)
14 |
15 | fun minus() = operation(Operation.MINUS)
16 |
17 | fun times() = operation(Operation.TIMES)
18 |
19 | fun divide() = operation(Operation.DIVIDE)
20 |
21 | private fun operation(operation: Operation) = store.accept(Event.PerformOperation(operation))
22 |
23 | fun evaluate() = store.accept(Event.Evaluate)
24 |
25 | fun errors(): Flow =
26 | store.effects.filter { it is Effect.NotifyError }.map { it as Effect.NotifyError }
27 |
28 | fun results(): Flow =
29 | store.effects.filter { it is Effect.NotifyNewResult }.map { it as Effect.NotifyNewResult }
30 | }
31 |
--------------------------------------------------------------------------------
/elmslie-android/src/main/kotlin/money/vivid/elmslie/android/processdeath/ProcessDeathDetector.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.android.processdeath
2 |
3 | import android.app.Activity
4 | import android.app.Application
5 | import android.os.Bundle
6 |
7 | object ProcessDeathDetector {
8 |
9 | private var isFirstStart = true
10 |
11 | /** Will be true in one onCreate..onResume cycle */
12 | var isRestoringAfterProcessDeath = false
13 | private set
14 |
15 | internal fun init(app: Application) {
16 | app.registerActivityLifecycleCallbacks(
17 | object : EmptyActivityLifecycleCallbacks {
18 |
19 | override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
20 | if (!isRestoringAfterProcessDeath && isFirstStart && savedInstanceState != null) {
21 | isRestoringAfterProcessDeath = true
22 | }
23 | isFirstStart = false
24 | }
25 |
26 | override fun onActivityResumed(activity: Activity) {
27 | isRestoringAfterProcessDeath = false
28 | }
29 | }
30 | )
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/elmslie-android/src/main/kotlin/money/vivid/elmslie/android/logger/EmptyContentProvider.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.android.logger
2 |
3 | import android.content.ContentProvider
4 | import android.content.ContentValues
5 | import android.net.Uri
6 |
7 | internal abstract class EmptyContentProvider : ContentProvider() {
8 |
9 | open fun init() {}
10 |
11 | override fun onCreate(): Boolean {
12 | init()
13 | return true
14 | }
15 |
16 | override fun query(
17 | uri: Uri,
18 | projection: Array?,
19 | selection: String?,
20 | selectionArgs: Array?,
21 | sortOrder: String?,
22 | ) = error("not allowed")
23 |
24 | override fun getType(uri: Uri) = error("not allowed")
25 |
26 | override fun insert(uri: Uri, values: ContentValues?) = error("not allowed")
27 |
28 | override fun delete(uri: Uri, selection: String?, selectionArgs: Array?) =
29 | error("not allowed")
30 |
31 | override fun update(
32 | uri: Uri,
33 | values: ContentValues?,
34 | selection: String?,
35 | selectionArgs: Array?,
36 | ) = error("not allowed")
37 | }
38 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/logger/ElmslieLogger.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.logger
2 |
3 | import money.vivid.elmslie.core.logger.strategy.IgnoreLog
4 | import money.vivid.elmslie.core.logger.strategy.LogStrategy
5 |
6 | /** Logs events happening in the Elmslie library */
7 | class ElmslieLogger(private val strategy: Map) {
8 |
9 | fun fatal(message: String = "", tag: String? = null, error: Throwable? = null) =
10 | handle(severity = LogSeverity.Fatal, message = message, tag = tag, error = error)
11 |
12 | fun nonfatal(message: String = "", tag: String? = null, error: Throwable? = null) =
13 | handle(severity = LogSeverity.NonFatal, message, tag = tag, error = error)
14 |
15 | fun debug(message: String, tag: String? = null) =
16 | handle(severity = LogSeverity.Debug, message, tag = tag, error = null)
17 |
18 | private fun handle(severity: LogSeverity, message: String, tag: String?, error: Throwable?) {
19 | (strategy[severity] ?: IgnoreLog).log(
20 | severity = severity,
21 | message = message,
22 | tag = tag,
23 | throwable = error,
24 | )
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/elmslie-android/detekt-baseline.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LongParameterList:ElmStoreLazy.kt$( key: String = this::class.java.canonicalName ?: this::class.java.simpleName, viewModelStoreOwner: () -> ViewModelStoreOwner = { this }, savedStateRegistryOwner: () -> SavedStateRegistryOwner = { this }, defaultArgs: () -> Bundle = { arguments ?: bundleOf() }, saveState: Bundle.(State) -> Unit = {}, storeFactory: SavedStateHandle.() -> Store<Event, Effect, State>, )
6 | LongParameterList:ElmStoreLazy.kt$( key: String = this::class.java.canonicalName ?: this::class.java.simpleName, viewModelStoreOwner: () -> ViewModelStoreOwner = { this }, savedStateRegistryOwner: () -> SavedStateRegistryOwner = { this }, defaultArgs: () -> Bundle = { this.intent?.extras ?: bundleOf() }, saveState: Bundle.(State) -> Unit = {}, storeFactory: SavedStateHandle.() -> Store<Event, Effect, State>, )
7 | LongParameterList:ElmStoreLazy.kt$( key: String, viewModelStoreOwner: () -> ViewModelStoreOwner, savedStateRegistryOwner: () -> SavedStateRegistryOwner, defaultArgs: () -> Bundle, saveState: Bundle.(State) -> Unit, storeFactory: SavedStateHandle.() -> Store<Event, Effect, State>, )
8 |
9 |
10 |
--------------------------------------------------------------------------------
/samples/coroutines-loader/src/main/kotlin/money/vivid/elmslie/samples/coroutines/timer/elm/TimerReducer.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.samples.coroutines.timer.elm
2 |
3 | import java.util.UUID
4 | import money.vivid.elmslie.core.store.StateReducer
5 |
6 | internal object TimerReducer : StateReducer() {
7 |
8 | override fun Result.reduce(event: Event) =
9 | when (event) {
10 | is Event.Init -> {
11 | state {
12 | copy(
13 | isStarted = true,
14 | generatedId =
15 | if (state.generatedId == null) {
16 | UUID.randomUUID().toString()
17 | } else {
18 | state.generatedId
19 | },
20 | )
21 | }
22 | commands { +Command.Start }
23 | }
24 | is Event.Start -> {
25 | state { copy(isStarted = true) }
26 | commands { +Command.Start }
27 | }
28 | is Event.Stop -> {
29 | state { copy(isStarted = false) }
30 | commands { +Command.Stop }
31 | }
32 | is Event.OnTimeTick -> {
33 | state { copy(secondsPassed = secondsPassed + 1) }
34 | }
35 | is Event.OnTimeError -> {
36 | state { copy(isStarted = false) }
37 | effects { +Effect.Error(event.throwable) }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Mac
2 | .DS_Store
3 |
4 | # Built application files
5 | *.apk
6 | *.aar
7 | *.ap_
8 | *.aab
9 |
10 | # Files for the ART/Dalvik VM
11 | *.dex
12 |
13 | # Java class files
14 | *.class
15 |
16 | # Kotlin class files
17 | .kotlin/
18 |
19 | # Generated files
20 | bin/
21 | gen/
22 | out/
23 | # Uncomment the following line in case you need and you don't have the release build type files in your app
24 | # release/
25 |
26 | # Gradle files
27 | .gradle/
28 | build/
29 |
30 | # Local configuration file (sdk path, etc)
31 | local.properties
32 |
33 | # Proguard folder generated by Eclipse
34 | proguard/
35 |
36 | # Log Files
37 | *.log
38 |
39 | # Android Studio Navigation editor temp files
40 | .navigation/
41 |
42 | # Android Studio captures folder
43 | captures/
44 |
45 | # IntelliJ
46 | *.iml
47 | .idea/*
48 |
49 | # Keystore files
50 | # Uncomment the following lines if you do not want to check your keystore files in.
51 | #*.jks
52 | #*.keystore
53 |
54 | # External native build folder generated in Android Studio 2.2 and later
55 | .externalNativeBuild
56 | .cxx/
57 |
58 | # Google Services (e.g. APIs or Firebase)
59 | # google-services.json
60 |
61 | # Freeline
62 | freeline.py
63 | freeline/
64 | freeline_project_description.json
65 |
66 | # fastlane
67 | fastlane/report.xml
68 | fastlane/Preview.html
69 | fastlane/screenshots
70 | fastlane/test_output
71 | fastlane/readme.md
72 |
73 | # Version control
74 | vcs.xml
75 |
76 | # lint
77 | lint/intermediates/
78 | lint/generated/
79 | lint/outputs/
80 | lint/tmp/
81 | # lint/reports/
82 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/store/Store.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.store
2 |
3 | import kotlinx.coroutines.CoroutineScope
4 | import kotlinx.coroutines.flow.Flow
5 | import kotlinx.coroutines.flow.StateFlow
6 |
7 | interface Store {
8 |
9 | /** Event that will be emitted upon store start. */
10 | val startEvent: Event?
11 |
12 | /** Store's scope. Active for the lifetime of store. */
13 | val scope: CoroutineScope
14 |
15 | /**
16 | * Returns the flow of [State]. Internally the store keeps the last emitted state value, so each
17 | * new subscribers will get it.
18 | *
19 | * Note that there will be no emission if a state isn't changed (it's [equals] method returned
20 | * `true`.
21 | *
22 | * By default, [State] is collected in [Dispatchers.Default].
23 | */
24 | val states: StateFlow
25 |
26 | /**
27 | * Returns the flow of [Effect]. It's a _hot_ flow and values produced by it **don't cache**.
28 | *
29 | * In order to implement cache of [Effect], consider extending [Store] with appropriate behavior.
30 | *
31 | * By default, [Effect] is collected in [Dispatchers.Default].
32 | */
33 | val effects: Flow
34 |
35 | /** Starts the operations inside the store. */
36 | fun start(): Store
37 |
38 | /**
39 | * Stops all operations inside the store and cancels coroutines scope. After this any calls of
40 | * [start] method has no effect.
41 | */
42 | fun stop()
43 |
44 | /** Sends a new [Event] for the store. */
45 | fun accept(event: Event)
46 | }
47 |
--------------------------------------------------------------------------------
/.github/workflows/publish-to-maven.yml:
--------------------------------------------------------------------------------
1 | name: Publish to Maven Central
2 |
3 | on: workflow_dispatch
4 |
5 | env:
6 | NEW_VERSION: ${{ github.event.inputs.version }}
7 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALUSERNAME }}
8 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALPASSWORD }}
9 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ORG_GRADLE_PROJECT_SIGNINGINMEMORYKEY }}
10 | ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.ORG_GRADLE_PROJECT_SIGNINGINMEMORYKEYID }}
11 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.ORG_GRADLE_PROJECT_SIGNINGINMEMORYKEYPASSWORD }}
12 |
13 | jobs:
14 | validate:
15 | runs-on: macos-latest
16 | steps:
17 | - name: Checkout
18 | uses: actions/checkout@v4
19 |
20 | - name: Setup node
21 | uses: actions/setup-node@v4
22 | with:
23 | node-version: 20
24 |
25 | - name: Validate Gradle Wrapper
26 | uses: gradle/actions/wrapper-validation@v3
27 |
28 | - name: Setup Java
29 | uses: actions/setup-java@v3
30 | with:
31 | distribution: zulu
32 | java-version: 17
33 |
34 | - name: Setup Gradle
35 | uses: gradle/actions/setup-gradle@v3
36 |
37 | - name: Ensure main branch
38 | run: ./.github/sh/validate_publishing_branch.sh
39 |
40 | - name: Validate publishing
41 | run: |
42 | ./gradlew \
43 | -xtest \
44 | -xlint \
45 | publishToMavenLocal
46 |
47 | - name: Publishing
48 | run: ./gradlew publishToMavenCentral
--------------------------------------------------------------------------------
/samples/coroutines-loader/src/main/res/layout/fragment_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
16 |
17 |
23 |
24 |
30 |
31 |
39 |
40 |
48 |
49 |
--------------------------------------------------------------------------------
/samples/kotlin-calculator/src/main/kotlin/money/vivid/elmslie/samples/calculator/Store.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.samples.calculator
2 |
3 | import money.vivid.elmslie.core.store.ElmStore
4 | import money.vivid.elmslie.core.store.NoOpActor
5 | import money.vivid.elmslie.core.store.StateReducer
6 | import money.vivid.elmslie.core.store.Store
7 |
8 | private const val MAX_INPUT_LENGTH = 9
9 |
10 | val Reducer =
11 | object : StateReducer() {
12 | override fun Result.reduce(event: Event) {
13 | when (event) {
14 | is Event.EnterDigit ->
15 | when {
16 | state.input.toString().length == MAX_INPUT_LENGTH -> {
17 | effects { +Effect.NotifyError("Reached max input length") }
18 | }
19 |
20 | event.digit.isDigit() -> {
21 | state { copy(input = "${state.input}${event.digit}".toInt()) }
22 | }
23 |
24 | else -> effects { +Effect.NotifyError("${event.digit} is not a digit") }
25 | }
26 |
27 | is Event.PerformOperation -> {
28 | val total = state.pendingOperation?.invoke(state.total, state.input) ?: state.total
29 | state { copy(pendingOperation = event.operation, total = total, input = 0) }
30 | effects { +Effect.NotifyNewResult(total) }
31 | }
32 |
33 | is Event.Evaluate -> {
34 | val total = state.pendingOperation?.invoke(state.total, state.input) ?: state.total
35 | state { copy(pendingOperation = null, total = total, input = 0) }
36 | effects { +Effect.NotifyNewResult(total) }
37 | }
38 | }
39 | }
40 | }
41 |
42 | fun createStore(): Store =
43 | ElmStore(initialState = State(), reducer = Reducer, actor = NoOpActor()).start()
44 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.10.0"
3 | coroutines = "1.9.0"
4 | dokka = "2.0.0"
5 | kotlin = "2.0.21"
6 | lifecycle = "2.9.0"
7 |
8 | [libraries]
9 | android-gradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "agp" }
10 | androidx-appcompat = "androidx.appcompat:appcompat:1.7.0"
11 | androidx-fragmentKtx = "androidx.fragment:fragment-ktx:1.8.6"
12 | androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "lifecycle" }
13 | androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycle" }
14 | androidx-startup-runtime = "androidx.startup:startup-runtime:1.2.0"
15 | detekt-gradlePlugin = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.8"
16 | dokka-gradlePlugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }
17 | google-material = "com.google.android.material:material:1.12.0"
18 | kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
19 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
20 | kotlinx-coroutinesCore = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
21 | kotlinx-coroutinesTest = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
22 | mavenPublishPlugin = { module = "com.vanniktech.maven.publish:com.vanniktech.maven.publish.gradle.plugin", version = "0.28.0" }
23 | spotless-gradlePlugin = "com.diffplug.spotless:spotless-plugin-gradle:7.0.3"
24 |
25 | [plugins]
26 | kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
27 | androidApplication = { id = "com.android.application", version.ref = "agp" }
28 | binaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.17.0" }
--------------------------------------------------------------------------------
/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/store/EffectCachingElmStore.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.store
2 |
3 | import kotlinx.coroutines.cancel
4 | import kotlinx.coroutines.flow.Flow
5 | import kotlinx.coroutines.flow.MutableSharedFlow
6 | import kotlinx.coroutines.flow.onSubscription
7 | import kotlinx.coroutines.launch
8 | import kotlinx.coroutines.sync.Mutex
9 | import kotlinx.coroutines.sync.withLock
10 | import money.vivid.elmslie.core.ElmScope
11 |
12 | /**
13 | * Caches effects until there is at least one collector.
14 | *
15 | * Note, that effects from the cache are replayed only for the first one.
16 | *
17 | * Wrap the store with the instance of [EffectCachingElmStore] to get the desired behavior like
18 | * this:
19 | * ```
20 | * ```
21 | */
22 | // TODO Should be moved to android artifact?
23 | class EffectCachingElmStore(
24 | private val elmStore: Store
25 | ) : Store by elmStore {
26 |
27 | private val effectsMutex = Mutex()
28 | private val effectsCache = mutableListOf()
29 | private val effectsFlow = MutableSharedFlow()
30 | private val storeScope = ElmScope("CachedStoreScope")
31 |
32 | init {
33 | storeScope.launch {
34 | elmStore.effects.collect { effect ->
35 | if (effectsFlow.subscriptionCount.value > 0) {
36 | effectsFlow.emit(effect)
37 | } else {
38 | effectsMutex.withLock { effectsCache.add(effect) }
39 | }
40 | }
41 | }
42 | }
43 |
44 | override fun stop() {
45 | elmStore.stop()
46 | storeScope.cancel()
47 | }
48 |
49 | override val effects: Flow =
50 | effectsFlow.onSubscription {
51 | effectsMutex.withLock {
52 | for (effect in effectsCache) {
53 | emit(effect)
54 | }
55 | effectsCache.clear()
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/config/ElmslieConfig.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.config
2 |
3 | import kotlin.concurrent.Volatile
4 | import kotlinx.coroutines.CoroutineDispatcher
5 | import kotlinx.coroutines.Dispatchers
6 | import money.vivid.elmslie.core.logger.ElmslieLogConfiguration
7 | import money.vivid.elmslie.core.logger.ElmslieLogger
8 | import money.vivid.elmslie.core.logger.strategy.IgnoreLog
9 | import money.vivid.elmslie.core.store.StoreListener
10 | import money.vivid.elmslie.core.utils.ElmDispatcher
11 |
12 | object ElmslieConfig {
13 |
14 | @Volatile
15 | var logger: ElmslieLogger = ElmslieLogConfiguration().apply { always(IgnoreLog) }.build()
16 | private set
17 |
18 | @Volatile
19 | var elmDispatcher: CoroutineDispatcher = ElmDispatcher
20 | private set
21 |
22 | @Volatile
23 | var shouldStopOnProcessDeath: Boolean = true
24 | private set
25 |
26 | @Volatile
27 | var globalStoreListeners: Set> = emptySet()
28 | private set
29 |
30 | /**
31 | * Configures logging and error handling
32 | *
33 | * Example:
34 | * ```
35 | * ElmslieConfig.logger {
36 | * fatal(Crash)
37 | * nonfatal(AndroidLog)
38 | * debug(Ignore)
39 | * }
40 | * ```
41 | */
42 | fun logger(config: (ElmslieLogConfiguration.() -> Unit)) {
43 | ElmslieLogConfiguration().apply(config).build().also { logger = it }
44 | }
45 |
46 | /**
47 | * Configures CoroutineDispatcher for performing operations in background. Default is
48 | * [Dispatchers.Default]
49 | */
50 | fun elmDispatcher(builder: () -> CoroutineDispatcher) {
51 | elmDispatcher = builder()
52 | }
53 |
54 | fun shouldStopOnProcessDeath(builder: () -> Boolean) {
55 | shouldStopOnProcessDeath = builder()
56 | }
57 |
58 | fun globalStoreListeners(builder: () -> Set>) {
59 | globalStoreListeners = builder()
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/switcher/Switcher.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.switcher
2 |
3 | import kotlin.time.Duration
4 | import kotlin.time.Duration.Companion.milliseconds
5 | import kotlinx.coroutines.channels.SendChannel
6 | import kotlinx.coroutines.delay
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.callbackFlow
9 | import kotlinx.coroutines.flow.catch
10 | import kotlinx.coroutines.flow.collect
11 | import kotlinx.coroutines.flow.onEach
12 | import kotlinx.coroutines.sync.Mutex
13 | import kotlinx.coroutines.sync.withLock
14 | import money.vivid.elmslie.core.store.Actor
15 |
16 | /**
17 | * Allows to execute requests for [Actor] implementations in a switching manner. Each request will
18 | * cancel the previous one.
19 | *
20 | * Example:
21 | * ```
22 | * private val switcher = Switcher()
23 | *
24 | * override fun execute(command: Command): Flow<*> = when (command) {
25 | * is MyCommand -> switcher.switch {
26 | * flowOf(123)
27 | * }
28 | * }
29 | * ```
30 | */
31 | internal class Switcher {
32 |
33 | private var currentChannel: SendChannel<*>? = null
34 | private val lock = Mutex()
35 |
36 | /**
37 | * Collect given flow as a job and cancels all previous ones.
38 | *
39 | * @param delay operation delay measured with milliseconds. Can be specified to debounce existing
40 | * requests.
41 | * @param action actual event source
42 | */
43 | fun switch(
44 | delay: Duration = 0.milliseconds,
45 | action: () -> Flow,
46 | ): Flow {
47 | return callbackFlow {
48 | lock.withLock {
49 | currentChannel?.close()
50 | currentChannel = channel
51 | }
52 |
53 | delay(delay)
54 |
55 | action.invoke().onEach { send(it) }.catch { close(it) }.collect()
56 |
57 | channel.close()
58 | }
59 | }
60 |
61 | suspend fun cancel() {
62 | lock.withLock {
63 | currentChannel?.close()
64 | currentChannel = null
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # ----- Gradle Options -----
2 | #
3 | # Specifies the JVM arguments used for the daemon process.
4 | # The setting is particularly useful for tweaking memory settings.
5 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
6 | # https://docs.gradle.org/current/userguide/multi_project_builds.html#sec:parallel_execution
7 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
8 | org.gradle.parallel=true
9 | # https://docs.gradle.org/current/userguide/multi_project_builds.html#sec:configuration_on_demand
10 | org.gradle.configureondemand=true
11 | # https://docs.gradle.org/current/userguide/gradle_daemon.html
12 | org.gradle.daemon=true
13 | # https://docs.gradle.org/current/userguide/build_cache.html
14 | org.gradle.caching=true
15 | #
16 | # ----- AndroidX -----
17 | #
18 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
19 | android.useAndroidX=true
20 | #
21 | # ----- Kotlin & Kapt -----
22 | #
23 | # Kotlin code style for this project: "official" or "obsolete":
24 | kotlin.code.style=official
25 | # https://kotlinlang.org/docs/reference/kapt.html#incremental-annotation-processing-since-1330
26 | kapt.incremental.apt=true
27 | # https://kotlinlang.org/docs/reference/kapt.html?_ga=2.52861352.147014511.1570608228-996117368.1567680991#compile-avoidance-for-kapt-since-1320
28 | kapt.include.compile.classpath=false
29 | #
30 | #
31 | # ----- Kotlin/Native features -----
32 | #
33 | kotlin.native.distribution.downloadFromMaven=true
34 | #
35 | # ----- AGP 4.0 build features -----
36 | #
37 | # disable build config generation
38 | android.defaults.buildfeatures.buildconfig=false
39 | # disable compileAidl tasks
40 | android.defaults.buildfeatures.aidl=false
41 | # disable (package/compile)Rendescript tasks
42 | android.defaults.buildfeatures.renderscript=false
43 | # disable providing custom values to resources from buildscript
44 | android.defaults.buildfeatures.resvalues=false
45 | # disable compileShaders tasks
46 | android.defaults.buildfeatures.shaders=false
47 | #
48 | # ----- Config -----
49 | #
50 | libraryVersion=3.0.0
51 | libraryGroup=money.vivid.elmslie
52 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/store/Actor.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.store
2 |
3 | import kotlin.time.Duration
4 | import kotlin.time.Duration.Companion.milliseconds
5 | import kotlinx.coroutines.flow.Flow
6 | import kotlinx.coroutines.flow.catch
7 | import kotlinx.coroutines.flow.flow
8 | import kotlinx.coroutines.flow.mapNotNull
9 | import kotlinx.coroutines.sync.Mutex
10 | import kotlinx.coroutines.sync.withLock
11 | import money.vivid.elmslie.core.config.ElmslieConfig
12 | import money.vivid.elmslie.core.switcher.Switcher
13 |
14 | abstract class Actor {
15 |
16 | private val switchers = mutableMapOf()
17 | private val mutex = Mutex()
18 |
19 | /** Executes a command. This method is performed on the [ElmslieConfig.elmDispatcher]. */
20 | abstract fun execute(command: Command): Flow
21 |
22 | protected fun Flow.mapEvents(
23 | eventMapper: (T) -> Event? = { null },
24 | errorMapper: (error: Throwable) -> Event? = { null },
25 | ) =
26 | mapNotNull { eventMapper(it) }
27 | .catch { it.logErrorEvent(errorMapper)?.let { event -> emit(event) } ?: throw it }
28 |
29 | /**
30 | * Extension function to switch the flow by a given key and optional delay. This function ensures
31 | * that only one flow with the same key is active at a time.
32 | *
33 | * @param key The key to identify the flow.
34 | * @param delay The delay in milliseconds before launching the initial flow. Defaults to 0.
35 | * @return A new flow that emits the values from the original flow.
36 | */
37 | protected fun Flow.switch(key: Any, delay: Duration = 0.milliseconds): Flow {
38 | return flow {
39 | val switcher = mutex.withLock { switchers.getOrPut(key) { Switcher() } }
40 | switcher.switch(delay) { this@switch }.collect { emit(it) }
41 | }
42 | }
43 |
44 | /**
45 | * Cancels the switch flow(s) by a given key(s).
46 | *
47 | * @param keys The keys to identify the flows.
48 | * @return A new flow that emits [Unit] when switch flows are cancelled.
49 | */
50 | protected fun cancelSwitchFlows(vararg keys: Any): Flow {
51 | return flow {
52 | keys.forEach { key -> mutex.withLock { switchers.remove(key)?.cancel() } }
53 | emit(Unit)
54 | }
55 | }
56 |
57 | private fun Throwable.logErrorEvent(errorMapper: (Throwable) -> Event?): Event? {
58 | return errorMapper(this).also { ElmslieConfig.logger.nonfatal(error = this) }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonTest/kotlin/money/vivid/elmslie/core/store/dsl/ScreenReducerTest.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.store.dsl
2 |
3 | import kotlin.test.Test
4 | import kotlin.test.assertEquals
5 | import kotlin.test.assertTrue
6 | import money.vivid.elmslie.core.store.ScreenReducer
7 | import money.vivid.elmslie.core.store.StateReducer
8 |
9 | object BasicScreenReducer :
10 | ScreenReducer<
11 | TestScreenEvent,
12 | TestScreenEvent.Ui,
13 | TestScreenEvent.Internal,
14 | TestState,
15 | TestEffect,
16 | TestCommand,
17 | >(TestScreenEvent.Ui::class, TestScreenEvent.Internal::class) {
18 |
19 | override fun Result.ui(event: TestScreenEvent.Ui) =
20 | when (event) {
21 | is TestScreenEvent.Ui.One -> state { copy(one = 1, two = 2) }
22 | }
23 |
24 | override fun Result.internal(event: TestScreenEvent.Internal) =
25 | when (event) {
26 | is TestScreenEvent.Internal.One ->
27 | commands {
28 | +TestCommand.One
29 | +TestCommand.Two
30 | }
31 | }
32 | }
33 |
34 | // The same code
35 | object PlainScreenDslReducer : StateReducer() {
36 |
37 | override fun Result.reduce(event: TestScreenEvent) =
38 | when (event) {
39 | is TestScreenEvent.Ui -> reduce(event)
40 | is TestScreenEvent.Internal -> reduce(event)
41 | }
42 |
43 | private fun Result.reduce(event: TestScreenEvent.Ui) =
44 | when (event) {
45 | is TestScreenEvent.Ui.One -> state { copy(one = 1, two = 2) }
46 | }
47 |
48 | private fun Result.reduce(event: TestScreenEvent.Internal) =
49 | when (event) {
50 | is TestScreenEvent.Internal.One ->
51 | commands {
52 | +TestCommand.One
53 | +TestCommand.Two
54 | }
55 | }
56 | }
57 |
58 | internal class ScreenReducerTest {
59 |
60 | private val reducer = BasicScreenReducer
61 |
62 | @Test
63 | fun `Ui event is executed`() {
64 | val initialState = TestState(one = 0, two = 0)
65 | val (state, effects, commands) = reducer.reduce(TestScreenEvent.Ui.One, initialState)
66 | assertEquals(state, TestState(one = 1, two = 2))
67 | assertTrue(effects.isEmpty())
68 | assertTrue(commands.isEmpty())
69 | }
70 |
71 | @Test
72 | fun `Internal event is executed`() {
73 | val initialState = TestState(one = 0, two = 0)
74 | val (state, effects, commands) = reducer.reduce(TestScreenEvent.Internal.One, initialState)
75 | assertEquals(state, initialState)
76 | assertTrue(effects.isEmpty())
77 | assertEquals(commands, listOf(TestCommand.One, TestCommand.Two))
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Publish library release
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | version:
7 | description: 'Library release version'
8 | required: true
9 |
10 | env:
11 | NEW_VERSION: ${{ github.event.inputs.version }}
12 |
13 | jobs:
14 | validate:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v2
18 | with:
19 | ref: ${{ env.GITHUB_REF }}
20 |
21 | - name: Setup Java
22 | uses: actions/setup-java@v3
23 | with:
24 | distribution: zulu
25 | java-version: 17
26 |
27 | - name: Ensure main branch
28 | run: ./.github/sh/validate_publishing_branch.sh
29 |
30 | - name: Validate library version update
31 | run: ./.github/sh/validate_version_update.sh "libraryVersion" "$NEW_VERSION"
32 |
33 | - name: Validate jitpack publishing
34 | run: |
35 | ./gradlew \
36 | -Pgroup=com.github.vivid-money \
37 | -Pversion=$VERSION \
38 | -xtest \
39 | -xlint \
40 | build \
41 | publishToMavenLocal
42 |
43 | release:
44 | needs: validate
45 | runs-on: ubuntu-latest
46 | steps:
47 | - uses: actions/checkout@v2
48 | with:
49 | ref: ${{ env.GITHUB_REF }}
50 |
51 | - name: Setup Java
52 | uses: actions/setup-java@v3
53 | with:
54 | distribution: zulu
55 | java-version: 17
56 |
57 | - name: Update library version
58 | run: ./.github/sh/update_release_version.sh "libraryVersion" "$NEW_VERSION"
59 |
60 | - uses: stefanzweifel/git-auto-commit-action@v4
61 | with:
62 | commit_message: "Update library version to ${{ env.NEW_VERSION }}"
63 | file_pattern: gradle.properties
64 | skip_dirty_check: true
65 |
66 | - name: Build Changelog
67 | id: github_release
68 | uses: mikepenz/release-changelog-builder-action@v1
69 | with:
70 | token: ${{ secrets.GITHUB_TOKEN }}
71 | toTag: HEAD
72 | failOnError: true
73 | configuration: .github/changelogconfig/configuration.json
74 |
75 | - name: Get current git commit SHA
76 | id: vars
77 | run: |
78 | calculatedSha=$(git rev-parse HEAD)
79 | echo "::set-output name=commit_sha::$calculatedSha"
80 |
81 | - name: Create github release
82 | uses: ncipollo/release-action@v1
83 | with:
84 | token: "${{ secrets.GITHUB_TOKEN }}"
85 | tag: "${{ env.NEW_VERSION }}"
86 | commit: "${{ steps.vars.outputs.commit_sha }}"
87 | body: ${{ steps.github_release.outputs.changelog }}
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | [](https://jitpack.io/#diklimchuk/test)
3 |
4 | [](https://central.sonatype.com/artifact/money.vivid.elmslie/elmslie-core)
5 | [](https://opensource.org/licenses/Apache-2.0)
6 |
7 | Elmslie is a minimalistic reactive implementation of TEA/ELM written in kotlin with java support.
8 | Named after [George Grant Elmslie](https://en.wikipedia.org/wiki/George_Grant_Elmslie), a Scottish-born architect.
9 |
10 | ## Why?
11 | - **Scalable and Reusable**: Built-in support for nesting components
12 | - **Multiplatform**: Written with pure Kotlin and Coroutines, supports KMP (Android, iOS, JS)
13 | - **Single immutable state**: Simplify state management
14 | - **UDF**: Say no to spaghetti code with Unidirectional Data Flow
15 |
16 | ## Documentation
17 | This is a visual representation of the architecture:
18 |
19 |
20 |
21 |
22 |
23 |
24 | For more info head to the [wiki](https://github.com/vivid-money/elmslie/wiki)
25 |
26 | ## Samples
27 | Samples are available [here](https://github.com/vivid-money/elmslie/tree/publish-elmslie-3.0/samples)
28 | - Basic loader for android: [link](https://github.com/vivid-money/elmslie/tree/publish-elmslie-3.0/samples/coroutines-loader)
29 | - Pure kotlin calculator: [link](https://github.com/vivid-money/elmslie/tree/publish-elmslie-3.0/samples/kotlin-calculator)
30 |
31 | ## Download
32 | Library is distributed through Maven Central
33 |
34 | #### Add repository in the root build.gradle
35 | ```kotlin
36 | allprojects {
37 | repositories {
38 | mavenCentral()
39 | }
40 | }
41 | ```
42 |
43 | #### Add required modules:
44 | - Core - for pure kotlin ELM implementation
45 |
46 | `implementation 'money.vivid.elmslie:elmslie-core:{latest-version}'`
47 |
48 | - Android - for android apps only, simplifies lifecycle handling
49 |
50 | `implementation 'money.vivid.elmslie:elmslie-android:{latest-version}'`
51 |
52 |
53 | ## Related articles
54 | - Why did we select ELM? ([Russian](https://habr.com/ru/company/vivid_money/blog/534386/), [English](https://medium.com/@klimchuk.daniil/how-we-chose-presentation-layer-architecture-and-didnt-regret-it-bc694cab3e80))
55 | - What is ELM architecture? ([Russian](https://habr.com/ru/company/vivid_money/blog/550932/))
56 | - How to use our library? ([Russian](https://habr.com/ru/company/vivid_money/blog/553232/))
57 |
--------------------------------------------------------------------------------
/elmslie-android/src/main/kotlin/money/vivid/elmslie/android/renderer/ElmRenderer.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.android.renderer
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import androidx.lifecycle.Lifecycle.State.RESUMED
5 | import androidx.lifecycle.Lifecycle.State.STARTED
6 | import androidx.lifecycle.coroutineScope
7 | import androidx.lifecycle.flowWithLifecycle
8 | import kotlinx.coroutines.CoroutineDispatcher
9 | import kotlinx.coroutines.flow.catch
10 | import kotlinx.coroutines.flow.flowOn
11 | import kotlinx.coroutines.flow.map
12 | import kotlinx.coroutines.launch
13 | import money.vivid.elmslie.core.config.ElmslieConfig
14 | import money.vivid.elmslie.core.store.Store
15 |
16 | internal class ElmRenderer(
17 | private val store: Store<*, Effect, State>,
18 | private val delegate: ElmRendererDelegate,
19 | private val lifecycle: Lifecycle,
20 | ) {
21 |
22 | private val logger = ElmslieConfig.logger
23 | private val elmDispatcher: CoroutineDispatcher = ElmslieConfig.elmDispatcher
24 | private val canRender
25 | get() = lifecycle.currentState.isAtLeast(STARTED)
26 |
27 | init {
28 | with(lifecycle) {
29 | coroutineScope.launch {
30 | store.effects.flowWithLifecycle(lifecycle = lifecycle, minActiveState = RESUMED).collect {
31 | effect ->
32 | catchEffectErrors { delegate.handleEffect(effect) }
33 | }
34 | }
35 | coroutineScope.launch {
36 | store.states
37 | .flowWithLifecycle(lifecycle = lifecycle, minActiveState = STARTED)
38 | .map { state ->
39 | val list = mapListItems(state)
40 | state to list
41 | }
42 | .catch { logger.fatal(message = "Crash while mapping state", error = it) }
43 | .flowOn(elmDispatcher)
44 | .collect { (state, listItems) ->
45 | catchStateErrors {
46 | if (canRender) {
47 | delegate.renderList(state, listItems)
48 | delegate.render(state)
49 | }
50 | }
51 | }
52 | }
53 | }
54 | }
55 |
56 | private fun mapListItems(state: State) =
57 | catchStateErrors { delegate.mapList(state) } ?: emptyList()
58 |
59 | @Suppress("TooGenericExceptionCaught")
60 | private fun catchStateErrors(action: () -> T?) =
61 | try {
62 | action()
63 | } catch (t: Throwable) {
64 | logger.fatal(message = "Crash while rendering state", error = t)
65 | null
66 | }
67 |
68 | @Suppress("TooGenericExceptionCaught")
69 | private fun catchEffectErrors(action: () -> T?) =
70 | try {
71 | action()
72 | } catch (t: Throwable) {
73 | logger.fatal(message = "Crash while handling effect", error = t)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/samples/coroutines-loader/src/main/kotlin/money/vivid/elmslie/samples/coroutines/timer/MainFragment.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.samples.coroutines.timer
2 |
3 | import android.annotation.SuppressLint
4 | import android.os.Bundle
5 | import android.view.View
6 | import android.view.View.GONE
7 | import android.view.View.VISIBLE
8 | import android.widget.Button
9 | import android.widget.TextView
10 | import androidx.core.os.bundleOf
11 | import androidx.fragment.app.Fragment
12 | import com.google.android.material.snackbar.Snackbar
13 | import money.vivid.elmslie.android.RetainedElmStore.Companion.StateBundleKey
14 | import money.vivid.elmslie.android.renderer.ElmRendererDelegate
15 | import money.vivid.elmslie.android.renderer.androidElmStore
16 | import money.vivid.elmslie.samples.coroutines.timer.elm.Effect
17 | import money.vivid.elmslie.samples.coroutines.timer.elm.Event
18 | import money.vivid.elmslie.samples.coroutines.timer.elm.State
19 | import money.vivid.elmslie.samples.coroutines.timer.elm.storeFactory
20 |
21 | internal class MainFragment : Fragment(R.layout.fragment_main), ElmRendererDelegate {
22 |
23 | companion object {
24 | private const val ARG = "ARG"
25 | private const val GENERATED_ID = "GENERATED_ID"
26 |
27 | fun newInstance(id: String): Fragment = MainFragment().apply { arguments = bundleOf(ARG to id) }
28 | }
29 |
30 | private val store by
31 | androidElmStore(saveState = { state -> putString(GENERATED_ID, state.generatedId) }) {
32 | storeFactory(
33 | id = get(ARG)!!,
34 | generatedId = get(StateBundleKey)?.getString(GENERATED_ID),
35 | )
36 | }
37 |
38 | private lateinit var startButton: Button
39 | private lateinit var stopButton: Button
40 | private lateinit var currentValueText: TextView
41 | private lateinit var screenIdText: TextView
42 | private lateinit var generatedIdText: TextView
43 |
44 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
45 | super.onViewCreated(view, savedInstanceState)
46 | startButton = view.findViewById(R.id.start)
47 | stopButton = view.findViewById(R.id.stop)
48 | currentValueText = view.findViewById(R.id.currentValue)
49 | screenIdText = view.findViewById(R.id.screenId)
50 | generatedIdText = view.findViewById(R.id.generatedID)
51 |
52 | startButton.setOnClickListener { store.accept(Event.Start) }
53 | stopButton.setOnClickListener { store.accept(Event.Stop) }
54 | }
55 |
56 | @SuppressLint("SetTextI18n")
57 | override fun render(state: State) {
58 | screenIdText.text = state.id
59 | generatedIdText.text = state.generatedId
60 | startButton.visibility = if (state.isStarted) GONE else VISIBLE
61 | stopButton.visibility = if (state.isStarted) VISIBLE else GONE
62 | currentValueText.text = "Seconds passed: ${state.secondsPassed}"
63 | }
64 |
65 | override fun handleEffect(effect: Effect) =
66 | when (effect) {
67 | is Effect.Error ->
68 | Snackbar.make(requireView().findViewById(R.id.content), "Error!", Snackbar.LENGTH_SHORT)
69 | .show()
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/elmslie-android/src/main/kotlin/money/vivid/elmslie/android/renderer/ElmRendererDelegate.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.android.renderer
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.annotation.MainThread
6 | import androidx.core.os.bundleOf
7 | import androidx.fragment.app.Fragment
8 | import androidx.lifecycle.LifecycleOwner
9 | import androidx.lifecycle.SavedStateHandle
10 | import androidx.lifecycle.ViewModelStoreOwner
11 | import androidx.lifecycle.lifecycleScope
12 | import androidx.lifecycle.withCreated
13 | import androidx.savedstate.SavedStateRegistryOwner
14 | import kotlinx.coroutines.launch
15 | import money.vivid.elmslie.android.elmStore
16 | import money.vivid.elmslie.core.store.Store
17 |
18 | @Suppress("OptionalUnit")
19 | interface ElmRendererDelegate {
20 | fun render(state: State)
21 |
22 | fun handleEffect(effect: Effect): Unit? = Unit
23 |
24 | fun mapList(state: State): List = emptyList()
25 |
26 | fun renderList(state: State, list: List): Unit = Unit
27 | }
28 |
29 | /**
30 | * The function makes a connection between the store and the lifecycle owner by collecting states
31 | * and effects and calling corresponds callbacks.
32 | *
33 | * Store creates and connects all required entities when given lifecycle reached CREATED state.
34 | *
35 | * In order to access previously saved state (via [saveState]) in [storeFactory] one must use
36 | * SavedStateHandle.get(StateBundleKey)
37 | *
38 | * NOTE: If you implement your own ElmRendererDelegate, you should also implement the following
39 | * interfaces: [ViewModelStoreOwner], [SavedStateRegistryOwner], [LifecycleOwner].
40 | */
41 | @Suppress("LongParameterList")
42 | @MainThread
43 | fun ElmRendererDelegate.androidElmStore(
44 | key: String = this::class.java.canonicalName ?: this::class.java.simpleName,
45 | defaultArgs: () -> Bundle = {
46 | val args =
47 | when (this) {
48 | is Fragment -> arguments
49 | is ComponentActivity -> intent.extras
50 | else -> null
51 | }
52 | args ?: bundleOf()
53 | },
54 | saveState: Bundle.(State) -> Unit = {},
55 | storeFactory: SavedStateHandle.() -> Store,
56 | ): Lazy> {
57 | require(this is ViewModelStoreOwner) { "Should implement [ViewModelStoreOwner]" }
58 | require(this is SavedStateRegistryOwner) { "Should implement [SavedStateRegistryOwner]" }
59 | return androidElmStore(
60 | key = key,
61 | viewModelStoreOwner = { this },
62 | savedStateRegistryOwner = { this },
63 | defaultArgs = defaultArgs,
64 | saveState = saveState,
65 | storeFactory = storeFactory,
66 | )
67 | }
68 |
69 | @Suppress("LongParameterList")
70 | @MainThread
71 | fun ElmRendererDelegate.androidElmStore(
72 | key: String = this::class.java.canonicalName ?: this::class.java.simpleName,
73 | viewModelStoreOwner: () -> ViewModelStoreOwner,
74 | savedStateRegistryOwner: () -> SavedStateRegistryOwner,
75 | defaultArgs: () -> Bundle = {
76 | val args =
77 | when (this) {
78 | is Fragment -> arguments
79 | is ComponentActivity -> intent.extras
80 | else -> null
81 | }
82 | args ?: bundleOf()
83 | },
84 | saveState: Bundle.(State) -> Unit = {},
85 | storeFactory: SavedStateHandle.() -> Store,
86 | ): Lazy> {
87 | require(this is LifecycleOwner) { "Should implement [LifecycleOwner]" }
88 | val lazyStore =
89 | elmStore(
90 | storeFactory = storeFactory,
91 | key = key,
92 | viewModelStoreOwner = viewModelStoreOwner,
93 | savedStateRegistryOwner = savedStateRegistryOwner,
94 | saveState = saveState,
95 | defaultArgs = defaultArgs,
96 | )
97 | with(this) {
98 | lifecycleScope.launch {
99 | withCreated {
100 | ElmRenderer(store = lazyStore.value, delegate = this@with, lifecycle = lifecycle)
101 | }
102 | }
103 | }
104 | return lazyStore
105 | }
106 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonTest/kotlin/money/vivid/elmslie/core/store/dsl/DslReducerTest.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.store.dsl
2 |
3 | import kotlin.test.Test
4 | import kotlin.test.assertEquals
5 | import kotlin.test.assertTrue
6 | import money.vivid.elmslie.core.store.StateReducer
7 |
8 | private object BasicDslReducer : StateReducer() {
9 |
10 | override fun Result.reduce(event: TestEvent) =
11 | when (event) {
12 | is TestEvent.One -> {
13 | state { copy(one = 1) }
14 | state { copy(two = 2) }
15 | }
16 | is TestEvent.Two -> effects { +TestEffect.One }
17 | is TestEvent.Three ->
18 | commands {
19 | +TestCommand.Two
20 | +TestCommand.One
21 | }
22 | is TestEvent.Four ->
23 | if (event.flag) {
24 | state { copy(one = 1) }
25 | commands { +TestCommand.One }
26 | effects { +TestEffect.One }
27 | } else {
28 | state { copy(one = state.two, two = state.one) }
29 | effects { +TestEffect.One }
30 | }
31 | is TestEvent.Five -> applyDiff()
32 | is TestEvent.Six -> {
33 | commands { +TestCommand.One.takeIf { event.flag } }
34 | }
35 | }
36 |
37 | // Result editing can be done in a separate function
38 | private fun Result.applyDiff() {
39 | state { copy(one = 0) }
40 | state { copy(one = initialState.one + 3) }
41 | }
42 | }
43 |
44 | internal class DslReducerTest {
45 |
46 | private val reducer = BasicDslReducer
47 |
48 | @Test
49 | fun `Multiple state updates are executed`() {
50 | val initialState = TestState(one = 0, two = 0)
51 | val (state, effects, commands) = reducer.reduce(TestEvent.One, initialState)
52 | assertEquals(state, TestState(one = 1, two = 2))
53 | assertTrue(effects.isEmpty())
54 | assertTrue(commands.isEmpty())
55 | }
56 |
57 | @Test
58 | fun `Effect is added`() {
59 | val initialState = TestState(one = 0, two = 0)
60 | val (state, effects, commands) = reducer.reduce(TestEvent.Two, initialState)
61 | assertEquals(state, initialState)
62 | assertEquals(effects, listOf(TestEffect.One))
63 | assertTrue(commands.isEmpty())
64 | }
65 |
66 | @Test
67 | fun `Multiple commands are added`() {
68 | val initialState = TestState(one = 0, two = 0)
69 | val (state, effects, commands) = reducer.reduce(TestEvent.Three, initialState)
70 | assertEquals(state, initialState)
71 | assertTrue(effects.isEmpty())
72 | assertEquals(commands, listOf(TestCommand.Two, TestCommand.One))
73 | }
74 |
75 | @Test
76 | fun `Complex operation`() {
77 | val initialState = TestState(one = 0, two = 0)
78 | val (state, effects, commands) = reducer.reduce(TestEvent.Four(true), initialState)
79 | assertEquals(state, TestState(one = 1, two = 0))
80 | assertEquals(effects, listOf(TestEffect.One))
81 | assertEquals(commands, listOf(TestCommand.One))
82 | }
83 |
84 | @Test
85 | fun `Condition switches state values`() {
86 | val initialState = TestState(one = 1, two = 2)
87 | val (state, effects, commands) = reducer.reduce(TestEvent.Four(false), initialState)
88 | assertEquals(state, TestState(one = 2, two = 1))
89 | assertEquals(effects, listOf(TestEffect.One))
90 | assertTrue(commands.isEmpty())
91 | }
92 |
93 | @Test
94 | fun `Can access initial state`() {
95 | val initialState = TestState(one = 1, two = 0)
96 | val (state, effects, commands) = reducer.reduce(TestEvent.Five, initialState)
97 | assertEquals(state, TestState(one = 4, two = 0))
98 | assertTrue(effects.isEmpty())
99 | assertTrue(commands.isEmpty())
100 | }
101 |
102 | @Test
103 | fun `Add command conditionally`() {
104 | val initialState = TestState(one = 0, two = 0)
105 | val (state, effects, commands) = reducer.reduce(TestEvent.Six(true), initialState)
106 | assertEquals(state, initialState)
107 | assertTrue(effects.isEmpty())
108 | assertEquals(commands, listOf(TestCommand.One))
109 | }
110 |
111 | @Test
112 | fun `Skip command conditionally`() {
113 | val initialState = TestState(one = 0, two = 0)
114 | val (state, effects, commands) = reducer.reduce(TestEvent.Six(false), initialState)
115 | assertEquals(state, initialState)
116 | assertTrue(effects.isEmpty())
117 | assertTrue(commands.isEmpty())
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/store/ElmStore.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.store
2 |
3 | import kotlinx.coroutines.CancellationException
4 | import kotlinx.coroutines.CoroutineScope
5 | import kotlinx.coroutines.ExperimentalCoroutinesApi
6 | import kotlinx.coroutines.cancel
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.MutableSharedFlow
9 | import kotlinx.coroutines.flow.MutableStateFlow
10 | import kotlinx.coroutines.flow.StateFlow
11 | import kotlinx.coroutines.flow.asSharedFlow
12 | import kotlinx.coroutines.flow.asStateFlow
13 | import kotlinx.coroutines.flow.cancellable
14 | import kotlinx.coroutines.flow.catch
15 | import kotlinx.coroutines.flow.onEach
16 | import kotlinx.coroutines.isActive
17 | import kotlinx.coroutines.launch
18 | import money.vivid.elmslie.core.ElmScope
19 | import money.vivid.elmslie.core.config.ElmslieConfig
20 | import money.vivid.elmslie.core.utils.resolveStoreKey
21 |
22 | @Suppress("TooGenericExceptionCaught")
23 | @OptIn(ExperimentalCoroutinesApi::class)
24 | class ElmStore(
25 | initialState: State,
26 | private val reducer: StateReducer,
27 | private val actor: Actor,
28 | storeListeners: Set>? = null,
29 | override val startEvent: Event? = null,
30 | private val key: String = resolveStoreKey(reducer),
31 | ) : Store {
32 |
33 | private val logger = ElmslieConfig.logger
34 | private val eventDispatcher = ElmslieConfig.elmDispatcher.limitedParallelism(parallelism = 1)
35 |
36 | private val effectsFlow = MutableSharedFlow()
37 |
38 | private val statesFlow: MutableStateFlow = MutableStateFlow(initialState)
39 |
40 | private val storeListeners: MutableSet> =
41 | mutableSetOf>().apply {
42 | ElmslieConfig.globalStoreListeners.forEach(::add)
43 | storeListeners?.forEach(::add)
44 | }
45 |
46 | override val scope = ElmScope("${key}Scope")
47 |
48 | override val states: StateFlow = statesFlow.asStateFlow()
49 |
50 | override val effects: Flow = effectsFlow.asSharedFlow()
51 |
52 | override fun accept(event: Event) {
53 | scope.handleEvent(event)
54 | }
55 |
56 | override fun start(): Store {
57 | startEvent?.let(::accept)
58 | return this
59 | }
60 |
61 | override fun stop() {
62 | scope.cancel()
63 | }
64 |
65 | private fun CoroutineScope.handleEvent(event: Event) =
66 | launch(eventDispatcher) {
67 | try {
68 | storeListeners.forEach { it.onBeforeEvent(key, event, statesFlow.value) }
69 | logger.debug(message = "New event: $event", tag = key)
70 | val oldState = statesFlow.value
71 | val (state, effects, commands) = reducer.reduce(event, statesFlow.value)
72 | statesFlow.value = state
73 | storeListeners.forEach { it.onAfterEvent(key, state, oldState, event) }
74 | effects.forEach { effect -> if (isActive) dispatchEffect(effect) }
75 | commands.forEach { if (isActive) executeCommand(it) }
76 | } catch (error: CancellationException) {
77 | throw error
78 | } catch (t: Throwable) {
79 | storeListeners.forEach { it.onReducerError(key, t, event) }
80 | logger.fatal(message = "You must handle all errors inside reducer", tag = key, error = t)
81 | }
82 | }
83 |
84 | private suspend fun dispatchEffect(effect: Effect) {
85 | storeListeners.forEach { it.onEffect(key, effect, statesFlow.value) }
86 | logger.debug(message = "New effect: $effect", tag = key)
87 | effectsFlow.emit(effect)
88 | }
89 |
90 | private fun executeCommand(command: Command) {
91 | scope.launch {
92 | storeListeners.forEach { it.onCommand(key, command, statesFlow.value) }
93 | logger.debug(message = "Executing command: $command", tag = key)
94 | actor
95 | .execute(command)
96 | .onEach { logger.debug(message = "Command $command produces event $it", tag = key) }
97 | .cancellable()
98 | .catch { throwable ->
99 | storeListeners.forEach { it.onActorError(key, throwable, command) }
100 | logger.nonfatal(
101 | message = "Unhandled exception inside the command $command",
102 | tag = key,
103 | error = throwable,
104 | )
105 | }
106 | .collect { accept(it) }
107 | }
108 | }
109 | }
110 |
111 | fun Store.toCachedStore() =
112 | EffectCachingElmStore(this)
113 |
--------------------------------------------------------------------------------
/elmslie-android/src/main/kotlin/money/vivid/elmslie/android/ElmStoreLazy.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.android
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.annotation.MainThread
6 | import androidx.core.os.bundleOf
7 | import androidx.fragment.app.Fragment
8 | import androidx.lifecycle.AbstractSavedStateViewModelFactory
9 | import androidx.lifecycle.SavedStateHandle
10 | import androidx.lifecycle.ViewModel
11 | import androidx.lifecycle.ViewModelProvider
12 | import androidx.lifecycle.ViewModelStoreOwner
13 | import androidx.savedstate.SavedStateRegistryOwner
14 | import money.vivid.elmslie.core.store.Store
15 | import money.vivid.elmslie.core.store.toCachedStore
16 |
17 | /**
18 | * In order to access previously saved state (via [saveState]) in [storeFactory] one must use
19 | * SavedStateHandle.get(StateBundleKey)
20 | */
21 | @MainThread
22 | fun Fragment.elmStore(
23 | key: String = this::class.java.canonicalName ?: this::class.java.simpleName,
24 | viewModelStoreOwner: () -> ViewModelStoreOwner = { this },
25 | savedStateRegistryOwner: () -> SavedStateRegistryOwner = { this },
26 | defaultArgs: () -> Bundle = { arguments ?: bundleOf() },
27 | saveState: Bundle.(State) -> Unit = {},
28 | storeFactory: SavedStateHandle.() -> Store,
29 | ): Lazy> =
30 | money.vivid.elmslie.android.elmStore(
31 | storeFactory = storeFactory,
32 | key = key,
33 | viewModelStoreOwner = viewModelStoreOwner,
34 | savedStateRegistryOwner = savedStateRegistryOwner,
35 | saveState = saveState,
36 | defaultArgs = defaultArgs,
37 | )
38 |
39 | /**
40 | * In order to access previously saved state (via [saveState]) in [storeFactory] one must use
41 | * SavedStateHandle.get(StateBundleKey)
42 | */
43 | @MainThread
44 | fun ComponentActivity.elmStore(
45 | key: String = this::class.java.canonicalName ?: this::class.java.simpleName,
46 | viewModelStoreOwner: () -> ViewModelStoreOwner = { this },
47 | savedStateRegistryOwner: () -> SavedStateRegistryOwner = { this },
48 | defaultArgs: () -> Bundle = { this.intent?.extras ?: bundleOf() },
49 | saveState: Bundle.(State) -> Unit = {},
50 | storeFactory: SavedStateHandle.() -> Store,
51 | ): Lazy> =
52 | money.vivid.elmslie.android.elmStore(
53 | storeFactory = storeFactory,
54 | key = key,
55 | viewModelStoreOwner = viewModelStoreOwner,
56 | savedStateRegistryOwner = savedStateRegistryOwner,
57 | defaultArgs = defaultArgs,
58 | saveState = saveState,
59 | )
60 |
61 | @MainThread
62 | internal fun elmStore(
63 | key: String,
64 | viewModelStoreOwner: () -> ViewModelStoreOwner,
65 | savedStateRegistryOwner: () -> SavedStateRegistryOwner,
66 | defaultArgs: () -> Bundle,
67 | saveState: Bundle.(State) -> Unit,
68 | storeFactory: SavedStateHandle.() -> Store,
69 | ): Lazy> =
70 | lazy(LazyThreadSafetyMode.NONE) {
71 | val factory =
72 | RetainedElmStoreFactory(
73 | stateRegistryOwner = savedStateRegistryOwner.invoke(),
74 | defaultArgs = defaultArgs.invoke(),
75 | storeFactory = storeFactory,
76 | saveState = saveState,
77 | )
78 | val provider = ViewModelProvider(viewModelStoreOwner.invoke(), factory)
79 |
80 | @Suppress("UNCHECKED_CAST")
81 | provider[key, RetainedElmStore::class.java].store as Store
82 | }
83 |
84 | class RetainedElmStore(
85 | savedStateHandle: SavedStateHandle,
86 | storeFactory: SavedStateHandle.() -> Store,
87 | saveState: Bundle.(State) -> Unit,
88 | ) : ViewModel() {
89 |
90 | val store = storeFactory.invoke(savedStateHandle).toCachedStore().also { it.start() }
91 |
92 | init {
93 | savedStateHandle.setSavedStateProvider(StateBundleKey) {
94 | bundleOf().apply { saveState(store.states.value) }
95 | }
96 | }
97 |
98 | override fun onCleared() {
99 | store.stop()
100 | }
101 |
102 | companion object {
103 |
104 | const val StateBundleKey = "elm_store_state_bundle"
105 | }
106 | }
107 |
108 | class RetainedElmStoreFactory(
109 | stateRegistryOwner: SavedStateRegistryOwner,
110 | defaultArgs: Bundle,
111 | private val storeFactory: SavedStateHandle.() -> Store,
112 | private val saveState: Bundle.(State) -> Unit,
113 | ) : AbstractSavedStateViewModelFactory(stateRegistryOwner, defaultArgs) {
114 |
115 | override fun create(
116 | key: String,
117 | modelClass: Class,
118 | handle: SavedStateHandle,
119 | ): T {
120 | @Suppress("UNCHECKED_CAST")
121 | return RetainedElmStore(
122 | savedStateHandle = handle,
123 | storeFactory = storeFactory,
124 | saveState = saveState,
125 | )
126 | as T
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/samples/kotlin-calculator/src/test/kotlin/money/vivid/elmslie/samples/calculator/StoreTest.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.samples.calculator
2 |
3 | import kotlin.test.AfterTest
4 | import kotlin.test.BeforeTest
5 | import kotlin.test.Test
6 | import kotlin.test.assertEquals
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.ExperimentalCoroutinesApi
9 | import kotlinx.coroutines.flow.toList
10 | import kotlinx.coroutines.launch
11 | import kotlinx.coroutines.test.StandardTestDispatcher
12 | import kotlinx.coroutines.test.advanceUntilIdle
13 | import kotlinx.coroutines.test.resetMain
14 | import kotlinx.coroutines.test.runTest
15 | import kotlinx.coroutines.test.setMain
16 | import money.vivid.elmslie.core.config.ElmslieConfig
17 |
18 | @OptIn(ExperimentalCoroutinesApi::class)
19 | internal class StoreTest {
20 |
21 | @BeforeTest
22 | fun beforeEach() {
23 | val testDispatcher = StandardTestDispatcher()
24 | ElmslieConfig.elmDispatcher { testDispatcher }
25 | Dispatchers.setMain(testDispatcher)
26 | }
27 |
28 | @AfterTest
29 | fun afterEach() {
30 | Dispatchers.resetMain()
31 | }
32 |
33 | @Test
34 | fun `1 + 1 = 2`() = runTest {
35 | val calculator = Calculator()
36 | val errors = mutableListOf()
37 | val results = mutableListOf()
38 |
39 | val errorsJob = launch { calculator.errors().toList(errors) }
40 | val resultJob = launch { calculator.results().toList(results) }
41 |
42 | calculator.digit('1')
43 | calculator.plus()
44 | calculator.digit('1')
45 | calculator.evaluate()
46 |
47 | advanceUntilIdle()
48 |
49 | assertEquals(listOf(Effect.NotifyNewResult(1), Effect.NotifyNewResult(2)), results)
50 | assertEquals(emptyList(), errors)
51 |
52 | errorsJob.cancel()
53 | resultJob.cancel()
54 | }
55 |
56 | @Test
57 | fun `1 + 1 + 1 = 3`() = runTest {
58 | val calculator = Calculator()
59 | val errors = mutableListOf()
60 | val results = mutableListOf()
61 |
62 | val errorsJob = launch { calculator.errors().toList(errors) }
63 | val resultJob = launch { calculator.results().toList(results) }
64 |
65 | calculator.digit('1')
66 | calculator.plus()
67 | calculator.digit('1')
68 | calculator.plus()
69 | calculator.digit('1')
70 | calculator.evaluate()
71 |
72 | advanceUntilIdle()
73 |
74 | assertEquals(
75 | listOf(
76 | Effect.NotifyNewResult(1),
77 | Effect.NotifyNewResult(2),
78 | Effect.NotifyNewResult(3),
79 | ),
80 | results,
81 | )
82 | assertEquals(emptyList(), errors)
83 |
84 | errorsJob.cancel()
85 | resultJob.cancel()
86 | }
87 |
88 | @Test
89 | fun `1 + 2 times 3 minus 4 div 5 = 1`() = runTest {
90 | val calculator = Calculator()
91 | val errors = mutableListOf()
92 | val results = mutableListOf()
93 |
94 | val errorsJob = launch { calculator.errors().toList(errors) }
95 | val resultJob = launch { calculator.results().toList(results) }
96 |
97 | calculator.digit('1')
98 | calculator.plus()
99 | calculator.digit('2')
100 | calculator.times()
101 | calculator.digit('3')
102 | calculator.minus()
103 | calculator.digit('4')
104 | calculator.divide()
105 | calculator.digit('5')
106 | calculator.evaluate()
107 |
108 | advanceUntilIdle()
109 |
110 | assertEquals(
111 | listOf(
112 | Effect.NotifyNewResult(1),
113 | Effect.NotifyNewResult(3),
114 | Effect.NotifyNewResult(9),
115 | Effect.NotifyNewResult(5),
116 | Effect.NotifyNewResult(1),
117 | ),
118 | results,
119 | )
120 | assertEquals(emptyList(), errors)
121 |
122 | errorsJob.cancel()
123 | resultJob.cancel()
124 | }
125 |
126 | @Test
127 | fun `not a digit produces error`() = runTest {
128 | val calculator = Calculator()
129 | val errors = mutableListOf()
130 | val results = mutableListOf()
131 |
132 | val errorsJob = launch { calculator.errors().toList(errors) }
133 | val resultJob = launch { calculator.results().toList(results) }
134 |
135 | calculator.digit('x')
136 |
137 | advanceUntilIdle()
138 |
139 | assertEquals(listOf(Effect.NotifyError("x is not a digit")), errors)
140 | assertEquals(emptyList(), results)
141 |
142 | errorsJob.cancel()
143 | resultJob.cancel()
144 | }
145 |
146 | @Test
147 | fun `10 digits produces error`() = runTest {
148 | val calculator = Calculator()
149 | val errors = mutableListOf()
150 | val results = mutableListOf()
151 |
152 | val errorsJob = launch { calculator.errors().toList(errors) }
153 | val resultJob = launch { calculator.results().toList(results) }
154 |
155 | calculator.digit('1')
156 | calculator.digit('1')
157 | calculator.digit('1')
158 | calculator.digit('1')
159 | calculator.digit('1')
160 | calculator.digit('1')
161 | calculator.digit('1')
162 | calculator.digit('1')
163 | calculator.digit('1')
164 | calculator.digit('1')
165 |
166 | advanceUntilIdle()
167 |
168 | assertEquals(listOf(Effect.NotifyError("Reached max input length")), errors)
169 | assertEquals(emptyList(), results)
170 |
171 | errorsJob.cancel()
172 | resultJob.cancel()
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonTest/kotlin/money/vivid/elmslie/core/store/EffectCachingElmStoreTest.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.store
2 |
3 | import kotlin.test.AfterTest
4 | import kotlin.test.BeforeTest
5 | import kotlin.test.Test
6 | import kotlin.test.assertEquals
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.ExperimentalCoroutinesApi
9 | import kotlinx.coroutines.flow.toList
10 | import kotlinx.coroutines.launch
11 | import kotlinx.coroutines.test.StandardTestDispatcher
12 | import kotlinx.coroutines.test.advanceUntilIdle
13 | import kotlinx.coroutines.test.resetMain
14 | import kotlinx.coroutines.test.runCurrent
15 | import kotlinx.coroutines.test.runTest
16 | import kotlinx.coroutines.test.setMain
17 | import money.vivid.elmslie.core.config.ElmslieConfig
18 | import money.vivid.elmslie.core.testutil.model.Command
19 | import money.vivid.elmslie.core.testutil.model.Effect
20 | import money.vivid.elmslie.core.testutil.model.Event
21 | import money.vivid.elmslie.core.testutil.model.State
22 |
23 | @OptIn(ExperimentalCoroutinesApi::class)
24 | class EffectCachingElmStoreTest {
25 |
26 | @BeforeTest
27 | fun beforeEach() {
28 | val testDispatcher = StandardTestDispatcher()
29 | ElmslieConfig.elmDispatcher { testDispatcher }
30 | Dispatchers.setMain(testDispatcher)
31 | }
32 |
33 | @AfterTest
34 | fun afterEach() {
35 | Dispatchers.resetMain()
36 | }
37 |
38 | @Test
39 | fun `Should collect effects which are emitted before collecting flow`() = runTest {
40 | val store =
41 | store(
42 | state = State(),
43 | reducer =
44 | object : StateReducer() {
45 | override fun Result.reduce(event: Event) {
46 | effects { +Effect(value = event.value) }
47 | }
48 | },
49 | )
50 | .toCachedStore()
51 |
52 | store.start()
53 | store.accept(Event(value = 1))
54 | store.accept(Event(value = 2))
55 | store.accept(Event(value = 2))
56 | advanceUntilIdle()
57 |
58 | val effects = mutableListOf()
59 | val job = launch { store.effects.toList(effects) }
60 | advanceUntilIdle()
61 |
62 | assertEquals(listOf(Effect(value = 1), Effect(value = 2), Effect(value = 2)), effects)
63 |
64 | job.cancel()
65 | }
66 |
67 | @Test
68 | fun `Should collect effects which are emitted before collecting flow and after`() = runTest {
69 | val store =
70 | store(
71 | state = State(),
72 | reducer =
73 | object : StateReducer() {
74 | override fun Result.reduce(event: Event) {
75 | effects { +Effect(value = event.value) }
76 | }
77 | },
78 | )
79 | .toCachedStore()
80 |
81 | store.start()
82 | store.accept(Event(value = 1))
83 | store.accept(Event(value = 2))
84 | store.accept(Event(value = 2))
85 | advanceUntilIdle()
86 |
87 | val effects = mutableListOf()
88 | val job = launch { store.effects.toList(effects) }
89 | store.accept(Event(value = 3))
90 | advanceUntilIdle()
91 |
92 | assertEquals(
93 | listOf(Effect(value = 1), Effect(value = 2), Effect(value = 2), Effect(value = 3)),
94 | effects,
95 | )
96 |
97 | job.cancel()
98 | }
99 |
100 | @Test
101 | fun `Should emit effects from cache only for the first subscriber`() = runTest {
102 | val store =
103 | store(
104 | state = State(),
105 | reducer =
106 | object : StateReducer() {
107 | override fun Result.reduce(event: Event) {
108 | effects { +Effect(value = event.value) }
109 | }
110 | },
111 | )
112 | .toCachedStore()
113 |
114 | store.start()
115 | store.accept(Event(value = 1))
116 | advanceUntilIdle()
117 |
118 | val effects1 = mutableListOf()
119 | val effects2 = mutableListOf()
120 | val job1 = launch { store.effects.toList(effects1) }
121 | runCurrent()
122 | val job2 = launch { store.effects.toList(effects2) }
123 | runCurrent()
124 |
125 | assertEquals(listOf(Effect(value = 1)), effects1)
126 |
127 | assertEquals(emptyList(), effects2)
128 |
129 | job1.cancel()
130 | job2.cancel()
131 | }
132 |
133 | @Test
134 | fun `Should cache effects if there is no left collectors`() = runTest {
135 | val store =
136 | store(
137 | state = State(),
138 | reducer =
139 | object : StateReducer() {
140 | override fun Result.reduce(event: Event) {
141 | effects { +Effect(value = event.value) }
142 | }
143 | },
144 | )
145 | .toCachedStore()
146 |
147 | store.start()
148 | val effects = mutableListOf()
149 | var job1 = launch { store.effects.toList(effects) }
150 | runCurrent()
151 | job1.cancel()
152 | store.accept(Event(value = 2))
153 | runCurrent()
154 | job1 = launch { store.effects.toList(effects) }
155 | runCurrent()
156 |
157 | assertEquals(listOf(Effect(value = 2)), effects)
158 |
159 | job1.cancel()
160 | }
161 |
162 | private fun store(
163 | state: State,
164 | reducer: StateReducer = NoOpReducer(),
165 | actor: Actor = NoOpActor(),
166 | ) = ElmStore(state, reducer, actor)
167 | }
168 |
--------------------------------------------------------------------------------
/elmslie-android/api/elmslie-android.api:
--------------------------------------------------------------------------------
1 | public final class money/vivid/elmslie/android/ElmStoreLazyKt {
2 | public static final fun elmStore (Landroidx/activity/ComponentActivity;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lkotlin/Lazy;
3 | public static final fun elmStore (Landroidx/fragment/app/Fragment;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lkotlin/Lazy;
4 | public static synthetic fun elmStore$default (Landroidx/activity/ComponentActivity;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlin/Lazy;
5 | public static synthetic fun elmStore$default (Landroidx/fragment/app/Fragment;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlin/Lazy;
6 | }
7 |
8 | public final class money/vivid/elmslie/android/RetainedElmStore : androidx/lifecycle/ViewModel {
9 | public static final field Companion Lmoney/vivid/elmslie/android/RetainedElmStore$Companion;
10 | public static final field StateBundleKey Ljava/lang/String;
11 | public fun (Landroidx/lifecycle/SavedStateHandle;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V
12 | public final fun getStore ()Lmoney/vivid/elmslie/core/store/EffectCachingElmStore;
13 | }
14 |
15 | public final class money/vivid/elmslie/android/RetainedElmStore$Companion {
16 | }
17 |
18 | public final class money/vivid/elmslie/android/RetainedElmStoreFactory : androidx/lifecycle/AbstractSavedStateViewModelFactory {
19 | public fun (Landroidx/savedstate/SavedStateRegistryOwner;Landroid/os/Bundle;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V
20 | }
21 |
22 | public final class money/vivid/elmslie/android/logger/DefaultLoggerConfigurationsKt {
23 | public static final fun defaultDebugLogger (Lmoney/vivid/elmslie/core/config/ElmslieConfig;)V
24 | public static final fun defaultReleaseLogger (Lmoney/vivid/elmslie/core/config/ElmslieConfig;)V
25 | }
26 |
27 | public final class money/vivid/elmslie/android/logger/DefaultLoggerInitializer : androidx/startup/Initializer {
28 | public fun ()V
29 | public synthetic fun create (Landroid/content/Context;)Ljava/lang/Object;
30 | public fun create (Landroid/content/Context;)V
31 | public fun dependencies ()Ljava/util/List;
32 | }
33 |
34 | public final class money/vivid/elmslie/android/logger/strategy/AndroidLog {
35 | public static final field INSTANCE Lmoney/vivid/elmslie/android/logger/strategy/AndroidLog;
36 | public final fun getD ()Lmoney/vivid/elmslie/core/logger/strategy/LogStrategy;
37 | public final fun getE ()Lmoney/vivid/elmslie/core/logger/strategy/LogStrategy;
38 | public final fun getI ()Lmoney/vivid/elmslie/core/logger/strategy/LogStrategy;
39 | public final fun getV ()Lmoney/vivid/elmslie/core/logger/strategy/LogStrategy;
40 | public final fun getW ()Lmoney/vivid/elmslie/core/logger/strategy/LogStrategy;
41 | }
42 |
43 | public final class money/vivid/elmslie/android/logger/strategy/Crash : money/vivid/elmslie/core/logger/strategy/LogStrategy {
44 | public static final field INSTANCE Lmoney/vivid/elmslie/android/logger/strategy/Crash;
45 | public fun log (Lmoney/vivid/elmslie/core/logger/LogSeverity;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)V
46 | }
47 |
48 | public final class money/vivid/elmslie/android/processdeath/ProcessDeathDetector {
49 | public static final field INSTANCE Lmoney/vivid/elmslie/android/processdeath/ProcessDeathDetector;
50 | public final fun isRestoringAfterProcessDeath ()Z
51 | }
52 |
53 | public final class money/vivid/elmslie/android/processdeath/ProcessDeathDetectorInitializer : androidx/startup/Initializer {
54 | public fun ()V
55 | public synthetic fun create (Landroid/content/Context;)Ljava/lang/Object;
56 | public fun create (Landroid/content/Context;)V
57 | public fun dependencies ()Ljava/util/List;
58 | }
59 |
60 | public abstract interface class money/vivid/elmslie/android/renderer/ElmRendererDelegate {
61 | public abstract fun handleEffect (Ljava/lang/Object;)Lkotlin/Unit;
62 | public abstract fun mapList (Ljava/lang/Object;)Ljava/util/List;
63 | public abstract fun render (Ljava/lang/Object;)V
64 | public abstract fun renderList (Ljava/lang/Object;Ljava/util/List;)V
65 | }
66 |
67 | public final class money/vivid/elmslie/android/renderer/ElmRendererDelegate$DefaultImpls {
68 | public static fun handleEffect (Lmoney/vivid/elmslie/android/renderer/ElmRendererDelegate;Ljava/lang/Object;)Lkotlin/Unit;
69 | public static fun mapList (Lmoney/vivid/elmslie/android/renderer/ElmRendererDelegate;Ljava/lang/Object;)Ljava/util/List;
70 | public static fun renderList (Lmoney/vivid/elmslie/android/renderer/ElmRendererDelegate;Ljava/lang/Object;Ljava/util/List;)V
71 | }
72 |
73 | public final class money/vivid/elmslie/android/renderer/ElmRendererDelegateKt {
74 | public static final fun androidElmStore (Lmoney/vivid/elmslie/android/renderer/ElmRendererDelegate;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lkotlin/Lazy;
75 | public static final fun androidElmStore (Lmoney/vivid/elmslie/android/renderer/ElmRendererDelegate;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lkotlin/Lazy;
76 | public static synthetic fun androidElmStore$default (Lmoney/vivid/elmslie/android/renderer/ElmRendererDelegate;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlin/Lazy;
77 | public static synthetic fun androidElmStore$default (Lmoney/vivid/elmslie/android/renderer/ElmRendererDelegate;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlin/Lazy;
78 | }
79 |
80 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
87 |
88 | # Use the maximum available, or set MAX_FD != -1 to use that value.
89 | MAX_FD=maximum
90 |
91 | warn () {
92 | echo "$*"
93 | } >&2
94 |
95 | die () {
96 | echo
97 | echo "$*"
98 | echo
99 | exit 1
100 | } >&2
101 |
102 | # OS specific support (must be 'true' or 'false').
103 | cygwin=false
104 | msys=false
105 | darwin=false
106 | nonstop=false
107 | case "$( uname )" in #(
108 | CYGWIN* ) cygwin=true ;; #(
109 | Darwin* ) darwin=true ;; #(
110 | MSYS* | MINGW* ) msys=true ;; #(
111 | NONSTOP* ) nonstop=true ;;
112 | esac
113 |
114 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
115 |
116 |
117 | # Determine the Java command to use to start the JVM.
118 | if [ -n "$JAVA_HOME" ] ; then
119 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
120 | # IBM's JDK on AIX uses strange locations for the executables
121 | JAVACMD=$JAVA_HOME/jre/sh/java
122 | else
123 | JAVACMD=$JAVA_HOME/bin/java
124 | fi
125 | if [ ! -x "$JAVACMD" ] ; then
126 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
127 |
128 | Please set the JAVA_HOME variable in your environment to match the
129 | location of your Java installation."
130 | fi
131 | else
132 | JAVACMD=java
133 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
134 |
135 | Please set the JAVA_HOME variable in your environment to match the
136 | location of your Java installation."
137 | fi
138 |
139 | # Increase the maximum file descriptors if we can.
140 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
141 | case $MAX_FD in #(
142 | max*)
143 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
144 | # shellcheck disable=SC3045
145 | MAX_FD=$( ulimit -H -n ) ||
146 | warn "Could not query maximum file descriptor limit"
147 | esac
148 | case $MAX_FD in #(
149 | '' | soft) :;; #(
150 | *)
151 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
152 | # shellcheck disable=SC3045
153 | ulimit -n "$MAX_FD" ||
154 | warn "Could not set maximum file descriptor limit to $MAX_FD"
155 | esac
156 | fi
157 |
158 | # Collect all arguments for the java command, stacking in reverse order:
159 | # * args from the command line
160 | # * the main class name
161 | # * -classpath
162 | # * -D...appname settings
163 | # * --module-path (only if needed)
164 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
165 |
166 | # For Cygwin or MSYS, switch paths to Windows format before running java
167 | if "$cygwin" || "$msys" ; then
168 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
169 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
170 |
171 | JAVACMD=$( cygpath --unix "$JAVACMD" )
172 |
173 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
174 | for arg do
175 | if
176 | case $arg in #(
177 | -*) false ;; # don't mess with options #(
178 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
179 | [ -e "$t" ] ;; #(
180 | *) false ;;
181 | esac
182 | then
183 | arg=$( cygpath --path --ignore --mixed "$arg" )
184 | fi
185 | # Roll the args list around exactly as many times as the number of
186 | # args, so each arg winds up back in the position where it started, but
187 | # possibly modified.
188 | #
189 | # NB: a `for` loop captures its iteration list before it begins, so
190 | # changing the positional parameters here affects neither the number of
191 | # iterations, nor the values presented in `arg`.
192 | shift # remove old arg
193 | set -- "$@" "$arg" # push replacement arg
194 | done
195 | fi
196 |
197 |
198 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
199 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
200 |
201 | # Collect all arguments for the java command;
202 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
203 | # shell script including quotes and variable substitutions, so put them in
204 | # double quotes to make sure that they get re-expanded; and
205 | # * put everything else in single quotes, so that it's not re-expanded.
206 |
207 | set -- \
208 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
209 | -classpath "$CLASSPATH" \
210 | org.gradle.wrapper.GradleWrapperMain \
211 | "$@"
212 |
213 | # Stop when "xargs" is not available.
214 | if ! command -v xargs >/dev/null 2>&1
215 | then
216 | die "xargs is not available"
217 | fi
218 |
219 | # Use "xargs" to parse quoted args.
220 | #
221 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
222 | #
223 | # In Bash we could simply go:
224 | #
225 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
226 | # set -- "${ARGS[@]}" "$@"
227 | #
228 | # but POSIX shell has neither arrays nor command substitution, so instead we
229 | # post-process each arg (as a line of input to sed) to backslash-escape any
230 | # character that might be a shell metacharacter, then use eval to reverse
231 | # that process (while maintaining the separation between arguments), and wrap
232 | # the whole thing up as a single "set" statement.
233 | #
234 | # This will of course break if any of these variables contains a newline or
235 | # an unmatched quote.
236 | #
237 |
238 | eval "set -- $(
239 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
240 | xargs -n1 |
241 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
242 | tr '\n' ' '
243 | )" '"$@"'
244 |
245 | exec "$JAVACMD" "$@"
246 |
--------------------------------------------------------------------------------
/elmslie-core/src/commonTest/kotlin/money/vivid/elmslie/core/store/ElmStoreTest.kt:
--------------------------------------------------------------------------------
1 | package money.vivid.elmslie.core.store
2 |
3 | import kotlin.test.AfterTest
4 | import kotlin.test.BeforeTest
5 | import kotlin.test.Test
6 | import kotlin.test.assertEquals
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.ExperimentalCoroutinesApi
9 | import kotlinx.coroutines.delay
10 | import kotlinx.coroutines.flow.Flow
11 | import kotlinx.coroutines.flow.flow
12 | import kotlinx.coroutines.flow.flowOf
13 | import kotlinx.coroutines.flow.onEach
14 | import kotlinx.coroutines.flow.toList
15 | import kotlinx.coroutines.launch
16 | import kotlinx.coroutines.test.StandardTestDispatcher
17 | import kotlinx.coroutines.test.advanceTimeBy
18 | import kotlinx.coroutines.test.advanceUntilIdle
19 | import kotlinx.coroutines.test.resetMain
20 | import kotlinx.coroutines.test.runCurrent
21 | import kotlinx.coroutines.test.runTest
22 | import kotlinx.coroutines.test.setMain
23 | import money.vivid.elmslie.core.config.ElmslieConfig
24 | import money.vivid.elmslie.core.testutil.model.Command
25 | import money.vivid.elmslie.core.testutil.model.Effect
26 | import money.vivid.elmslie.core.testutil.model.Event
27 | import money.vivid.elmslie.core.testutil.model.State
28 |
29 | @OptIn(ExperimentalCoroutinesApi::class)
30 | class ElmStoreTest {
31 |
32 | @BeforeTest
33 | fun beforeEach() {
34 | val testDispatcher = StandardTestDispatcher()
35 | ElmslieConfig.elmDispatcher { testDispatcher }
36 | Dispatchers.setMain(testDispatcher)
37 | }
38 |
39 | @AfterTest
40 | fun afterEach() {
41 | Dispatchers.resetMain()
42 | }
43 |
44 | @Test
45 | fun `Should stop the store properly`() = runTest {
46 | val store = store(State())
47 |
48 | store.start()
49 | store.accept(Event())
50 | store.stop()
51 | advanceUntilIdle()
52 | }
53 |
54 | @Test
55 | fun `Should stop getting state updates when the store is stopped`() = runTest {
56 | val actor =
57 | object : Actor() {
58 | override fun execute(command: Command): Flow =
59 | flow { emit(Event()) }.onEach { delay(1000) }
60 | }
61 |
62 | val store =
63 | store(
64 | state = State(),
65 | reducer =
66 | object : StateReducer() {
67 | override fun Result.reduce(event: Event) {
68 | state { copy(value = state.value + 1) }
69 | commands { +Command() }
70 | }
71 | },
72 | actor = actor,
73 | )
74 | .start()
75 |
76 | val emittedStates = mutableListOf()
77 | val collectJob = launch { store.states.toList(emittedStates) }
78 | store.accept(Event())
79 | advanceTimeBy(3500)
80 | store.stop()
81 |
82 | assertEquals(
83 | mutableListOf(
84 | State(0), // Initial state
85 | State(1), // State after receiving trigger Event
86 | State(2), // State after executing the first command
87 | State(3), // State after executing the second command
88 | State(4), // State after executing the third command
89 | ),
90 | emittedStates,
91 | )
92 | collectJob.cancel()
93 | }
94 |
95 | @Test
96 | fun `Should update state when event is received`() = runTest {
97 | val store =
98 | store(
99 | state = State(),
100 | reducer =
101 | object : StateReducer() {
102 | override fun Result.reduce(event: Event) {
103 | state { copy(value = event.value) }
104 | }
105 | },
106 | )
107 | .start()
108 |
109 | assertEquals(State(0), store.states.value)
110 | store.accept(Event(value = 10))
111 | advanceUntilIdle()
112 |
113 | assertEquals(State(10), store.states.value)
114 | }
115 |
116 | @Test
117 | fun `Should not update state when it's equal to previous one`() = runTest {
118 | val store =
119 | store(
120 | state = State(),
121 | reducer =
122 | object : StateReducer() {
123 | override fun Result.reduce(event: Event) {
124 | state { copy(value = event.value) }
125 | }
126 | },
127 | )
128 | .start()
129 |
130 | val emittedStates = mutableListOf()
131 | val collectJob = launch { store.states.toList(emittedStates) }
132 |
133 | store.accept(Event(value = 0))
134 | advanceUntilIdle()
135 |
136 | assertEquals(
137 | mutableListOf(
138 | State(0) // Initial state
139 | ),
140 | emittedStates,
141 | )
142 | collectJob.cancel()
143 | }
144 |
145 | @Test
146 | fun `Should collect all emitted effects`() = runTest {
147 | val store =
148 | store(
149 | state = State(),
150 | reducer =
151 | object : StateReducer() {
152 | override fun Result.reduce(event: Event) {
153 | effects { +Effect(value = event.value) }
154 | }
155 | },
156 | )
157 | .start()
158 |
159 | val effects = mutableListOf()
160 | val collectJob = launch { store.effects.toList(effects) }
161 | store.accept(Event(value = 1))
162 | store.accept(Event(value = -1))
163 | advanceUntilIdle()
164 |
165 | assertEquals(
166 | mutableListOf(
167 | Effect(value = 1), // The first effect
168 | Effect(value = -1), // The second effect
169 | ),
170 | effects,
171 | )
172 | collectJob.cancel()
173 | }
174 |
175 | @Test
176 | fun `Should skip the effect which is emitted before subscribing to effects`() = runTest {
177 | val store =
178 | store(
179 | state = State(),
180 | reducer =
181 | object : StateReducer() {
182 | override fun Result.reduce(event: Event) {
183 | effects { +Effect(value = event.value) }
184 | }
185 | },
186 | )
187 | .start()
188 |
189 | val effects = mutableListOf()
190 | store.accept(Event(value = 1))
191 | runCurrent()
192 | val collectJob = launch { store.effects.toList(effects) }
193 | store.accept(Event(value = -1))
194 | runCurrent()
195 |
196 | assertEquals(mutableListOf(Effect(value = -1)), effects)
197 | collectJob.cancel()
198 | }
199 |
200 | @Test
201 | fun `Should collect all effects emitted once per time`() = runTest {
202 | val store =
203 | store(
204 | state = State(),
205 | reducer =
206 | object : StateReducer() {
207 | override fun Result.reduce(event: Event) {
208 | effects {
209 | +Effect(value = event.value)
210 | +Effect(value = event.value)
211 | }
212 | }
213 | },
214 | )
215 | .start()
216 |
217 | val effects = mutableListOf()
218 | val collectJob = launch { store.effects.toList(effects) }
219 | store.accept(Event(value = 1))
220 | advanceUntilIdle()
221 |
222 | assertEquals(
223 | mutableListOf(
224 | Effect(value = 1), // The first effect
225 | Effect(value = 1), // The second effect
226 | ),
227 | effects,
228 | )
229 | collectJob.cancel()
230 | }
231 |
232 | @Test
233 | fun `Should collect all emitted effects by all collectors`() = runTest {
234 | val store =
235 | store(
236 | state = State(),
237 | reducer =
238 | object : StateReducer() {
239 | override fun Result.reduce(event: Event) {
240 | effects { +Effect(value = event.value) }
241 | }
242 | },
243 | )
244 | .start()
245 |
246 | val effects1 = mutableListOf()
247 | val effects2 = mutableListOf()
248 | val collectJob1 = launch { store.effects.toList(effects1) }
249 | val collectJob2 = launch { store.effects.toList(effects2) }
250 | store.accept(Event(value = 1))
251 | store.accept(Event(value = -1))
252 | advanceUntilIdle()
253 |
254 | assertEquals(
255 | mutableListOf(
256 | Effect(value = 1), // The first effect
257 | Effect(value = -1), // The second effect
258 | ),
259 | effects1,
260 | )
261 | assertEquals(
262 | mutableListOf(
263 | Effect(value = 1), // The first effect
264 | Effect(value = -1), // The second effect
265 | ),
266 | effects2,
267 | )
268 | collectJob1.cancel()
269 | collectJob2.cancel()
270 | }
271 |
272 | @Test
273 | fun `Should collect duplicated effects`() = runTest {
274 | val store =
275 | store(
276 | state = State(),
277 | reducer =
278 | object : StateReducer() {
279 | override fun Result.reduce(event: Event) {
280 | effects { +Effect(value = event.value) }
281 | }
282 | },
283 | )
284 | .start()
285 |
286 | val effects = mutableListOf()
287 | val collectJob = launch { store.effects.toList(effects) }
288 | store.accept(Event(value = 1))
289 | store.accept(Event(value = 1))
290 | advanceUntilIdle()
291 |
292 | assertEquals(mutableListOf(Effect(value = 1), Effect(value = 1)), effects)
293 | collectJob.cancel()
294 | }
295 |
296 | @Test
297 | fun `Should collect event caused by actor`() = runTest {
298 | val actor =
299 | object : Actor() {
300 | override fun execute(command: Command): Flow = flowOf(Event(command.value))
301 | }
302 | val store =
303 | store(
304 | state = State(),
305 | reducer =
306 | object : StateReducer() {
307 | override fun Result.reduce(event: Event) {
308 | state { copy(value = event.value) }
309 | commands { +Command(event.value - 1).takeIf { event.value > 0 } }
310 | }
311 | },
312 | actor = actor,
313 | )
314 | .start()
315 |
316 | val states = mutableListOf()
317 | val collectJob = launch { store.states.toList(states) }
318 |
319 | store.accept(Event(3))
320 | advanceUntilIdle()
321 |
322 | assertEquals(
323 | mutableListOf(
324 | State(0), // Initial state
325 | State(3), // State after receiving Event with command number
326 | State(2), // State after executing the first command
327 | State(1), // State after executing the second command
328 | State(0), // State after executing the third command
329 | ),
330 | states,
331 | )
332 |
333 | collectJob.cancel()
334 | }
335 |
336 | private fun store(
337 | state: State,
338 | reducer: StateReducer = NoOpReducer(),
339 | actor: Actor = NoOpActor(),
340 | ) = ElmStore(state, reducer, actor)
341 | }
342 |
--------------------------------------------------------------------------------
/elmslie-core/api/elmslie-core.api:
--------------------------------------------------------------------------------
1 | public final class money/vivid/elmslie/core/ElmScopeKt {
2 | public static final fun ElmScope (Ljava/lang/String;)Lkotlinx/coroutines/CoroutineScope;
3 | }
4 |
5 | public final class money/vivid/elmslie/core/config/ElmslieConfig {
6 | public static final field INSTANCE Lmoney/vivid/elmslie/core/config/ElmslieConfig;
7 | public final fun elmDispatcher (Lkotlin/jvm/functions/Function0;)V
8 | public final fun getElmDispatcher ()Lkotlinx/coroutines/CoroutineDispatcher;
9 | public final fun getGlobalStoreListeners ()Ljava/util/Set;
10 | public final fun getLogger ()Lmoney/vivid/elmslie/core/logger/ElmslieLogger;
11 | public final fun getShouldStopOnProcessDeath ()Z
12 | public final fun globalStoreListeners (Lkotlin/jvm/functions/Function0;)V
13 | public final fun logger (Lkotlin/jvm/functions/Function1;)V
14 | public final fun shouldStopOnProcessDeath (Lkotlin/jvm/functions/Function0;)V
15 | }
16 |
17 | public final class money/vivid/elmslie/core/logger/ElmslieLogConfiguration {
18 | public fun ()V
19 | public final fun always (Lmoney/vivid/elmslie/core/logger/strategy/LogStrategy;)Lmoney/vivid/elmslie/core/logger/ElmslieLogConfiguration;
20 | public final fun debug (Lmoney/vivid/elmslie/core/logger/strategy/LogStrategy;)Lmoney/vivid/elmslie/core/logger/ElmslieLogConfiguration;
21 | public final fun fatal (Lmoney/vivid/elmslie/core/logger/strategy/LogStrategy;)Lmoney/vivid/elmslie/core/logger/ElmslieLogConfiguration;
22 | public final fun nonfatal (Lmoney/vivid/elmslie/core/logger/strategy/LogStrategy;)Lmoney/vivid/elmslie/core/logger/ElmslieLogConfiguration;
23 | }
24 |
25 | public final class money/vivid/elmslie/core/logger/ElmslieLogger {
26 | public fun (Ljava/util/Map;)V
27 | public final fun debug (Ljava/lang/String;Ljava/lang/String;)V
28 | public static synthetic fun debug$default (Lmoney/vivid/elmslie/core/logger/ElmslieLogger;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V
29 | public final fun fatal (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)V
30 | public static synthetic fun fatal$default (Lmoney/vivid/elmslie/core/logger/ElmslieLogger;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;ILjava/lang/Object;)V
31 | public final fun nonfatal (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)V
32 | public static synthetic fun nonfatal$default (Lmoney/vivid/elmslie/core/logger/ElmslieLogger;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;ILjava/lang/Object;)V
33 | }
34 |
35 | public final class money/vivid/elmslie/core/logger/LogSeverity : java/lang/Enum {
36 | public static final field Debug Lmoney/vivid/elmslie/core/logger/LogSeverity;
37 | public static final field Fatal Lmoney/vivid/elmslie/core/logger/LogSeverity;
38 | public static final field NonFatal Lmoney/vivid/elmslie/core/logger/LogSeverity;
39 | public static fun getEntries ()Lkotlin/enums/EnumEntries;
40 | public static fun valueOf (Ljava/lang/String;)Lmoney/vivid/elmslie/core/logger/LogSeverity;
41 | public static fun values ()[Lmoney/vivid/elmslie/core/logger/LogSeverity;
42 | }
43 |
44 | public final class money/vivid/elmslie/core/logger/strategy/IgnoreLog : money/vivid/elmslie/core/logger/strategy/LogStrategy {
45 | public static final field INSTANCE Lmoney/vivid/elmslie/core/logger/strategy/IgnoreLog;
46 | public fun log (Lmoney/vivid/elmslie/core/logger/LogSeverity;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)V
47 | }
48 |
49 | public abstract interface class money/vivid/elmslie/core/logger/strategy/LogStrategy {
50 | public abstract fun log (Lmoney/vivid/elmslie/core/logger/LogSeverity;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)V
51 | }
52 |
53 | public abstract class money/vivid/elmslie/core/store/Actor {
54 | public fun ()V
55 | protected final fun cancelSwitchFlows ([Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
56 | public abstract fun execute (Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
57 | protected final fun mapEvents (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow;
58 | public static synthetic fun mapEvents$default (Lmoney/vivid/elmslie/core/store/Actor;Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
59 | protected final fun switch-SxA4cEA (Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;J)Lkotlinx/coroutines/flow/Flow;
60 | public static synthetic fun switch-SxA4cEA$default (Lmoney/vivid/elmslie/core/store/Actor;Lkotlinx/coroutines/flow/Flow;Ljava/lang/Object;JILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
61 | }
62 |
63 | public final class money/vivid/elmslie/core/store/EffectCachingElmStore : money/vivid/elmslie/core/store/Store {
64 | public fun (Lmoney/vivid/elmslie/core/store/Store;)V
65 | public fun accept (Ljava/lang/Object;)V
66 | public fun getEffects ()Lkotlinx/coroutines/flow/Flow;
67 | public fun getScope ()Lkotlinx/coroutines/CoroutineScope;
68 | public fun getStartEvent ()Ljava/lang/Object;
69 | public fun getStates ()Lkotlinx/coroutines/flow/StateFlow;
70 | public fun start ()Lmoney/vivid/elmslie/core/store/Store;
71 | public fun stop ()V
72 | }
73 |
74 | public final class money/vivid/elmslie/core/store/ElmStore : money/vivid/elmslie/core/store/Store {
75 | public fun (Ljava/lang/Object;Lmoney/vivid/elmslie/core/store/StateReducer;Lmoney/vivid/elmslie/core/store/Actor;Ljava/util/Set;Ljava/lang/Object;Ljava/lang/String;)V
76 | public synthetic fun (Ljava/lang/Object;Lmoney/vivid/elmslie/core/store/StateReducer;Lmoney/vivid/elmslie/core/store/Actor;Ljava/util/Set;Ljava/lang/Object;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
77 | public fun accept (Ljava/lang/Object;)V
78 | public fun getEffects ()Lkotlinx/coroutines/flow/Flow;
79 | public fun getScope ()Lkotlinx/coroutines/CoroutineScope;
80 | public fun getStartEvent ()Ljava/lang/Object;
81 | public fun getStates ()Lkotlinx/coroutines/flow/StateFlow;
82 | public fun start ()Lmoney/vivid/elmslie/core/store/Store;
83 | public fun stop ()V
84 | }
85 |
86 | public final class money/vivid/elmslie/core/store/ElmStoreKt {
87 | public static final fun toCachedStore (Lmoney/vivid/elmslie/core/store/Store;)Lmoney/vivid/elmslie/core/store/EffectCachingElmStore;
88 | }
89 |
90 | public final class money/vivid/elmslie/core/store/NoOpActor : money/vivid/elmslie/core/store/Actor {
91 | public fun ()V
92 | public fun execute (Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
93 | }
94 |
95 | public final class money/vivid/elmslie/core/store/NoOpReducer : money/vivid/elmslie/core/store/StateReducer {
96 | public fun ()V
97 | }
98 |
99 | public final class money/vivid/elmslie/core/store/Result {
100 | public fun (Ljava/lang/Object;)V
101 | public fun (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)V
102 | public synthetic fun (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
103 | public fun (Ljava/lang/Object;Ljava/util/List;)V
104 | public fun (Ljava/lang/Object;Ljava/util/List;Ljava/util/List;)V
105 | public final fun component1 ()Ljava/lang/Object;
106 | public final fun component2 ()Ljava/util/List;
107 | public final fun component3 ()Ljava/util/List;
108 | public final fun copy (Ljava/lang/Object;Ljava/util/List;Ljava/util/List;)Lmoney/vivid/elmslie/core/store/Result;
109 | public static synthetic fun copy$default (Lmoney/vivid/elmslie/core/store/Result;Ljava/lang/Object;Ljava/util/List;Ljava/util/List;ILjava/lang/Object;)Lmoney/vivid/elmslie/core/store/Result;
110 | public fun equals (Ljava/lang/Object;)Z
111 | public final fun getCommands ()Ljava/util/List;
112 | public final fun getEffects ()Ljava/util/List;
113 | public final fun getState ()Ljava/lang/Object;
114 | public fun hashCode ()I
115 | public fun toString ()Ljava/lang/String;
116 | }
117 |
118 | public abstract class money/vivid/elmslie/core/store/ScreenReducer : money/vivid/elmslie/core/store/StateReducer {
119 | public fun (Lkotlin/reflect/KClass;Lkotlin/reflect/KClass;)V
120 | protected abstract fun internal (Lmoney/vivid/elmslie/core/store/StateReducer$Result;Ljava/lang/Object;)V
121 | protected fun reduce (Lmoney/vivid/elmslie/core/store/StateReducer$Result;Ljava/lang/Object;)V
122 | protected abstract fun ui (Lmoney/vivid/elmslie/core/store/StateReducer$Result;Ljava/lang/Object;)V
123 | }
124 |
125 | public abstract class money/vivid/elmslie/core/store/StateReducer {
126 | public fun ()V
127 | public final fun reduce (Ljava/lang/Object;Ljava/lang/Object;)Lmoney/vivid/elmslie/core/store/Result;
128 | protected abstract fun reduce (Lmoney/vivid/elmslie/core/store/StateReducer$Result;Ljava/lang/Object;)V
129 | }
130 |
131 | protected final class money/vivid/elmslie/core/store/StateReducer$Result : money/vivid/elmslie/core/store/dsl/ResultBuilder {
132 | public fun (Lmoney/vivid/elmslie/core/store/StateReducer;Ljava/lang/Object;)V
133 | }
134 |
135 | public abstract interface class money/vivid/elmslie/core/store/Store {
136 | public abstract fun accept (Ljava/lang/Object;)V
137 | public abstract fun getEffects ()Lkotlinx/coroutines/flow/Flow;
138 | public abstract fun getScope ()Lkotlinx/coroutines/CoroutineScope;
139 | public abstract fun getStartEvent ()Ljava/lang/Object;
140 | public abstract fun getStates ()Lkotlinx/coroutines/flow/StateFlow;
141 | public abstract fun start ()Lmoney/vivid/elmslie/core/store/Store;
142 | public abstract fun stop ()V
143 | }
144 |
145 | public abstract interface class money/vivid/elmslie/core/store/StoreListener {
146 | public abstract fun onActorError (Ljava/lang/String;Ljava/lang/Throwable;Ljava/lang/Object;)V
147 | public abstract fun onAfterEvent (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)V
148 | public abstract fun onBeforeEvent (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V
149 | public abstract fun onCommand (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V
150 | public abstract fun onEffect (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V
151 | public abstract fun onReducerError (Ljava/lang/String;Ljava/lang/Throwable;Ljava/lang/Object;)V
152 | }
153 |
154 | public final class money/vivid/elmslie/core/store/StoreListener$DefaultImpls {
155 | public static fun onActorError (Lmoney/vivid/elmslie/core/store/StoreListener;Ljava/lang/String;Ljava/lang/Throwable;Ljava/lang/Object;)V
156 | public static fun onAfterEvent (Lmoney/vivid/elmslie/core/store/StoreListener;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)V
157 | public static fun onBeforeEvent (Lmoney/vivid/elmslie/core/store/StoreListener;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V
158 | public static fun onCommand (Lmoney/vivid/elmslie/core/store/StoreListener;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V
159 | public static fun onEffect (Lmoney/vivid/elmslie/core/store/StoreListener;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V
160 | public static fun onReducerError (Lmoney/vivid/elmslie/core/store/StoreListener;Ljava/lang/String;Ljava/lang/Throwable;Ljava/lang/Object;)V
161 | }
162 |
163 | public final class money/vivid/elmslie/core/store/dsl/OperationsBuilder {
164 | public fun ()V
165 | public final fun unaryPlus (Ljava/lang/Object;)V
166 | }
167 |
168 | public class money/vivid/elmslie/core/store/dsl/ResultBuilder {
169 | public fun (Ljava/lang/Object;)V
170 | public final fun commands (Lkotlin/jvm/functions/Function1;)V
171 | public final fun effects (Lkotlin/jvm/functions/Function1;)V
172 | public final fun getInitialState ()Ljava/lang/Object;
173 | public final fun getState ()Ljava/lang/Object;
174 | public final fun state (Lkotlin/jvm/functions/Function1;)V
175 | }
176 |
177 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/detekt/detekt.yml:
--------------------------------------------------------------------------------
1 | build:
2 | maxIssues: 0
3 | excludeCorrectable: false
4 | weights:
5 | # complexity: 2
6 | # LongParameterList: 1
7 | # style: 1
8 | # comments: 1
9 |
10 | config:
11 | validation: true
12 | warningsAsErrors: false
13 | checkExhaustiveness: false
14 | # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
15 | excludes: ''
16 |
17 | processors:
18 | active: true
19 | exclude:
20 | - 'DetektProgressListener'
21 | # - 'KtFileCountProcessor'
22 | # - 'PackageCountProcessor'
23 | # - 'ClassCountProcessor'
24 | # - 'FunctionCountProcessor'
25 | # - 'PropertyCountProcessor'
26 | # - 'ProjectComplexityProcessor'
27 | # - 'ProjectCognitiveComplexityProcessor'
28 | # - 'ProjectLLOCProcessor'
29 | # - 'ProjectCLOCProcessor'
30 | # - 'ProjectLOCProcessor'
31 | # - 'ProjectSLOCProcessor'
32 | # - 'LicenseHeaderLoaderExtension'
33 |
34 | console-reports:
35 | active: true
36 | exclude:
37 | - 'ProjectStatisticsReport'
38 | - 'ComplexityReport'
39 | - 'NotificationReport'
40 | - 'FindingsReport'
41 | - 'FileBasedFindingsReport'
42 | # - 'LiteFindingsReport'
43 |
44 | output-reports:
45 | active: true
46 | exclude:
47 | # - 'TxtOutputReport'
48 | # - 'XmlOutputReport'
49 | # - 'HtmlOutputReport'
50 | # - 'MdOutputReport'
51 | # - 'SarifOutputReport'
52 |
53 | comments:
54 | active: true
55 | AbsentOrWrongFileLicense:
56 | active: false
57 | licenseTemplateFile: 'license.template'
58 | licenseTemplateIsRegex: false
59 | CommentOverPrivateFunction:
60 | active: false
61 | CommentOverPrivateProperty:
62 | active: false
63 | DeprecatedBlockTag:
64 | active: false
65 | EndOfSentenceFormat:
66 | active: false
67 | endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
68 | KDocReferencesNonPublicProperty:
69 | active: false
70 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
71 | OutdatedDocumentation:
72 | active: false
73 | matchTypeParameters: true
74 | matchDeclarationsOrder: true
75 | allowParamOnConstructorProperties: false
76 | UndocumentedPublicClass:
77 | active: false
78 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
79 | searchInNestedClass: true
80 | searchInInnerClass: true
81 | searchInInnerObject: true
82 | searchInInnerInterface: true
83 | searchInProtectedClass: false
84 | UndocumentedPublicFunction:
85 | active: false
86 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
87 | searchProtectedFunction: false
88 | UndocumentedPublicProperty:
89 | active: false
90 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
91 | searchProtectedProperty: false
92 |
93 | complexity:
94 | active: true
95 | CognitiveComplexMethod:
96 | active: false
97 | threshold: 15
98 | ComplexCondition:
99 | active: true
100 | threshold: 4
101 | ComplexInterface:
102 | active: false
103 | threshold: 10
104 | includeStaticDeclarations: false
105 | includePrivateDeclarations: false
106 | ignoreOverloaded: false
107 | CyclomaticComplexMethod:
108 | active: true
109 | threshold: 15
110 | ignoreSingleWhenExpression: false
111 | ignoreSimpleWhenEntries: false
112 | ignoreNestingFunctions: false
113 | nestingFunctions:
114 | - 'also'
115 | - 'apply'
116 | - 'forEach'
117 | - 'isNotNull'
118 | - 'ifNull'
119 | - 'let'
120 | - 'run'
121 | - 'use'
122 | - 'with'
123 | LabeledExpression:
124 | active: false
125 | ignoredLabels: []
126 | LargeClass:
127 | active: true
128 | threshold: 600
129 | LongMethod:
130 | active: true
131 | threshold: 60
132 | LongParameterList:
133 | active: true
134 | functionThreshold: 6
135 | constructorThreshold: 7
136 | ignoreDefaultParameters: false
137 | ignoreDataClasses: true
138 | ignoreAnnotatedParameter: []
139 | MethodOverloading:
140 | active: false
141 | threshold: 6
142 | NamedArguments:
143 | active: false
144 | threshold: 3
145 | ignoreArgumentsMatchingNames: false
146 | NestedBlockDepth:
147 | active: true
148 | threshold: 4
149 | NestedScopeFunctions:
150 | active: false
151 | threshold: 1
152 | functions:
153 | - 'kotlin.apply'
154 | - 'kotlin.run'
155 | - 'kotlin.with'
156 | - 'kotlin.let'
157 | - 'kotlin.also'
158 | ReplaceSafeCallChainWithRun:
159 | active: false
160 | StringLiteralDuplication:
161 | active: false
162 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
163 | threshold: 3
164 | ignoreAnnotation: true
165 | excludeStringsWithLessThan5Characters: true
166 | ignoreStringsRegex: '$^'
167 | TooManyFunctions:
168 | active: true
169 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
170 | thresholdInFiles: 11
171 | thresholdInClasses: 11
172 | thresholdInInterfaces: 11
173 | thresholdInObjects: 11
174 | thresholdInEnums: 11
175 | ignoreDeprecated: false
176 | ignorePrivate: false
177 | ignoreOverridden: false
178 |
179 | coroutines:
180 | active: true
181 | GlobalCoroutineUsage:
182 | active: false
183 | InjectDispatcher:
184 | active: true
185 | dispatcherNames:
186 | - 'IO'
187 | - 'Default'
188 | - 'Unconfined'
189 | RedundantSuspendModifier:
190 | active: true
191 | SleepInsteadOfDelay:
192 | active: true
193 | SuspendFunSwallowedCancellation:
194 | active: false
195 | SuspendFunWithCoroutineScopeReceiver:
196 | active: false
197 | SuspendFunWithFlowReturnType:
198 | active: true
199 |
200 | empty-blocks:
201 | active: true
202 | EmptyCatchBlock:
203 | active: true
204 | allowedExceptionNameRegex: '_|(ignore|expected).*'
205 | EmptyClassBlock:
206 | active: true
207 | EmptyDefaultConstructor:
208 | active: true
209 | EmptyDoWhileBlock:
210 | active: true
211 | EmptyElseBlock:
212 | active: true
213 | EmptyFinallyBlock:
214 | active: true
215 | EmptyForBlock:
216 | active: true
217 | EmptyFunctionBlock:
218 | active: true
219 | ignoreOverridden: false
220 | EmptyIfBlock:
221 | active: true
222 | EmptyInitBlock:
223 | active: true
224 | EmptyKtFile:
225 | active: true
226 | EmptySecondaryConstructor:
227 | active: true
228 | EmptyTryBlock:
229 | active: true
230 | EmptyWhenBlock:
231 | active: true
232 | EmptyWhileBlock:
233 | active: true
234 |
235 | exceptions:
236 | active: true
237 | ExceptionRaisedInUnexpectedLocation:
238 | active: true
239 | methodNames:
240 | - 'equals'
241 | - 'finalize'
242 | - 'hashCode'
243 | - 'toString'
244 | InstanceOfCheckForException:
245 | active: true
246 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
247 | NotImplementedDeclaration:
248 | active: false
249 | ObjectExtendsThrowable:
250 | active: false
251 | PrintStackTrace:
252 | active: true
253 | RethrowCaughtException:
254 | active: true
255 | ReturnFromFinally:
256 | active: true
257 | ignoreLabeled: false
258 | SwallowedException:
259 | active: true
260 | ignoredExceptionTypes:
261 | - 'InterruptedException'
262 | - 'MalformedURLException'
263 | - 'NumberFormatException'
264 | - 'ParseException'
265 | allowedExceptionNameRegex: '_|(ignore|expected).*'
266 | ThrowingExceptionFromFinally:
267 | active: true
268 | ThrowingExceptionInMain:
269 | active: false
270 | ThrowingExceptionsWithoutMessageOrCause:
271 | active: true
272 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
273 | exceptions:
274 | - 'ArrayIndexOutOfBoundsException'
275 | - 'Exception'
276 | - 'IllegalArgumentException'
277 | - 'IllegalMonitorStateException'
278 | - 'IllegalStateException'
279 | - 'IndexOutOfBoundsException'
280 | - 'NullPointerException'
281 | - 'RuntimeException'
282 | - 'Throwable'
283 | ThrowingNewInstanceOfSameException:
284 | active: true
285 | TooGenericExceptionCaught:
286 | active: true
287 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
288 | exceptionNames:
289 | - 'ArrayIndexOutOfBoundsException'
290 | - 'Error'
291 | - 'Exception'
292 | - 'IllegalMonitorStateException'
293 | - 'IndexOutOfBoundsException'
294 | - 'NullPointerException'
295 | - 'RuntimeException'
296 | - 'Throwable'
297 | allowedExceptionNameRegex: '_|(ignore|expected).*'
298 | TooGenericExceptionThrown:
299 | active: true
300 | exceptionNames:
301 | - 'Error'
302 | - 'Exception'
303 | - 'RuntimeException'
304 | - 'Throwable'
305 |
306 | naming:
307 | active: true
308 | BooleanPropertyNaming:
309 | active: false
310 | allowedPattern: '^(is|has|are)'
311 | ClassNaming:
312 | active: true
313 | classPattern: '[A-Z][a-zA-Z0-9]*'
314 | ConstructorParameterNaming:
315 | active: true
316 | parameterPattern: '[a-z][A-Za-z0-9]*'
317 | privateParameterPattern: '[a-z][A-Za-z0-9]*'
318 | excludeClassPattern: '$^'
319 | EnumNaming:
320 | active: true
321 | enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
322 | ForbiddenClassName:
323 | active: false
324 | forbiddenName: []
325 | FunctionMaxLength:
326 | active: false
327 | maximumFunctionNameLength: 30
328 | FunctionMinLength:
329 | active: false
330 | minimumFunctionNameLength: 3
331 | FunctionNaming:
332 | active: true
333 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
334 | functionPattern: '[a-z][a-zA-Z0-9]*'
335 | excludeClassPattern: '$^'
336 | FunctionParameterNaming:
337 | active: true
338 | parameterPattern: '[a-z][A-Za-z0-9]*'
339 | excludeClassPattern: '$^'
340 | InvalidPackageDeclaration:
341 | active: true
342 | rootPackage: ''
343 | requireRootInDeclaration: false
344 | LambdaParameterNaming:
345 | active: false
346 | parameterPattern: '[a-z][A-Za-z0-9]*|_'
347 | MatchingDeclarationName:
348 | active: true
349 | mustBeFirst: true
350 | MemberNameEqualsClassName:
351 | active: true
352 | ignoreOverridden: true
353 | NoNameShadowing:
354 | active: true
355 | NonBooleanPropertyPrefixedWithIs:
356 | active: false
357 | ObjectPropertyNaming:
358 | active: true
359 | constantPattern: '[A-Za-z][_A-Za-z0-9]*'
360 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
361 | privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
362 | PackageNaming:
363 | active: true
364 | packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
365 | TopLevelPropertyNaming:
366 | active: true
367 | constantPattern: '[A-Z][_A-Z0-9]*'
368 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
369 | privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
370 | VariableMaxLength:
371 | active: false
372 | maximumVariableNameLength: 64
373 | VariableMinLength:
374 | active: false
375 | minimumVariableNameLength: 1
376 | VariableNaming:
377 | active: true
378 | variablePattern: '[a-z][A-Za-z0-9]*'
379 | privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
380 | excludeClassPattern: '$^'
381 |
382 | performance:
383 | active: true
384 | ArrayPrimitive:
385 | active: true
386 | CouldBeSequence:
387 | active: false
388 | threshold: 3
389 | ForEachOnRange:
390 | active: true
391 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
392 | SpreadOperator:
393 | active: true
394 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
395 | UnnecessaryPartOfBinaryExpression:
396 | active: false
397 | UnnecessaryTemporaryInstantiation:
398 | active: true
399 |
400 | potential-bugs:
401 | active: true
402 | AvoidReferentialEquality:
403 | active: true
404 | forbiddenTypePatterns:
405 | - 'kotlin.String'
406 | CastNullableToNonNullableType:
407 | active: false
408 | CastToNullableType:
409 | active: false
410 | Deprecation:
411 | active: false
412 | DontDowncastCollectionTypes:
413 | active: false
414 | DoubleMutabilityForCollection:
415 | active: true
416 | mutableTypes:
417 | - 'kotlin.collections.MutableList'
418 | - 'kotlin.collections.MutableMap'
419 | - 'kotlin.collections.MutableSet'
420 | - 'java.util.ArrayList'
421 | - 'java.util.LinkedHashSet'
422 | - 'java.util.HashSet'
423 | - 'java.util.LinkedHashMap'
424 | - 'java.util.HashMap'
425 | ElseCaseInsteadOfExhaustiveWhen:
426 | active: false
427 | ignoredSubjectTypes: []
428 | EqualsAlwaysReturnsTrueOrFalse:
429 | active: true
430 | EqualsWithHashCodeExist:
431 | active: true
432 | ExitOutsideMain:
433 | active: false
434 | ExplicitGarbageCollectionCall:
435 | active: true
436 | HasPlatformType:
437 | active: true
438 | IgnoredReturnValue:
439 | active: true
440 | restrictToConfig: true
441 | returnValueAnnotations:
442 | - 'CheckResult'
443 | - '*.CheckResult'
444 | - 'CheckReturnValue'
445 | - '*.CheckReturnValue'
446 | ignoreReturnValueAnnotations:
447 | - 'CanIgnoreReturnValue'
448 | - '*.CanIgnoreReturnValue'
449 | returnValueTypes:
450 | - 'kotlin.sequences.Sequence'
451 | - 'kotlinx.coroutines.flow.*Flow'
452 | - 'java.util.stream.*Stream'
453 | ignoreFunctionCall: []
454 | ImplicitDefaultLocale:
455 | active: true
456 | ImplicitUnitReturnType:
457 | active: false
458 | allowExplicitReturnType: true
459 | InvalidRange:
460 | active: true
461 | IteratorHasNextCallsNextMethod:
462 | active: true
463 | IteratorNotThrowingNoSuchElementException:
464 | active: true
465 | LateinitUsage:
466 | active: false
467 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
468 | ignoreOnClassesPattern: ''
469 | MapGetWithNotNullAssertionOperator:
470 | active: true
471 | MissingPackageDeclaration:
472 | active: false
473 | excludes: ['**/*.kts']
474 | NullCheckOnMutableProperty:
475 | active: false
476 | NullableToStringCall:
477 | active: false
478 | PropertyUsedBeforeDeclaration:
479 | active: false
480 | UnconditionalJumpStatementInLoop:
481 | active: false
482 | UnnecessaryNotNullCheck:
483 | active: false
484 | UnnecessaryNotNullOperator:
485 | active: true
486 | UnnecessarySafeCall:
487 | active: true
488 | UnreachableCatchBlock:
489 | active: true
490 | UnreachableCode:
491 | active: true
492 | UnsafeCallOnNullableType:
493 | active: true
494 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
495 | UnsafeCast:
496 | active: true
497 | UnusedUnaryOperator:
498 | active: true
499 | UselessPostfixExpression:
500 | active: true
501 | WrongEqualsTypeParameter:
502 | active: true
503 |
504 | style:
505 | active: true
506 | AlsoCouldBeApply:
507 | active: false
508 | BracesOnIfStatements:
509 | active: false
510 | singleLine: 'never'
511 | multiLine: 'always'
512 | BracesOnWhenStatements:
513 | active: false
514 | singleLine: 'necessary'
515 | multiLine: 'consistent'
516 | CanBeNonNullable:
517 | active: false
518 | CascadingCallWrapping:
519 | active: false
520 | includeElvis: true
521 | ClassOrdering:
522 | active: false
523 | CollapsibleIfStatements:
524 | active: false
525 | DataClassContainsFunctions:
526 | active: false
527 | conversionFunctionPrefix:
528 | - 'to'
529 | allowOperators: false
530 | DataClassShouldBeImmutable:
531 | active: false
532 | DestructuringDeclarationWithTooManyEntries:
533 | active: true
534 | maxDestructuringEntries: 3
535 | DoubleNegativeLambda:
536 | active: false
537 | negativeFunctions:
538 | - reason: 'Use `takeIf` instead.'
539 | value: 'takeUnless'
540 | - reason: 'Use `all` instead.'
541 | value: 'none'
542 | negativeFunctionNameParts:
543 | - 'not'
544 | - 'non'
545 | EqualsNullCall:
546 | active: true
547 | EqualsOnSignatureLine:
548 | active: false
549 | ExplicitCollectionElementAccessMethod:
550 | active: false
551 | ExplicitItLambdaParameter:
552 | active: true
553 | ExpressionBodySyntax:
554 | active: false
555 | includeLineWrapping: false
556 | ForbiddenAnnotation:
557 | active: false
558 | annotations:
559 | - reason: 'it is a java annotation. Use `Suppress` instead.'
560 | value: 'java.lang.SuppressWarnings'
561 | - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.'
562 | value: 'java.lang.Deprecated'
563 | - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.'
564 | value: 'java.lang.annotation.Documented'
565 | - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.'
566 | value: 'java.lang.annotation.Target'
567 | - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.'
568 | value: 'java.lang.annotation.Retention'
569 | - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.'
570 | value: 'java.lang.annotation.Repeatable'
571 | - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265'
572 | value: 'java.lang.annotation.Inherited'
573 | ForbiddenComment:
574 | active: true
575 | comments:
576 | - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.'
577 | value: 'FIXME:'
578 | - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.'
579 | value: 'STOPSHIP:'
580 | - reason: 'Forbidden TODO todo marker in comment, please do the changes.'
581 | value: 'TODO:'
582 | allowedPatterns: ''
583 | ForbiddenImport:
584 | active: false
585 | imports: []
586 | forbiddenPatterns: ''
587 | ForbiddenMethodCall:
588 | active: false
589 | methods:
590 | - reason: 'print does not allow you to configure the output stream. Use a logger instead.'
591 | value: 'kotlin.io.print'
592 | - reason: 'println does not allow you to configure the output stream. Use a logger instead.'
593 | value: 'kotlin.io.println'
594 | ForbiddenSuppress:
595 | active: false
596 | rules: []
597 | ForbiddenVoid:
598 | active: true
599 | ignoreOverridden: false
600 | ignoreUsageInGenerics: false
601 | FunctionOnlyReturningConstant:
602 | active: true
603 | ignoreOverridableFunction: true
604 | ignoreActualFunction: true
605 | excludedFunctions: []
606 | LoopWithTooManyJumpStatements:
607 | active: true
608 | maxJumpCount: 1
609 | MagicNumber:
610 | active: true
611 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts']
612 | ignoreNumbers:
613 | - '-1'
614 | - '0'
615 | - '1'
616 | - '2'
617 | ignoreHashCodeFunction: true
618 | ignorePropertyDeclaration: false
619 | ignoreLocalVariableDeclaration: false
620 | ignoreConstantDeclaration: true
621 | ignoreCompanionObjectPropertyDeclaration: true
622 | ignoreAnnotation: false
623 | ignoreNamedArgument: true
624 | ignoreEnums: false
625 | ignoreRanges: false
626 | ignoreExtensionFunctions: true
627 | MandatoryBracesLoops:
628 | active: false
629 | MaxChainedCallsOnSameLine:
630 | active: false
631 | maxChainedCalls: 5
632 | MaxLineLength:
633 | active: true
634 | maxLineLength: 120
635 | excludePackageStatements: true
636 | excludeImportStatements: true
637 | excludeCommentStatements: false
638 | excludeRawStrings: true
639 | MayBeConst:
640 | active: true
641 | ModifierOrder:
642 | active: true
643 | MultilineLambdaItParameter:
644 | active: false
645 | MultilineRawStringIndentation:
646 | active: false
647 | indentSize: 4
648 | trimmingMethods:
649 | - 'trimIndent'
650 | - 'trimMargin'
651 | NestedClassesVisibility:
652 | active: true
653 | NewLineAtEndOfFile:
654 | active: true
655 | NoTabs:
656 | active: false
657 | NullableBooleanCheck:
658 | active: false
659 | ObjectLiteralToLambda:
660 | active: true
661 | OptionalAbstractKeyword:
662 | active: true
663 | OptionalUnit:
664 | active: false
665 | OptionalWhenBraces:
666 | active: false
667 | PreferToOverPairSyntax:
668 | active: false
669 | ProtectedMemberInFinalClass:
670 | active: true
671 | RedundantExplicitType:
672 | active: false
673 | RedundantHigherOrderMapUsage:
674 | active: true
675 | RedundantVisibilityModifierRule:
676 | active: false
677 | ReturnCount:
678 | active: true
679 | max: 2
680 | excludedFunctions:
681 | - 'equals'
682 | excludeLabeled: false
683 | excludeReturnFromLambda: true
684 | excludeGuardClauses: false
685 | SafeCast:
686 | active: true
687 | SerialVersionUIDInSerializableClass:
688 | active: true
689 | SpacingBetweenPackageAndImports:
690 | active: false
691 | StringShouldBeRawString:
692 | active: false
693 | maxEscapedCharacterCount: 2
694 | ignoredCharacters: []
695 | ThrowsCount:
696 | active: true
697 | max: 2
698 | excludeGuardClauses: false
699 | TrailingWhitespace:
700 | active: false
701 | TrimMultilineRawString:
702 | active: false
703 | trimmingMethods:
704 | - 'trimIndent'
705 | - 'trimMargin'
706 | UnderscoresInNumericLiterals:
707 | active: false
708 | acceptableLength: 4
709 | allowNonStandardGrouping: false
710 | UnnecessaryAbstractClass:
711 | active: true
712 | UnnecessaryAnnotationUseSiteTarget:
713 | active: false
714 | UnnecessaryApply:
715 | active: true
716 | UnnecessaryBackticks:
717 | active: false
718 | UnnecessaryBracesAroundTrailingLambda:
719 | active: false
720 | UnnecessaryFilter:
721 | active: true
722 | UnnecessaryInheritance:
723 | active: true
724 | UnnecessaryInnerClass:
725 | active: false
726 | UnnecessaryLet:
727 | active: false
728 | UnnecessaryParentheses:
729 | active: false
730 | allowForUnclearPrecedence: false
731 | UntilInsteadOfRangeTo:
732 | active: false
733 | UnusedImports:
734 | active: false
735 | UnusedParameter:
736 | active: true
737 | allowedNames: 'ignored|expected'
738 | UnusedPrivateClass:
739 | active: true
740 | UnusedPrivateMember:
741 | active: true
742 | allowedNames: ''
743 | UnusedPrivateProperty:
744 | active: true
745 | allowedNames: '_|ignored|expected|serialVersionUID'
746 | UseAnyOrNoneInsteadOfFind:
747 | active: true
748 | UseArrayLiteralsInAnnotations:
749 | active: true
750 | UseCheckNotNull:
751 | active: true
752 | UseCheckOrError:
753 | active: true
754 | UseDataClass:
755 | active: false
756 | allowVars: false
757 | UseEmptyCounterpart:
758 | active: false
759 | UseIfEmptyOrIfBlank:
760 | active: false
761 | UseIfInsteadOfWhen:
762 | active: false
763 | ignoreWhenContainingVariableDeclaration: false
764 | UseIsNullOrEmpty:
765 | active: true
766 | UseLet:
767 | active: false
768 | UseOrEmpty:
769 | active: true
770 | UseRequire:
771 | active: true
772 | UseRequireNotNull:
773 | active: true
774 | UseSumOfInsteadOfFlatMapSize:
775 | active: false
776 | UselessCallOnNotNull:
777 | active: true
778 | UtilityClassWithPublicConstructor:
779 | active: true
780 | VarCouldBeVal:
781 | active: true
782 | ignoreLateinitVar: false
783 | WildcardImport:
784 | active: true
785 | excludeImports:
786 | - 'java.util.*'
--------------------------------------------------------------------------------