Skip to content

Commit cc89a6c

Browse files
feat: add reactions + mobk viewmodel wrapper
1 parent 24a1276 commit cc89a6c

File tree

14 files changed

+339
-16
lines changed

14 files changed

+339
-16
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/secret.properties
22
/local.properties
3-
/build/
43
/.gradle/
54
/.idea/
65
/mobk-compose/build/
76
/mobk-core/build/
7+
build/

mobk-core/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ kotlin {
2828
iosSimulatorArm64()
2929
).forEach {
3030
it.binaries.framework {
31-
baseName = "shared"
31+
baseName = "mobk-core"
3232
}
3333

3434
}

mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/api/Api.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import io.monkeypatch.mobk.core.*
44
import kotlin.properties.ReadOnlyProperty
55
import kotlin.properties.ReadWriteProperty
66
import kotlin.reflect.KProperty
7+
import kotlin.time.Duration
78

89

910
public fun autorun(body: () -> Unit): ReactionDisposer =
@@ -13,6 +14,23 @@ public fun action(body: () -> Unit) {
1314
Action(body).runAction()
1415
}
1516

17+
public fun <T> reaction(context: ReactiveContext = ReactiveContext.main,
18+
delay: Duration? = null,
19+
equals: ((T?, T?) -> Boolean)? = null,
20+
onError: ReactionErrorHandler? = null,trackingFn: (Reaction) -> T, effect: (T?) -> Unit): ReactionDisposer =
21+
createReaction(
22+
context = context,
23+
delay = delay,
24+
equals = equals,
25+
onError = onError,
26+
trackingFn = trackingFn, effect = effect)
27+
28+
public fun whenReaction(context: ReactiveContext = ReactiveContext.main,
29+
timeout: Duration? = null,
30+
onError: ReactionErrorHandler? = null,
31+
predicate: (Reaction) -> Boolean,
32+
effect: () -> Unit) = createWhenReaction(context = context, timeout = timeout, onError = onError, predicate = predicate, effect = effect)
33+
1634
public fun <T> observable(initialValue: T): ReadWriteProperty<Any?, T> = ObservableDelegate(initialValue)
1735

1836
public fun <T> computed(body: () -> T): ReadOnlyProperty<Any?, T> = ComputedDelegate(body)

mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/Autorun.kt

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,5 @@
11
package io.monkeypatch.mobk.core
22

3-
public interface ReactionDisposer {
4-
public operator fun invoke()
5-
}
6-
7-
internal data class ReactionDisposerImpl(private val reaction: Reaction): ReactionDisposer {
8-
override operator fun invoke() {
9-
reaction.dispose()
10-
}
11-
}
123

134
internal fun createAutorun(
145
context: ReactiveContext = ReactiveContext.main,

mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/Derivation.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public enum class DerivationState {
77
STALE
88
}
99

10-
internal interface Derivation {
10+
interface Derivation {
1111
val name: String
1212
var observables: Set<Atom>
1313
var newObservables: MutableSet<Atom>

mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/Exception.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ public sealed class MobXException(message: String): Exception(message) {
88

99
/// This captures the stack trace when user-land code throws an exception
1010
public class MobXCaughtException(public val exception: Throwable): MobXException("MobXCaughtException: $exception")
11+
12+
public class MobXTimeoutException(message: String): MobXException(message)
1113
}

mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/Reaction.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package io.monkeypatch.mobk.core
22

3-
internal typealias ReactionErrorHandler = (error: Throwable, reaction: Reaction) -> Unit
3+
typealias ReactionErrorHandler = (error: Throwable, reaction: Reaction) -> Unit
44

5-
internal interface Reaction : Derivation {
5+
interface Reaction : Derivation {
66
val isDisposed: Boolean
77

88
fun dispose()
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package io.monkeypatch.mobk.core
2+
3+
class ReactionDisposers {
4+
private val disposers = mutableListOf<ReactionDisposer>()
5+
6+
fun add(disposer: ReactionDisposer) {
7+
disposers.add(disposer)
8+
}
9+
10+
fun clear() {
11+
disposers.forEach { it() }
12+
disposers.clear()
13+
}
14+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package io.monkeypatch.mobk.core
2+
3+
import kotlinx.coroutines.Delay
4+
import kotlinx.coroutines.Dispatchers
5+
import kotlinx.coroutines.delay
6+
import kotlinx.coroutines.launch
7+
import kotlinx.coroutines.withContext
8+
import kotlinx.coroutines.withTimeout
9+
import kotlin.time.Duration
10+
11+
public interface ReactionDisposer {
12+
public operator fun invoke()
13+
}
14+
15+
internal data class ReactionDisposerImpl(private val reaction: Reaction): ReactionDisposer {
16+
override operator fun invoke() {
17+
reaction.dispose()
18+
}
19+
}
20+
21+
internal fun <T> createReaction(
22+
context: ReactiveContext = ReactiveContext.main,
23+
name: String = context.nameFor("Reaction"),
24+
delay: Duration? = null,
25+
equals: ((T?, T?) -> Boolean)? = null,
26+
onError: ReactionErrorHandler? = null,
27+
trackingFn: (Reaction) -> T,
28+
effect: (T?) -> Unit
29+
): ReactionDisposer {
30+
var lastValue: T? = null
31+
var firstTime = true
32+
var runSync = delay == null
33+
var rxn: ReactionImpl? = null
34+
val effectAction = effect
35+
36+
val reactionRunner: () -> Unit = {
37+
val reaction = rxn
38+
if (reaction == null || !reaction.isDisposed) {
39+
var changed = false
40+
41+
reaction?.run {
42+
track {
43+
val nextValue = trackingFn(reaction)
44+
val isEqual = if (equals != null) equals(nextValue, lastValue) else lastValue == nextValue
45+
46+
changed = firstTime || !isEqual
47+
lastValue = nextValue
48+
}
49+
}
50+
51+
val canInvokeEffect = !firstTime && changed
52+
53+
if (canInvokeEffect) {
54+
effectAction(lastValue)
55+
}
56+
57+
if (firstTime) {
58+
firstTime = false
59+
}
60+
}
61+
}
62+
63+
var isScheduled = false
64+
rxn = ReactionImpl(
65+
context, name, onError
66+
) {
67+
if (firstTime || runSync) {
68+
reactionRunner()
69+
} else if(!isScheduled) {
70+
isScheduled = true
71+
72+
context.config.reactionCoroutineScope.launch {
73+
if (delay != null) {
74+
delay(delay)
75+
}
76+
77+
isScheduled = false
78+
79+
withContext(Dispatchers.Main) {
80+
reactionRunner()
81+
}
82+
}
83+
}
84+
}
85+
rxn.schedule()
86+
return ReactionDisposerImpl(rxn)
87+
}
88+
89+
internal fun createWhenReaction(
90+
context: ReactiveContext = ReactiveContext.main,
91+
name: String = context.nameFor("Reaction"),
92+
timeout: Duration? = null,
93+
onError: ReactionErrorHandler? = null,
94+
predicate: (Reaction) -> Boolean,
95+
effect: () -> Unit
96+
): ReactionDisposer {
97+
val effectAction = effect
98+
var dispose: ReactionDisposer? = null
99+
val rxn: ReactionImpl? = null
100+
101+
if (timeout != null) {
102+
context.config.reactionCoroutineScope.launch {
103+
withTimeout(timeout) {
104+
val d = dispose
105+
val r = rxn
106+
if (d != null && r != null) {
107+
if (!r.isDisposed) {
108+
d()
109+
110+
val error = MobXException.MobXTimeoutException("WHEN_TIMEOUT")
111+
if (onError != null) {
112+
onError(error, r)
113+
} else {
114+
throw error
115+
}
116+
}
117+
}
118+
}
119+
}
120+
}
121+
122+
dispose = createAutorun(
123+
context = context,
124+
name = name,
125+
onError = onError
126+
) { reaction ->
127+
if (predicate(reaction)) {
128+
reaction.dispose()
129+
effectAction()
130+
}
131+
}
132+
return dispose
133+
}

mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/ReactiveContext.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package io.monkeypatch.mobk.core
22

33
import io.monkeypatch.mobk.utils.isMainThread
4+
import kotlinx.coroutines.CoroutineScope
5+
import kotlinx.coroutines.Dispatchers
46
import kotlin.native.concurrent.ThreadLocal
57
import kotlin.properties.Delegates
68

@@ -59,7 +61,8 @@ public data class ReactiveConfig(
5961
val writePolicy: ReactiveWritePolicy,
6062
val readPolicy: ReactiveReadPolicy,
6163
val maxIterations: Int = 100,
62-
val enforceWriteOnMainThread: Boolean = true
64+
val enforceWriteOnMainThread: Boolean = true,
65+
val reactionCoroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Default)
6366
) {
6467
internal val reactionErrorHandlers: MutableSet<ReactionErrorHandler> = mutableSetOf()
6568

0 commit comments

Comments
 (0)