Skip to content

Commit ecb3538

Browse files
committed
Unit testing API improvements
1 parent a915e18 commit ecb3538

File tree

8 files changed

+133
-18
lines changed

8 files changed

+133
-18
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
# ReactiveState for Kotlin Multiplatform and Android
1+
# ReactiveState
22

3-
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.ensody.reactivestate/reactivestate/badge.svg?gav=true)](https://maven-badges.herokuapp.com/maven-central/com.ensody.reactivestate/reactivestate-core?gav=true)
3+
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.ensody.reactivestate/reactivestate-core/badge.svg?gav=true)](https://maven-badges.herokuapp.com/maven-central/com.ensody.reactivestate/reactivestate-core?gav=true)
44

5-
Easy reactive state management and ViewModels for Kotlin Multiplatform.
5+
Kotlin Multiplatform ViewModels and reactive state management based on `StateFlow`.
66

7-
See the [ReactiveState documentation](https://ensody.github.io/ReactiveState-Kotlin/reactivestate-core/) for more details.
7+
See the [ReactiveState documentation](https://ensody.github.io/ReactiveState-Kotlin/) for more details.
88

99
## Examples
1010

@@ -60,7 +60,7 @@ val mutable: MutableStateFlow<Int> = readOnly.toMutable { value: Int ->
6060
}
6161
```
6262

63-
See the [ReactiveState documentation](https://ensody.github.io/ReactiveState-Kotlin/reactivestate-core/) for more details.
63+
See the [ReactiveState documentation](https://ensody.github.io/ReactiveState-Kotlin/) for more details.
6464

6565
## Supported platforms
6666

build.gradle

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -219,16 +219,11 @@ subprojects {
219219

220220
apply from: "$rootDir/gradle/common/dokka.gradle"
221221
setupDokka()
222-
dependencies {
223-
dokka(project)
224-
}
225222
dokka {
226-
dokkaSourceSets {
223+
dokkaPublications {
227224
configureEach {
228-
includes.from("README.md")
225+
includes.from(rootProject.file("docs/README.md"))
229226
}
230-
}
231-
dokkaPublications {
232227
html {
233228
outputDirectory.set(project.file("build/docs/html"))
234229
}

docs/README.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# ReactiveState
2+
3+
Kotlin Multiplatform ViewModels and reactive state management based on `StateFlow`.
4+
5+
See the `reactivestate-core` module documentation as a starting point with several usage examples.
6+
7+
## Short examples
8+
9+
Map/transform a StateFlow:
10+
11+
```kotlin
12+
val number = MutableStateFlow(0)
13+
val doubledNumber: StateFlow<Int> = derived { 2 * get(number) }
14+
```
15+
16+
Collect two StateFlows (just collect without transforming):
17+
18+
```kotlin
19+
val base = MutableStateFlow(0)
20+
val extra = MutableStateFlow(0)
21+
autoRun {
22+
if (get(base) + get(extra) > 10) {
23+
alert("You're flying too high")
24+
}
25+
}
26+
```
27+
28+
Multiplatform ViewModels with automatic error handling and loading indicator tracking:
29+
30+
```kotlin
31+
class ExampleViewModel(scope: CoroutineScope, val repository: ExampleRepository) : ReactiveViewModel(scope) {
32+
val inputFieldValue = MutableStateFlow("default")
33+
34+
fun submit() {
35+
// The launch function automatically catches exceptions and increments/decrements the loading indicator.
36+
// This way you can't forget the fundamentals that have to be always handled correctly.
37+
launch {
38+
repository.submit(inputFieldValue.value)
39+
}
40+
}
41+
}
42+
```
43+
44+
Intercept MutableStateFlow:
45+
46+
```kotlin
47+
public val state: MutableStateFlow<String> = MutableStateFlow("value").afterUpdate {
48+
// This is called every time after someone sets `state.value = ...`
49+
}
50+
```
51+
52+
Convert StateFlow to MutableStateFlow:
53+
54+
```kotlin
55+
val readOnly: StateFlow<Int> = getSomeStateFlow()
56+
val mutable: MutableStateFlow<Int> = readOnly.toMutable { value: Int ->
57+
// This is executed whenever someone sets `mutable.value = ...`.
58+
}
59+
```
60+
61+
## Supported platforms
62+
63+
android, jvm, ios, tvos, watchos, macosArm64, macosX64, mingwX64, linuxX64
64+
65+
## Installation
66+
67+
Add the package to your `build.gradle`:
68+
69+
```groovy
70+
dependencies {
71+
// Add the BOM using the desired ReactiveState version
72+
api platform("com.ensody.reactivestate:reactivestate-bom:VERSION")
73+
// Leave out the version number from now on.
74+
75+
// Jetpack Compose integration
76+
implementation "com.ensody.reactivestate:reactivestate-compose"
77+
78+
// Android-only integration for Activity/Fragment
79+
implementation "com.ensody.reactivestate:reactivestate-android"
80+
81+
// UI-independent core APIs
82+
implementation "com.ensody.reactivestate:reactivestate-core"
83+
84+
// Utils for unit tests that want to use coroutines
85+
implementation "com.ensody.reactivestate:reactivestate-core-test"
86+
87+
// Android-only unit test extensions
88+
implementation "com.ensody.reactivestate:reactivestate-android-test"
89+
}
90+
```
91+
92+
Also, make sure you've integrated the Maven Central repo, e.g. in your root `build.gradle`:
93+
94+
```groovy
95+
subprojects {
96+
repositories {
97+
// ...
98+
mavenCentral()
99+
// ...
100+
}
101+
}
102+
```

gradle/common/scm-version.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ ext.setupScmVersion = { args ->
3030
}
3131
}
3232
allprojects {
33-
version = scmVersion.version
33+
version = System.getenv("OVERRIDE_VERSION") ?: scmVersion.version
3434
}
3535
println("Version: $version")
3636
}

reactivestate-core-test/src/commonMain/kotlin/com/ensody/reactivestate/test/CoroutineTestRule.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package com.ensody.reactivestate.test
22

3+
import com.ensody.reactivestate.ContextualErrorsFlow
4+
import com.ensody.reactivestate.ContextualValRoot
35
import com.ensody.reactivestate.DefaultCoroutineDispatcherConfig
46
import com.ensody.reactivestate.dispatchers
7+
import kotlinx.coroutines.CoroutineScope
58
import kotlinx.coroutines.Dispatchers
9+
import kotlinx.coroutines.plus
610
import kotlinx.coroutines.test.StandardTestDispatcher
711
import kotlinx.coroutines.test.TestCoroutineScheduler
812
import kotlinx.coroutines.test.TestDispatcher
@@ -30,7 +34,7 @@ public open class CoroutineTestRule(
3034
testDispatcherBuilder: (TestCoroutineScheduler) -> TestDispatcher = { StandardTestDispatcher(it) },
3135
context: CoroutineContext = EmptyCoroutineContext,
3236
) {
33-
public val testScope: TestScope = TestScope(context)
37+
public val testScope: TestScope = TestScope(ContextualErrorsFlow.valued { ThrowOnEmitFlow() } + context)
3438
public val testDispatcher: TestDispatcher = testDispatcherBuilder(testScope.testScheduler)
3539

3640
init {
@@ -52,3 +56,7 @@ public open class CoroutineTestRule(
5256
dispatchers = DefaultCoroutineDispatcherConfig
5357
}
5458
}
59+
60+
/** Creates a [TestScope.backgroundScope] with a [ContextualValRoot] for ViewModels. */
61+
public fun TestScope.contextualBackgroundScope(): CoroutineScope =
62+
backgroundScope + ContextualValRoot()
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.ensody.reactivestate.test
2+
3+
import com.ensody.reactivestate.MutableFlow
4+
5+
public class ThrowOnEmitFlow : MutableFlow<Throwable> by MutableFlow() {
6+
override fun tryEmit(value: Throwable): Boolean {
7+
throw value
8+
}
9+
10+
override suspend fun emit(value: Throwable) {
11+
throw value
12+
}
13+
}

reactivestate-core/src/commonMain/kotlin/com/ensody/reactivestate/CoroutineLauncher.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,6 @@ public interface CoroutineLauncher {
9393
}
9494
}
9595

96-
public val ContextualOnError: ContextualVal<(Throwable) -> Unit> = ContextualVal("ContextualOnError") {
97-
{ throw it }
98-
}
99-
10096
public val ContextualStateFlowStore: ContextualVal<StateFlowStore> = ContextualVal("ContextualStateFlowStore") {
10197
error("ContextualStateFlowStore missing in CoroutineScope")
10298
}

reactivestate-core/src/commonMain/kotlin/com/ensody/reactivestate/ReactiveViewModel.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@ public abstract class ReactiveViewModel(final override val scope: CoroutineScope
2525

2626
@ExperimentalReactiveStateApi
2727
public val ContextualErrorsFlow: ContextualVal<MutableFlow<Throwable>> = ContextualVal("ContextualErrorsFlow") {
28+
requireContextualValRoot(it)
2829
MutableFlow(capacity = Channel.UNLIMITED, onBufferOverflow = BufferOverflow.DROP_OLDEST)
2930
}

0 commit comments

Comments
 (0)