Skip to content

Commit 80bf402

Browse files
committed
Refactored experimental OnInit API
1 parent c4468b7 commit 80bf402

File tree

9 files changed

+57
-42
lines changed

9 files changed

+57
-42
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.bat text eol=crlf

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* Added reactive dependency injection solution (see `DI`) with graph tracking that can even re-create ViewModels when a DI module/node is changed at runtime.
66
* Added `ContextualVal`/`ContextualValSuspend` for coroutine-local variables (like thread-locals or `CompositionLocal` for coroutines).
77
* Made the experimental `ReactiveViewModel` more flexible by building it around `ContextualVal`.
8+
* Added experimental `OnInit` API for initial loading of ViewModel data with error and retry handling.
89
* Added Composable `rememberOnViewModel { ... }` which remembers the block's result across configuration changes.
910
* Added Composable `DI.derivedState { ... }` and `DI.derivedValue { ... }` which remember the block's result until a dependency changes and re-render the UI when needed.
1011
* Changed `loading` and all `withLoading` parameters from `MutableValueFlow` to `MutableStateFlow`.

build-logic/build-logic-base/src/main/kotlin/com/ensody/buildlogic/KotlinMultiplatformExt.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,20 +75,26 @@ fun KotlinMultiplatformExtension.applyKmpHierarchy(block: KotlinHierarchyBuilder
7575
}
7676
group("appleMobile") {
7777
withIos()
78+
group("ios")
7879
withTvos()
7980
withWatchos()
8081
}
8182
group("compose") {
83+
group("js")
8284
withJs()
85+
group("wasmJs")
8386
withWasmJs()
8487
withWasmWasi()
88+
group("ios")
8589
withIos()
8690
withJvm()
8791
withAndroidTarget()
8892
}
8993
group("nonJvm") {
9094
withNative()
95+
group("js")
9196
withJs()
97+
group("wasmJs")
9298
withWasmJs()
9399
}
94100
group("nonJs") {

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ ktlint-cli = { module = "com.pinterest.ktlint:ktlint-cli", version = "1.6.0" }
4040
mockk = { module = "io.mockk:mockk", version = "1.14.4" }
4141
robolectric = { module = "org.robolectric:robolectric", version = "4.15.1" }
4242

43-
gradle-android = { module = "com.android.tools.build:gradle", version = "8.10.0" }
43+
gradle-android = { module = "com.android.tools.build:gradle", version = "8.11.1" }
4444
gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
4545
gradle-cocoapods = { module = "org.jetbrains.kotlin.native.cocoapods:org.jetbrains.kotlin.native.cocoapods.gradle.plugin", version.ref = "kotlin" }
4646
gradle-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version = "2.0.0" }

reactivestate-compose/src/composeMain/kotlin/com/ensody/reactivestate/compose/ViewModelExt.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,12 @@ import androidx.lifecycle.viewmodel.compose.viewModel
1313
import com.ensody.reactivestate.ContextualErrorsFlow
1414
import com.ensody.reactivestate.ContextualStateFlowStore
1515
import com.ensody.reactivestate.ContextualValRoot
16-
import com.ensody.reactivestate.CoroutineLauncher
1716
import com.ensody.reactivestate.DI
1817
import com.ensody.reactivestate.ExperimentalReactiveStateApi
1918
import com.ensody.reactivestate.InMemoryStateFlowStore
2019
import com.ensody.reactivestate.ReactiveStateContext
20+
import com.ensody.reactivestate.ReactiveViewModel
2121
import com.ensody.reactivestate.invokeOnCompletion
22-
import com.ensody.reactivestate.triggerOnInit
2322
import com.ensody.reactivestate.withSpinLock
2423
import kotlinx.coroutines.CoroutineScope
2524
import kotlinx.coroutines.MainScope
@@ -39,14 +38,14 @@ import kotlinx.coroutines.sync.Mutex
3938
*/
4039
@ExperimentalReactiveStateApi
4140
@Composable
42-
public inline fun <reified VM : CoroutineLauncher> reactiveViewModel(
41+
public inline fun <reified VM : ReactiveViewModel> reactiveViewModel(
4342
key: String? = null,
4443
crossinline onError: (Throwable) -> Unit,
4544
crossinline provider: ReactiveStateContext.() -> VM,
4645
): State<VM> =
4746
onViewModel(key = key) {
4847
provider().also {
49-
it.triggerOnInit()
48+
it.onInit.trigger()
5049
}
5150
}.also { viewModel ->
5251
LaunchedEffect(viewModel.value) {

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import kotlinx.coroutines.CoroutineScope
44
import kotlinx.coroutines.flow.MutableStateFlow
55
import kotlinx.coroutines.plus
66
import kotlinx.coroutines.withContext
7+
import kotlin.collections.set
78
import kotlin.coroutines.CoroutineContext
89
import kotlin.coroutines.EmptyCoroutineContext
910
import kotlin.coroutines.coroutineContext
@@ -105,3 +106,32 @@ public fun requireContextualValRoot(scope: CoroutineScope) {
105106
public fun requireContextualValRoot(context: CoroutineContext) {
106107
requireNotNull(context[ContextualValRootInternal.key]) { "ContextualValRoot is missing in the CoroutineScope" }
107108
}
109+
110+
public val ContextualStore: ContextualVal<ContextualValStore> = ContextualVal("ContextualStore") {
111+
requireContextualValRoot(it)
112+
ContextualValStore()
113+
}
114+
115+
public class ContextualValStore {
116+
public class Key<T>
117+
118+
private val storage = mutableMapOf<Key<*>, Any?>()
119+
120+
public operator fun contains(key: Key<*>): Boolean =
121+
key in storage
122+
123+
@Suppress("UNCHECKED_CAST")
124+
public operator fun <T> get(key: Key<T>): T? =
125+
storage[key] as T?
126+
127+
@Suppress("UNCHECKED_CAST")
128+
public fun <T> getOrPut(
129+
key: Key<T>,
130+
defaultValue: () -> T,
131+
): T =
132+
storage.getOrPut(key, defaultValue) as T
133+
134+
public fun <T> put(key: Key<T>, value: T) {
135+
storage[key] = value
136+
}
137+
}

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

Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ import kotlin.coroutines.EmptyCoroutineContext
1818
import kotlin.js.JsName
1919

2020
/**
21-
* A mechanism for ViewModel initialization-time tasks like repository cache refresh. Use via [ContextualOnInit].
21+
* A mechanism for [ReactiveViewModel] initialization-time tasks like repository cache refresh.
2222
*
2323
* With [observe] you get notified when the ViewModel is ready. On the provided [OnInitContext] you can launch
2424
* coroutines which are associated with your observer. If any of the coroutines fail the [state] will reflect that
25-
* and you can run [trigger] (or the simpler [CoroutineLauncher.triggerOnInit]) again for only the failing observers.
25+
* and you can run [trigger] again for only the failing observers.
2626
*/
2727
@ExperimentalReactiveStateApi
2828
public interface OnInit {
@@ -37,7 +37,7 @@ public interface OnInit {
3737

3838
public fun observe(block: OnInitContext.() -> Unit)
3939
public fun unobserve(block: OnInitContext.() -> Unit)
40-
public fun trigger(source: CoroutineLauncher)
40+
public fun trigger()
4141

4242
public sealed interface State {
4343
public data object Initializing : State
@@ -46,24 +46,6 @@ public interface OnInit {
4646
}
4747
}
4848

49-
/**
50-
* Provides access to a ViewModel's [OnInit] instance.
51-
*/
52-
@ExperimentalReactiveStateApi
53-
public val ContextualOnInit: ContextualVal<OnInit> = ContextualVal("ContextualOnInit") {
54-
requireContextualValRoot(it)
55-
OnInit()
56-
}
57-
58-
@ExperimentalReactiveStateApi
59-
public val CoroutineLauncher.onInit: OnInit get() = ContextualOnInit.get(scope)
60-
61-
/** Runs [OnInit.trigger] for this class. */
62-
@ExperimentalReactiveStateApi
63-
public fun CoroutineLauncher.triggerOnInit() {
64-
ContextualOnInit.get(scope).trigger(this)
65-
}
66-
6749
/**
6850
* When the ViewModel is ready this API allows launching coroutines.
6951
*
@@ -110,32 +92,25 @@ public interface OnInitContext {
11092

11193
@JsName("createOnInit")
11294
@ExperimentalReactiveStateApi
113-
public fun OnInit(): OnInit =
114-
OnInitImpl()
95+
public fun OnInit(source: CoroutineLauncher): OnInit =
96+
OnInitImpl(source)
11597

116-
private class OnInitImpl : OnInit {
117-
private var source: CoroutineLauncher? = null
98+
private class OnInitImpl(private val source: CoroutineLauncher) : OnInit {
11899
private val observers: MutableStateFlow<List<OnInitContext.() -> Unit>> = MutableStateFlow(emptyList())
119100
private val mutex = Mutex()
120101

121102
override val state: MutableStateFlow<OnInit.State> = MutableStateFlow(OnInit.State.Initializing)
122103

123104
override fun observe(block: OnInitContext.() -> Unit) {
124105
observers.replace { plus(block) }
125-
source?.let {
126-
trigger(it)
127-
}
106+
trigger()
128107
}
129108

130109
override fun unobserve(block: OnInitContext.() -> Unit) {
131110
observers.replace { minus(block) }
132111
}
133112

134-
override fun trigger(source: CoroutineLauncher) {
135-
if (this.source == null) {
136-
this.source = source
137-
}
138-
113+
override fun trigger() {
139114
source.launch(withLoading = null) {
140115
mutex.withLock {
141116
if (observers.value.isNotEmpty()) {

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ import kotlinx.coroutines.flow.MutableStateFlow
1010
*
1111
* The [scope] is meant to be filled with [ContextualVal] entries.
1212
*
13-
* For example, the [ContextualOnInit] hook can be used to launch coroutines via a [CoroutineLauncher]
14-
* directly from the ViewModel constructor, so you can have nicer error handling and loading indicators.
13+
* The [onInit] hook can be used to launch coroutines via a [CoroutineLauncher] directly from the ViewModel constructor,
14+
* so you can have nicer error handling and loading indicators.
1515
*/
1616
@ExperimentalReactiveStateApi
17-
public abstract class ReactiveViewModel(final override val scope: CoroutineScope) : CoroutineLauncher {
17+
public abstract class ReactiveViewModel(public final override val scope: CoroutineScope) : CoroutineLauncher {
18+
public val onInit: OnInit = ContextualStore.get(scope).getOrPut(OnInitKey) { OnInit(this) }
1819
private val emittedErrors: MutableFlow<Throwable> = ContextualErrorsFlow.get(scope)
1920
override val loading: MutableStateFlow<Int> = ContextualLoading.get(scope)
2021
public val stateFlowStore: StateFlowStore by lazy { ContextualStateFlowStore.get(scope) }
@@ -24,6 +25,8 @@ public abstract class ReactiveViewModel(final override val scope: CoroutineScope
2425
}
2526
}
2627

28+
private val OnInitKey = ContextualValStore.Key<OnInit>()
29+
2730
@ExperimentalReactiveStateApi
2831
public val ContextualErrorsFlow: ContextualVal<MutableFlow<Throwable>> = ContextualVal("ContextualErrorsFlow") {
2932
requireContextualValRoot(it)

reactivestate-core/src/commonTest/kotlin/com/ensody/reactivestate/ReactiveViewModelTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ internal class ReactiveViewModelTest : CoroutineTest(ContextualValRoot()) {
1616
fun nestingOfReactiveViewModels() = runTest {
1717
assertEquals(0, viewModel.loading.value)
1818
assertEquals(OnInit.State.Initializing, viewModel.onInit.state.value)
19-
viewModel.triggerOnInit()
19+
viewModel.onInit.trigger()
2020
runCurrent()
2121
assertEquals(OnInit.State.Initializing, viewModel.onInit.state.value)
2222
assertEquals(1, viewModel.loading.value)

0 commit comments

Comments
 (0)