Skip to content

Commit 37937ff

Browse files
authored
Add system property to control snapshot notifier default (#615)
Since the Compose UI snapshot notifier is a global singleton that can be started in one place, this allows you to also switch Molecule's default in that same location. Much easier than updating tens or hundreds of call sites.
1 parent a1e8d4e commit 37937ff

File tree

9 files changed

+152
-13
lines changed

9 files changed

+152
-13
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
## [Unreleased]
44

55
New:
6-
- Add `SnapshotNotifier` enum to control whether Molecule automatically sends snapshot apply notifications. If you are using Molecule with another Compose-based library in a single application, you may want to disable our snapshot notification. See the enum for details on when that is appropriate.
6+
- Add `SnapshotNotifier` enum to control whether Molecule automatically sends snapshot apply notifications.
7+
If you are using Molecule with another Compose-based library in a single application, you may want to disable our snapshot notification.
8+
See the enum for details on when that is appropriate.
9+
Additionally, the `app.cash.molecule.snapshotNotifier` system property can be set to one of the enum entry names to control the default process-wide.
710

811
Changed:
912
- Any specified additional coroutine context elements will now be honored in the coroutine used internally with `RecompositionMode.Immediate` to send frames and cause recomposition to occur. This is observable, most notably, when a `CoroutineDispatcher` is included, as now recompositions which occur after the first, synchronous one will occur on that dispatcher.

molecule-runtime/build.gradle

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ kotlin {
5555
}
5656
it.group("watchos") {}
5757
}
58+
it.group('nonJava') {
59+
it.group('native') {}
60+
it.withJs()
61+
it.withWasmJs()
62+
}
5863
}
5964
}
6065

@@ -84,14 +89,17 @@ kotlin {
8489
}
8590
}
8691

92+
// We use a common folder instead of a common source set because there is no commonizer
93+
// which exposes the Java APIs across these two targets.
94+
androidMain { kotlin.srcDir('src/javaMain/kotlin') }
95+
androidTest { kotlin.srcDir('src/javaTest/kotlin') }
96+
jvmMain { kotlin.srcDir('src/javaMain/kotlin') }
97+
jvmTest { kotlin.srcDir('src/javaTest/kotlin') }
98+
8799
// We use a common folder instead of a common source set because there is no commonizer
88100
// which exposes the browser APIs across these two targets.
89-
jsMain {
90-
kotlin.srcDir('src/browserMain/kotlin')
91-
}
92-
wasmJsMain {
93-
kotlin.srcDir('src/browserMain/kotlin')
94-
}
101+
jsMain { kotlin.srcDir('src/browserMain/kotlin') }
102+
wasmJsMain { kotlin.srcDir('src/browserMain/kotlin') }
95103
}
96104
}
97105

@@ -118,6 +126,7 @@ android {
118126
sourceSets {
119127
androidTest {
120128
java.srcDirs += 'src/commonTest/kotlin'
129+
java.srcDirs += 'src/javaTest/kotlin'
121130
}
122131
}
123132

molecule-runtime/src/commonMain/kotlin/app/cash/molecule/SnapshotNotifier.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package app.cash.molecule
1717

18+
import app.cash.molecule.SnapshotNotifier.WhileActive
19+
1820
/**
1921
* The different snapshot notification modes of Molecule.
2022
*
@@ -25,6 +27,10 @@ package app.cash.molecule
2527
* have one in place, whereas applications that only use Molecule need its automatic registering
2628
* of this notifier.
2729
*
30+
* On the JVM and Android, Molecule will read the `app.cash.molecule.snapshotNotifier` system
31+
* property in order to determine the default mechanism. The value is parsed with [enumValueOf],
32+
* or else defaults to [WhileActive] if not set or the property does not parse to a value.
33+
*
2834
* @see androidx.compose.runtime.snapshots.Snapshot.sendApplyNotifications
2935
*/
3036
public enum class SnapshotNotifier {

molecule-runtime/src/commonMain/kotlin/app/cash/molecule/molecule.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import kotlinx.coroutines.launch
3939
public fun <T> moleculeFlow(mode: RecompositionMode, body: @Composable () -> T): Flow<T> {
4040
return moleculeFlow(
4141
mode = mode,
42-
snapshotNotifier = SnapshotNotifier.WhileActive,
42+
snapshotNotifier = defaultSnapshotNotifier(),
4343
body = body,
4444
)
4545
}
@@ -50,7 +50,7 @@ public fun <T> moleculeFlow(mode: RecompositionMode, body: @Composable () -> T):
5050
*/
5151
public fun <T> moleculeFlow(
5252
mode: RecompositionMode,
53-
snapshotNotifier: SnapshotNotifier = SnapshotNotifier.WhileActive,
53+
snapshotNotifier: SnapshotNotifier = defaultSnapshotNotifier(),
5454
body: @Composable () -> T,
5555
): Flow<T> {
5656
return when (mode) {
@@ -136,7 +136,7 @@ public fun <T> CoroutineScope.launchMolecule(
136136
return launchMolecule(
137137
mode = mode,
138138
context = context,
139-
snapshotNotifier = SnapshotNotifier.WhileActive,
139+
snapshotNotifier = defaultSnapshotNotifier(),
140140
body = body,
141141
)
142142
}
@@ -151,7 +151,7 @@ public fun <T> CoroutineScope.launchMolecule(
151151
public fun <T> CoroutineScope.launchMolecule(
152152
mode: RecompositionMode,
153153
context: CoroutineContext = EmptyCoroutineContext,
154-
snapshotNotifier: SnapshotNotifier = SnapshotNotifier.WhileActive,
154+
snapshotNotifier: SnapshotNotifier = defaultSnapshotNotifier(),
155155
body: @Composable () -> T,
156156
): StateFlow<T> {
157157
var flow: MutableStateFlow<T>? = null
@@ -199,7 +199,7 @@ public fun <T> CoroutineScope.launchMolecule(
199199
mode = mode,
200200
emitter = emitter,
201201
context = context,
202-
snapshotNotifier = SnapshotNotifier.WhileActive,
202+
snapshotNotifier = defaultSnapshotNotifier(),
203203
body = body,
204204
)
205205
}
@@ -218,7 +218,7 @@ public fun <T> CoroutineScope.launchMolecule(
218218
mode: RecompositionMode,
219219
emitter: (value: T) -> Unit,
220220
context: CoroutineContext = EmptyCoroutineContext,
221-
snapshotNotifier: SnapshotNotifier = SnapshotNotifier.WhileActive,
221+
snapshotNotifier: SnapshotNotifier = defaultSnapshotNotifier(),
222222
body: @Composable () -> T,
223223
) {
224224
val clockContext = when (mode) {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright (C) 2025 Square, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package app.cash.molecule
17+
18+
internal expect fun defaultSnapshotNotifier(): SnapshotNotifier

molecule-runtime/src/commonTest/kotlin/app/cash/molecule/MoleculeTest.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,5 +459,9 @@ class MoleculeTest {
459459
job.cancelAndJoin()
460460
}
461461

462+
@Test fun defaultSnapshotNotifierChangeDetector() {
463+
assertThat(defaultSnapshotNotifier()).isEqualTo(WhileActive)
464+
}
465+
462466
private suspend fun <T> Channel<T>.awaitValue(): T = withTimeout(1000) { receive() }
463467
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright (C) 2025 Square, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package app.cash.molecule
17+
18+
internal actual fun defaultSnapshotNotifier(): SnapshotNotifier {
19+
return System.getProperty("app.cash.molecule.snapshotNotifier")
20+
?.let { property ->
21+
runCatching { SnapshotNotifier.valueOf(property) }.getOrNull()
22+
}
23+
?: SnapshotNotifier.WhileActive
24+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright (C) 2025 Square, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package app.cash.molecule
17+
18+
import app.cash.molecule.SnapshotNotifier.External
19+
import app.cash.molecule.SnapshotNotifier.WhileActive
20+
import assertk.assertThat
21+
import assertk.assertions.isEqualTo
22+
import kotlin.test.Test
23+
24+
// Note: We do not share this constant with the production code to verify its value doesn't change.
25+
private const val property = "app.cash.molecule.snapshotNotifier"
26+
27+
class DefaultSnapshotNotifierPropertyTest {
28+
@Test fun propertyEmpty() {
29+
System.setProperty(property, "")
30+
try {
31+
assertThat(defaultSnapshotNotifier()).isEqualTo(WhileActive)
32+
} finally {
33+
System.clearProperty(property)
34+
}
35+
}
36+
37+
@Test fun propertyInvalid() {
38+
System.setProperty(property, "sup")
39+
try {
40+
assertThat(defaultSnapshotNotifier()).isEqualTo(WhileActive)
41+
} finally {
42+
System.clearProperty(property)
43+
}
44+
}
45+
46+
@Test fun propertyValid() {
47+
System.setProperty(property, "External")
48+
try {
49+
assertThat(defaultSnapshotNotifier()).isEqualTo(External)
50+
} finally {
51+
System.clearProperty(property)
52+
}
53+
}
54+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright (C) 2025 Square, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package app.cash.molecule
17+
18+
@Suppress("NOTHING_TO_INLINE")
19+
internal actual inline fun defaultSnapshotNotifier(): SnapshotNotifier {
20+
return SnapshotNotifier.WhileActive
21+
}

0 commit comments

Comments
 (0)