|
2 | 2 |
|
3 | 3 | [](https://maven-badges.herokuapp.com/maven-central/com.ensody.reactivestate/reactivestate?gav=true)
|
4 | 4 |
|
5 |
| -Easy reactive state management and ViewModels for Kotlin Multiplatform. No boilerplate. Compatible with Android. |
| 5 | +Easy reactive state management and ViewModels for Kotlin Multiplatform. |
6 | 6 |
|
7 |
| -ReactiveState-Kotlin provides these foundations for building multiplatform ViewModels and lower-level logic: |
| 7 | +See the [ReactiveState documentation](https://ensody.github.io/ReactiveState-Kotlin/reactivestate-core/) for more details. |
8 | 8 |
|
9 |
| -* [reactive programming](https://ensody.github.io/ReactiveState-Kotlin/reactive-programming/): everything is recomputed/updated automatically based on straightforward code |
10 |
| -* [demand-driven programming](https://ensody.github.io/ReactiveState-Kotlin/demand-driven-programming/): resource-consuming computations and values are allocated on-demand and disposed when not needed |
11 |
| -* [multiplatform](https://ensody.github.io/ReactiveState-Kotlin/multiplatform-viewmodels/): share your ViewModels and reactive state handling logic between all platforms |
12 |
| -* [event handling](https://ensody.github.io/ReactiveState-Kotlin/event-handling/): simple events based on interfaces (more composable and less boilerplate than sealed classes) |
13 |
| -* [automatic error catching](https://ensody.github.io/ReactiveState-Kotlin/error-handling/): no more forgotten try-catch or copy-pasted error handling logic all over the place |
14 |
| -* [coroutine-based unit tests](https://ensody.github.io/ReactiveState-Kotlin/unit-testing-coroutines/): worry no more about passing around `CoroutineDispatcher`s everywhere |
15 |
| -* [lifecycle handling](https://ensody.github.io/ReactiveState-Kotlin/lifecycle-handling/) |
16 |
| -* [state restoration](https://ensody.github.io/ReactiveState-Kotlin/state-restoration/) |
| 9 | +## Examples |
17 | 10 |
|
18 |
| -See the [ReactiveState documentation](https://ensody.github.io/ReactiveState-Kotlin/) for more details. |
| 11 | +Map/transform a StateFlow: |
19 | 12 |
|
20 |
| -## Supported platforms |
21 |
| - |
22 |
| -android, jvm, ios, tvos, watchos, macosArm64, macosX64, mingwX64, linuxX64 |
23 |
| - |
24 |
| -## Installation |
25 |
| - |
26 |
| -Add the package to your `build.gradle`'s `dependencies {}`: |
27 |
| - |
28 |
| -```groovy |
29 |
| -dependencies { |
30 |
| - // Add the BOM using the desired ReactiveState version |
31 |
| - api platform("com.ensody.reactivestate:reactivestate-bom:VERSION") |
| 13 | +```kotlin |
| 14 | +val number = MutableStateFlow(0) |
| 15 | +val doubledNumber: StateFlow<Int> = derived { 2 * get(number) } |
| 16 | +``` |
32 | 17 |
|
33 |
| - // Leave out the version number from now on: |
34 |
| - implementation "com.ensody.reactivestate:reactivestate" |
| 18 | +Collect two StateFlows (just collect without transforming): |
35 | 19 |
|
36 |
| - // Utils for unit tests that want to use coroutines |
37 |
| - implementation "com.ensody.reactivestate:reactivestate-test" |
38 |
| - // Note: kotlin-coroutines-test only supports the "jvm" target, |
39 |
| - // so reactivestate-test has the same limitation |
| 20 | +```kotlin |
| 21 | +val base = MutableStateFlow(0) |
| 22 | +val extra = MutableStateFlow(0) |
| 23 | +autoRun { |
| 24 | + if (get(base) + get(extra) > 10) { |
| 25 | + alert("You're flying too high") |
| 26 | + } |
40 | 27 | }
|
41 | 28 | ```
|
42 | 29 |
|
43 |
| -Also, make sure you've integrated the Maven Central repo, e.g. in your root `build.gradle`: |
| 30 | +Multiplatform ViewModels with automatic error handling and loading indicator tracking: |
44 | 31 |
|
45 |
| -```groovy |
46 |
| -subprojects { |
47 |
| - repositories { |
48 |
| - // ... |
49 |
| - mavenCentral() |
50 |
| - // ... |
| 32 | +```kotlin |
| 33 | +class ExampleViewModel(scope: CoroutineScope, val repository: ExampleRepository) : ReactiveViewModel(scope) { |
| 34 | + val inputFieldValue = MutableStateFlow("default") |
| 35 | + |
| 36 | + fun submit() { |
| 37 | + // The launch function automatically catches exceptions and increments/decrements the loading indicator. |
| 38 | + // This way you can't forget the fundamentals that have to be always handled correctly. |
| 39 | + launch { |
| 40 | + repository.submit(inputFieldValue.value) |
| 41 | + } |
51 | 42 | }
|
52 | 43 | }
|
53 | 44 | ```
|
54 | 45 |
|
55 |
| -## Quick intro |
56 |
| - |
57 |
| -The following two principles are here to give you a quick idea of the reactive programming aspect only. |
58 |
| -The "Guide" section in the [documentation](https://ensody.github.io/ReactiveState-Kotlin/) describes how to work with the more advanced aspects like multiplatform ViewModels, lifecycle handling, etc. |
59 |
| - |
60 |
| -Note: While the discussion is about `StateFlow`, you can also use `LiveData` or even implement extensions for other observable values. |
61 |
| - |
62 |
| -### Observing StateFlow |
63 |
| - |
64 |
| -Imagine you have an input form with first and last name and want to observe two `StateFlow` values at the same time: |
65 |
| - |
66 |
| -* `isFirstNameValid: StateFlow<Boolean>` |
67 |
| -* `isLastNameValid: StateFlow<Boolean>` |
68 |
| - |
69 |
| -This is how you'd do it by using the `autoRun` function: |
| 46 | +Intercept MutableStateFlow: |
70 | 47 |
|
71 | 48 | ```kotlin
|
72 |
| -autoRun { |
73 |
| - submitButton.isEnabled = get(isFirstNameValid) && get(isLastNameValid) |
| 49 | +public val state: MutableStateFlow<String> = MutableStateFlow("value").afterUpdate { |
| 50 | + // This is called every time after someone sets `state.value = ...` |
74 | 51 | }
|
75 | 52 | ```
|
76 | 53 |
|
77 |
| -With `get(isFirstNameValid)` you retrieve `isFirstNameValid.value` and at the same time tell `autoRun` to re-execute the block whenever the value is changed. |
78 |
| -That code is similar to writing this: |
| 54 | +Convert StateFlow to MutableStateFlow: |
79 | 55 |
|
80 | 56 | ```kotlin
|
81 |
| -lifecycleScope.launchWhenStarted { |
82 |
| - isFirstNameValid |
83 |
| - .combine(isLastNameValid) { firstNameValid, lastNameValid -> |
84 |
| - firstNameValid to lastNameValid |
85 |
| - } |
86 |
| - .conflate() |
87 |
| - .collect { (firstNameValid, lastNameValid) -> |
88 |
| - try { |
89 |
| - submitButton.isEnabled = firstNameValid && lastNameValid |
90 |
| - } catch (e: CancellationException) { |
91 |
| - throw e |
92 |
| - } catch (e: Throwable) { |
93 |
| - onError(e) |
94 |
| - } |
95 |
| - } |
| 57 | +val readOnly: StateFlow<Int> = getSomeStateFlow() |
| 58 | +val mutable: MutableStateFlow<Int> = readOnly.toMutable { value: Int -> |
| 59 | + // This is executed whenever someone sets `mutable.value = ...`. |
96 | 60 | }
|
97 | 61 | ```
|
98 | 62 |
|
99 |
| -### Reactive StateFlow / reactive data |
| 63 | +See the [ReactiveState documentation](https://ensody.github.io/ReactiveState-Kotlin/reactivestate-core/) for more details. |
100 | 64 |
|
101 |
| -The same principle can be used to create a `derived`, reactive `StateFlow`: |
| 65 | +## Supported platforms |
102 | 66 |
|
103 |
| -```kotlin |
104 |
| -val isFormValid: StateFlow<Boolean> = derived { |
105 |
| - get(isFirstNameValid) && get(isLastNameValid) |
106 |
| -} |
107 |
| -``` |
| 67 | +android, jvm, ios, tvos, watchos, macosArm64, macosX64, mingwX64, linuxX64 |
108 | 68 |
|
109 |
| -Now you can use `autoRun { submitButton.isEnabled = get(isFormValid) }` in the rest of your code. |
| 69 | +## Installation |
110 | 70 |
|
111 |
| -Going even further, `isFirstNameValid` itself would usually also be the result of a `derived` computation. |
112 |
| -So, you can have multiple layers of reactive `derived` `StateFlow`s. |
| 71 | +Add the package to your `build.gradle`'s `dependencies {}`: |
113 | 72 |
|
114 |
| -## Relation to Jetpack Compose / Flutter / React |
| 73 | +```groovy |
| 74 | +dependencies { |
| 75 | + // Add the BOM using the desired ReactiveState version |
| 76 | + api platform("com.ensody.reactivestate:reactivestate-bom:VERSION") |
| 77 | + // Leave out the version number from now on. |
115 | 78 |
|
116 |
| -Reactive UI frameworks like Jetpack Compose automatically rebuild the UI whenever e.g. a `StateFlow` changes. |
117 |
| -So, in the UI layer `autoRun` can usually be replaced with a `Composable`. |
| 79 | + // Jetpack Compose integration |
| 80 | + implementation "com.ensody.reactivestate:reactivestate-compose" |
118 | 81 |
|
119 |
| -However, below the UI your data still needs to be reactive, too. |
120 |
| -Here ReactiveState provides `derived` to automatically recompute a `StateFlow` based on other `StateFlow`s. |
121 |
| -This pattern is very useful in practice and provides the perfect foundation for frameworks like Jetpack Compose which primarily focus on the UI aspect. |
122 |
| -ReactiveState's `derived` and `autoRun` provide the same reactivity for your data and business logic. |
| 82 | + // Android-only integration for Activity/Fragment |
| 83 | + implementation "com.ensody.reactivestate:reactivestate-android" |
123 | 84 |
|
124 |
| -In Jetpack Compose you even have `derivedStateOf` which is very similar to `derived`. |
125 |
| -So, you can choose whether you want to build your business logic based on the official coroutines library (`StateFlow`/`derived`) or Jetpack Compose (`State`/`derivedStateOf`). However, the coroutines library has the advantage that it's available for more platforms and it's fully independent of any UI frameworks. Finally, most open-source non-UI libraries will probably be based on coroutines, so `StateFlow` based code might also be better for compatibility/interoperability. |
| 85 | + // UI-independent core APIs |
| 86 | + implementation "com.ensody.reactivestate:reactivestate-core" |
126 | 87 |
|
127 |
| -In other words, the combination of both solutions used together results in a fully reactive, multiplatform codebase - which improves code simplicity and avoids many bugs. |
| 88 | + // Utils for unit tests that want to use coroutines |
| 89 | + implementation "com.ensody.reactivestate:reactivestate-core-test" |
128 | 90 |
|
129 |
| -Moreover, Jetpack Compose currently doesn't provide any multiplatform ViewModel support or any large-scale architecture. |
130 |
| -So, this library solves that by providing `BaseReactiveState` for ViewModels. |
131 |
| -It also comes with a lifecycle-aware event system (`eventNotifier`) and loading state handling (so you can track one or multiple different loading indicators based on coroutines that you launch). |
| 91 | + // Android-only unit test extensions |
| 92 | + implementation "com.ensody.reactivestate:reactivestate-android-test" |
| 93 | +} |
| 94 | +``` |
132 | 95 |
|
133 |
| -## See also |
| 96 | +Also, make sure you've integrated the Maven Central repo, e.g. in your root `build.gradle`: |
134 | 97 |
|
135 |
| -This library is based on [reactive_state](https://github.com/ensody/reactive_state) for Flutter and adapted to Kotlin Multiplatform and Android patterns. |
| 98 | +```groovy |
| 99 | +subprojects { |
| 100 | + repositories { |
| 101 | + // ... |
| 102 | + mavenCentral() |
| 103 | + // ... |
| 104 | + } |
| 105 | +} |
| 106 | +``` |
136 | 107 |
|
137 | 108 | ## License
|
138 | 109 |
|
139 | 110 | ```
|
140 |
| -Copyright 2024 Ensody GmbH, Waldemar Kornewald |
| 111 | +Copyright 2025 Ensody GmbH, Waldemar Kornewald |
141 | 112 |
|
142 | 113 | Licensed under the Apache License, Version 2.0 (the "License");
|
143 | 114 | you may not use this file except in compliance with the License.
|
|
0 commit comments