Skip to content

Commit 073d468

Browse files
Merge pull request #1248 from square/sedwards/partial-tree
1247: Partial Tree Rerendering
2 parents 56e6ee0 + 7d3a6fe commit 073d468

File tree

15 files changed

+399
-28
lines changed

15 files changed

+399
-28
lines changed

.github/workflows/kotlin.yml

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,27 @@ jobs :
222222
with :
223223
report_paths : '**/build/test-results/test/TEST-*.xml'
224224

225+
jvm-partial-runtime-test:
226+
name: Partial Tree Rendering Only Runtime JVM Tests
227+
runs-on: ubuntu-latest
228+
timeout-minutes: 20
229+
steps:
230+
- name: Checkout
231+
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
232+
233+
- name: Check with Gradle
234+
uses: ./.github/actions/gradle-task
235+
with:
236+
task: jvmTest --continue -Pworkflow.runtime=baseline-partial
237+
restore-cache-key: main-build-artifacts
238+
239+
# Report as GitHub Pull Request Check.
240+
- name: Publish Test Report
241+
uses: mikepenz/action-junit-report@5f47764eec0e1c1f19f40c8e60a5ba47e47015c5 # v4
242+
if: always() # always run even if the previous step fails
243+
with:
244+
report_paths: '**/build/test-results/test/TEST-*.xml'
245+
225246
jvm-conflate-stateChange-runtime-test :
226247
name : Render On State Change Only and Conflate Stale Runtime JVM Tests
227248
runs-on : ubuntu-latest
@@ -243,6 +264,27 @@ jobs :
243264
with :
244265
report_paths : '**/build/test-results/test/TEST-*.xml'
245266

267+
jvm-conflate-partial-runtime-test:
268+
name: Render On State Change Only and Conflate Stale Runtime JVM Tests
269+
runs-on: ubuntu-latest
270+
timeout-minutes: 20
271+
steps:
272+
- name: Checkout
273+
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
274+
275+
- name: Check with Gradle
276+
uses: ./.github/actions/gradle-task
277+
with:
278+
task: jvmTest --continue -Pworkflow.runtime=conflate-partial
279+
restore-cache-key: main-build-artifacts
280+
281+
# Report as GitHub Pull Request Check.
282+
- name: Publish Test Report
283+
uses: mikepenz/action-junit-report@5f47764eec0e1c1f19f40c8e60a5ba47e47015c5 # v4
284+
if: always() # always run even if the previous step fails
285+
with:
286+
report_paths: '**/build/test-results/test/TEST-*.xml'
287+
246288
ios-tests :
247289
name : iOS Tests
248290
runs-on : macos-latest
@@ -349,7 +391,7 @@ jobs :
349391
### <start-connected-check-shards>
350392
shardNum: [ 1, 2, 3 ]
351393
### <end-connected-check-shards>
352-
runtime : [ conflate, baseline-stateChange, conflate-stateChange ]
394+
runtime : [ conflate, baseline-stateChange, conflate-stateChange, baseline-partial, conflate-partial ]
353395
steps :
354396
- name: Checkout
355397
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
@@ -378,8 +420,10 @@ jobs :
378420
- ios-tests
379421
- js-tests
380422
- jvm-conflate-runtime-test
381-
- jvm-conflate-stateChange-runtime-test
382423
- jvm-stateChange-runtime-test
424+
- jvm-partial-runtime-test
425+
- jvm-conflate-stateChange-runtime-test
426+
- jvm-conflate-partial-runtime-test
383427
- ktlint
384428
- performance-tests
385429
- runtime-instrumentation-tests

workflow-config/config-android/src/main/java/com/squareup/workflow1/config/AndroidRuntimeConfigTools.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.squareup.workflow1.config
33
import com.squareup.workflow1.RuntimeConfig
44
import com.squareup.workflow1.RuntimeConfigOptions
55
import com.squareup.workflow1.RuntimeConfigOptions.CONFLATE_STALE_RENDERINGS
6+
import com.squareup.workflow1.RuntimeConfigOptions.PARTIAL_TREE_RENDERING
67
import com.squareup.workflow1.RuntimeConfigOptions.RENDER_ONLY_WHEN_STATE_CHANGES
78
import com.squareup.workflow1.WorkflowExperimentalRuntime
89

@@ -27,6 +28,9 @@ public class AndroidRuntimeConfigTools {
2728
* Then, these can be combined (via '-') with:
2829
* "stateChange" : Only re-render when the state of some WorkflowNode has been changed by an
2930
* action cascade.
31+
* "partial" : Which includes "stateChange" as well as partial tree rendering, which only
32+
* re-renders each Workflow node if: 1) its state changed; or 2) one of its descendant's state
33+
* changed.
3034
*
3135
* E.g., "baseline-stateChange" to turn on the stateChange option with the baseline runtime.
3236
*
@@ -37,6 +41,12 @@ public class AndroidRuntimeConfigTools {
3741
"conflate" -> setOf(CONFLATE_STALE_RENDERINGS)
3842
"conflate-stateChange" -> setOf(CONFLATE_STALE_RENDERINGS, RENDER_ONLY_WHEN_STATE_CHANGES)
3943
"baseline-stateChange" -> setOf(RENDER_ONLY_WHEN_STATE_CHANGES)
44+
"conflate-partial" -> setOf(
45+
CONFLATE_STALE_RENDERINGS,
46+
RENDER_ONLY_WHEN_STATE_CHANGES,
47+
PARTIAL_TREE_RENDERING
48+
)
49+
"baseline-partial" -> setOf(RENDER_ONLY_WHEN_STATE_CHANGES, PARTIAL_TREE_RENDERING)
4050
"", "baseline" -> RuntimeConfigOptions.RENDER_PER_ACTION
4151
else ->
4252
throw IllegalArgumentException("Unrecognized config \"${BuildConfig.WORKFLOW_RUNTIME}\"")

workflow-config/config-jvm/src/main/java/com/squareup/workflow1/config/JvmTestRuntimeConfigTools.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.squareup.workflow1.config
33
import com.squareup.workflow1.RuntimeConfig
44
import com.squareup.workflow1.RuntimeConfigOptions
55
import com.squareup.workflow1.RuntimeConfigOptions.CONFLATE_STALE_RENDERINGS
6+
import com.squareup.workflow1.RuntimeConfigOptions.PARTIAL_TREE_RENDERING
67
import com.squareup.workflow1.RuntimeConfigOptions.RENDER_ONLY_WHEN_STATE_CHANGES
78
import com.squareup.workflow1.WorkflowExperimentalRuntime
89

@@ -27,6 +28,9 @@ public class JvmTestRuntimeConfigTools {
2728
* Then, these can be combined (via '-') with:
2829
* "stateChange" : Only re-render when the state of some WorkflowNode has been changed by an
2930
* action cascade.
31+
* "partial" : Which includes "stateChange" as well as partial tree rendering, which only
32+
* re-renders each Workflow node if: 1) its state changed; or 2) one of its descendant's state
33+
* changed.
3034
*
3135
* E.g., "baseline-stateChange" to turn on the stateChange option with the baseline runtime.
3236
*
@@ -38,6 +42,12 @@ public class JvmTestRuntimeConfigTools {
3842
"conflate" -> setOf(CONFLATE_STALE_RENDERINGS)
3943
"conflate-stateChange" -> setOf(CONFLATE_STALE_RENDERINGS, RENDER_ONLY_WHEN_STATE_CHANGES)
4044
"baseline-stateChange" -> setOf(RENDER_ONLY_WHEN_STATE_CHANGES)
45+
"conflate-partial" -> setOf(
46+
CONFLATE_STALE_RENDERINGS,
47+
RENDER_ONLY_WHEN_STATE_CHANGES,
48+
PARTIAL_TREE_RENDERING
49+
)
50+
"baseline-partial" -> setOf(RENDER_ONLY_WHEN_STATE_CHANGES, PARTIAL_TREE_RENDERING)
4151
"", "baseline" -> RuntimeConfigOptions.RENDER_PER_ACTION
4252
else ->
4353
throw IllegalArgumentException("Unrecognized config \"$runtimeConfig\"")

workflow-core/api/workflow-core.api

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,26 @@ public abstract class com/squareup/workflow1/LifecycleWorker : com/squareup/work
7777
public final fun run ()Lkotlinx/coroutines/flow/Flow;
7878
}
7979

80+
public final class com/squareup/workflow1/NullableInitBox {
81+
public static final synthetic fun box-impl (Ljava/lang/Object;)Lcom/squareup/workflow1/NullableInitBox;
82+
public static fun constructor-impl (Ljava/lang/Object;)Ljava/lang/Object;
83+
public static synthetic fun constructor-impl$default (Ljava/lang/Object;ILkotlin/jvm/internal/DefaultConstructorMarker;)Ljava/lang/Object;
84+
public fun equals (Ljava/lang/Object;)Z
85+
public static fun equals-impl (Ljava/lang/Object;Ljava/lang/Object;)Z
86+
public static final fun equals-impl0 (Ljava/lang/Object;Ljava/lang/Object;)Z
87+
public static final fun getOrThrow-impl (Ljava/lang/Object;)Ljava/lang/Object;
88+
public fun hashCode ()I
89+
public static fun hashCode-impl (Ljava/lang/Object;)I
90+
public static final fun isInitialized-impl (Ljava/lang/Object;)Z
91+
public fun toString ()Ljava/lang/String;
92+
public static fun toString-impl (Ljava/lang/Object;)Ljava/lang/String;
93+
public final synthetic fun unbox-impl ()Ljava/lang/Object;
94+
}
95+
96+
public final class com/squareup/workflow1/NullableInitBox$Uninitialized {
97+
public static final field INSTANCE Lcom/squareup/workflow1/NullableInitBox$Uninitialized;
98+
}
99+
80100
public final class com/squareup/workflow1/PropsUpdated : com/squareup/workflow1/ActionProcessingResult {
81101
public static final field INSTANCE Lcom/squareup/workflow1/PropsUpdated;
82102
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.squareup.workflow1
2+
3+
import kotlin.jvm.JvmInline
4+
5+
/**
6+
* Used to wrap immutable nullable values whose holder may not yet be initialized.
7+
* Check [isInitialized] to see if the value has been assigned.
8+
*/
9+
@JvmInline
10+
public value class NullableInitBox<T>(private val _value: Any? = Uninitialized) {
11+
/**
12+
* Whether or not a value has been set for this [NullableInitBox]
13+
*/
14+
public val isInitialized: Boolean get() = _value !== Uninitialized
15+
16+
/**
17+
* Get the value this has been initialized with.
18+
*
19+
* @throws [IllegalStateException] if the value in the box has not been initialized.
20+
*/
21+
@Suppress("UNCHECKED_CAST")
22+
public fun getOrThrow(): T {
23+
check(isInitialized) { "NullableInitBox was fetched before it was initialized with a value." }
24+
return _value as T
25+
}
26+
27+
public object Uninitialized
28+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.squareup.workflow1
2+
3+
import kotlin.test.Test
4+
import kotlin.test.assertEquals
5+
import kotlin.test.assertFailsWith
6+
import kotlin.test.assertFalse
7+
import kotlin.test.assertTrue
8+
9+
class NullableInitBoxTest {
10+
11+
@Test fun reports_not_initialized() {
12+
val box = NullableInitBox<String>()
13+
14+
assertFalse(box.isInitialized)
15+
}
16+
17+
@Test fun reports_initialized() {
18+
val box = NullableInitBox<String>("Hello")
19+
20+
assertTrue(box.isInitialized)
21+
}
22+
23+
@Test fun returns_value() {
24+
val box = NullableInitBox<String>("Hello")
25+
26+
assertEquals("Hello", box.getOrThrow())
27+
}
28+
29+
@Test fun throws_exceptions() {
30+
val box = NullableInitBox<String>()
31+
32+
val exception = assertFailsWith<IllegalStateException> {
33+
box.getOrThrow()
34+
}
35+
36+
assertEquals(
37+
"NullableInitBox was fetched before it was initialized with a value.",
38+
exception.message
39+
)
40+
}
41+
}

workflow-core/src/commonTest/kotlin/com/squareup/workflow1/WorkerTest.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import kotlin.test.assertNotSame
1616
import kotlin.test.assertTrue
1717

1818
@ExperimentalCoroutinesApi
19-
@OptIn(ExperimentalStdlibApi::class)
2019
class WorkerTest {
2120

2221
@Test fun timer_returns_equivalent_workers_keyed() {

workflow-core/src/commonTest/kotlin/com/squareup/workflow1/WorkflowIdentifierTest.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import kotlin.test.assertFailsWith
1212
import kotlin.test.assertNotEquals
1313
import kotlin.test.assertNull
1414

15-
@OptIn(ExperimentalStdlibApi::class)
1615
internal class WorkflowIdentifierTest {
1716

1817
@Test fun restored_identifier_toString() {

workflow-runtime/api/workflow-runtime.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public final class com/squareup/workflow1/RenderingAndSnapshot {
2525
public final class com/squareup/workflow1/RuntimeConfigOptions : java/lang/Enum {
2626
public static final field CONFLATE_STALE_RENDERINGS Lcom/squareup/workflow1/RuntimeConfigOptions;
2727
public static final field Companion Lcom/squareup/workflow1/RuntimeConfigOptions$Companion;
28+
public static final field PARTIAL_TREE_RENDERING Lcom/squareup/workflow1/RuntimeConfigOptions;
2829
public static final field RENDER_ONLY_WHEN_STATE_CHANGES Lcom/squareup/workflow1/RuntimeConfigOptions;
2930
public static fun getEntries ()Lkotlin/enums/EnumEntries;
3031
public static fun valueOf (Ljava/lang/String;)Lcom/squareup/workflow1/RuntimeConfigOptions;

workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RuntimeConfig.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,22 @@ public enum class RuntimeConfigOptions {
3838
@WorkflowExperimentalRuntime
3939
RENDER_ONLY_WHEN_STATE_CHANGES,
4040

41+
/**
42+
* Only re-render each active Workflow node if:
43+
* 1. Its own state changed, OR
44+
* 2. One of its descendant's state has changed.
45+
*
46+
* Otherwise return the cached rendering (as there is no way it could have changed).
47+
*
48+
* Note however that you must be careful using this because there may be external
49+
* state that your Workflow draws in and re-renders, and if that is not explicitly
50+
* tracked within that Workflow's state then the Workflow will not re-render.
51+
* In this case make sure that the implicit state is tracked within the Workflow's
52+
* `StateT` in some way, even if only via a hash token.
53+
*/
54+
@WorkflowExperimentalRuntime
55+
PARTIAL_TREE_RENDERING,
56+
4157
/**
4258
* If we have more actions to process, do so before passing the rendering to the UI layer.
4359
*/

0 commit comments

Comments
 (0)