Skip to content
This repository was archived by the owner on Feb 5, 2021. It is now read-only.

Commit 059db5e

Browse files
Merge pull request #16 from square/zachklipp/compose-root
Introduce WorkflowContainer for running a workflow inside a Compose app.
2 parents 039f1c8 + 50bfb92 commit 059db5e

File tree

14 files changed

+881
-1
lines changed

14 files changed

+881
-1
lines changed

buildSrc/src/main/java/Dependencies.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ object Dependencies {
3737
const val foundation = "androidx.ui:ui-foundation:${Versions.compose}"
3838
const val layout = "androidx.ui:ui-layout:${Versions.compose}"
3939
const val material = "androidx.ui:ui-material:${Versions.compose}"
40+
const val savedstate = "androidx.ui:ui-saved-instance-state:${Versions.compose}"
4041
const val test = "androidx.ui:ui-test:${Versions.compose}"
4142
const val tooling = "androidx.ui:ui-tooling:${Versions.compose}"
4243
}

core-compose/api/core-compose.api

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,25 @@ public final class com/squareup/workflow/ui/compose/ViewEnvironmentsKt {
5151
public static synthetic fun showRendering$default (Lcom/squareup/workflow/ui/ViewEnvironment;Ljava/lang/Object;Landroidx/ui/core/Modifier;Landroidx/compose/Composer;ILjava/lang/Object;)V
5252
}
5353

54+
public final class com/squareup/workflow/ui/compose/WorkflowContainerKt {
55+
public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;)V
56+
public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;)V
57+
public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Ljava/lang/Object;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;)V
58+
public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;)V
59+
public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;)V
60+
public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;)V
61+
public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;)V
62+
public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;)V
63+
public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;ILjava/lang/Object;)V
64+
public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;ILjava/lang/Object;)V
65+
public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Ljava/lang/Object;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;ILjava/lang/Object;)V
66+
public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;ILjava/lang/Object;)V
67+
public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;ILjava/lang/Object;)V
68+
public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;ILjava/lang/Object;)V
69+
public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;ILjava/lang/Object;)V
70+
public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;ILjava/lang/Object;)V
71+
}
72+
5473
public final class com/squareup/workflow/ui/core/compose/BuildConfig {
5574
public static final field BUILD_TYPE Ljava/lang/String;
5675
public static final field DEBUG Z

core-compose/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,6 @@ dependencies {
4040

4141
implementation(Dependencies.Compose.foundation)
4242
implementation(Dependencies.Compose.layout)
43-
implementation(Dependencies.Compose.tooling)
43+
implementation(Dependencies.Compose.savedstate)
4444
implementation(Dependencies.Workflow.runtime)
4545
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/*
2+
* Copyright 2020 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+
@file:Suppress("RemoveEmptyParenthesesFromAnnotationEntry")
17+
18+
package com.squareup.workflow.ui.compose
19+
20+
import androidx.compose.FrameManager
21+
import androidx.compose.Providers
22+
import androidx.compose.mutableStateOf
23+
import androidx.compose.onActive
24+
import androidx.test.ext.junit.runners.AndroidJUnit4
25+
import androidx.ui.foundation.Clickable
26+
import androidx.ui.foundation.Text
27+
import androidx.ui.layout.Column
28+
import androidx.ui.savedinstancestate.UiSavedStateRegistry
29+
import androidx.ui.savedinstancestate.UiSavedStateRegistryAmbient
30+
import androidx.ui.test.createComposeRule
31+
import androidx.ui.test.doClick
32+
import androidx.ui.test.findByText
33+
import androidx.ui.test.waitForIdle
34+
import com.google.common.truth.Truth.assertThat
35+
import com.squareup.workflow.RenderContext
36+
import com.squareup.workflow.Snapshot
37+
import com.squareup.workflow.StatefulWorkflow
38+
import com.squareup.workflow.Workflow
39+
import com.squareup.workflow.action
40+
import com.squareup.workflow.parse
41+
import com.squareup.workflow.readUtf8WithLength
42+
import com.squareup.workflow.stateless
43+
import com.squareup.workflow.ui.compose.WorkflowContainerTest.SnapshottingWorkflow.SnapshottedRendering
44+
import com.squareup.workflow.writeUtf8WithLength
45+
import okio.ByteString
46+
import okio.ByteString.Companion.decodeBase64
47+
import org.junit.Rule
48+
import org.junit.Test
49+
import org.junit.runner.RunWith
50+
51+
@RunWith(AndroidJUnit4::class)
52+
class WorkflowContainerTest {
53+
54+
@Rule @JvmField val composeRule = createComposeRule()
55+
56+
@Test fun passesPropsThrough() {
57+
val workflow = Workflow.stateless<String, Nothing, String> { it }
58+
59+
composeRule.setContent {
60+
WorkflowContainer(workflow, "foo") {
61+
assertThat(it).isEqualTo("foo")
62+
}
63+
}
64+
}
65+
66+
@Test fun seesPropsAndRenderingUpdates() {
67+
val workflow = Workflow.stateless<String, Nothing, String> { it }
68+
val props = mutableStateOf("foo")
69+
70+
composeRule.setContent {
71+
WorkflowContainer(workflow, props.value) {
72+
Text(it)
73+
}
74+
}
75+
76+
findByText("foo").assertExists()
77+
FrameManager.framed {
78+
props.value = "bar"
79+
}
80+
findByText("bar").assertExists()
81+
}
82+
83+
@Test fun invokesOutputCallback() {
84+
val workflow = Workflow.stateless<Unit, String, (String) -> Unit> {
85+
{ string -> actionSink.send(action { setOutput(string) }) }
86+
}
87+
88+
val receivedOutputs = mutableListOf<String>()
89+
composeRule.setContent {
90+
WorkflowContainer(workflow, onOutput = { receivedOutputs += it }) { sendOutput ->
91+
Column {
92+
Clickable(onClick = { sendOutput("one") }) {
93+
Text("send one")
94+
}
95+
Clickable(onClick = { sendOutput("two") }) {
96+
Text("send two")
97+
}
98+
}
99+
}
100+
}
101+
102+
waitForIdle()
103+
assertThat(receivedOutputs).isEmpty()
104+
findByText("send one").doClick()
105+
106+
waitForIdle()
107+
assertThat(receivedOutputs).isEqualTo(listOf("one"))
108+
findByText("send two").doClick()
109+
110+
waitForIdle()
111+
assertThat(receivedOutputs).isEqualTo(listOf("one", "two"))
112+
}
113+
114+
@Test fun savesSnapshot() {
115+
val savedStateRegistry = UiSavedStateRegistry(emptyMap()) { true }
116+
117+
composeRule.setContent {
118+
Providers(UiSavedStateRegistryAmbient provides savedStateRegistry) {
119+
WorkflowContainerImpl(
120+
SnapshottingWorkflow,
121+
props = Unit,
122+
onOutput = {},
123+
snapshotKey = SNAPSHOT_KEY
124+
) { (string, updateString) ->
125+
onActive {
126+
assertThat(string).isEmpty()
127+
updateString("foo")
128+
}
129+
}
130+
}
131+
}
132+
133+
waitForIdle()
134+
val savedValues = FrameManager.framed {
135+
savedStateRegistry.performSave()
136+
}
137+
println("saved keys: ${savedValues.keys}")
138+
// Relying on the int key across all runtimes might be flaky, might need to pass explicit key.
139+
val snapshot = ByteString.of(*(savedValues.getValue(SNAPSHOT_KEY) as ByteArray))
140+
println("snapshot: ${snapshot.base64()}")
141+
assertThat(snapshot).isEqualTo(EXPECTED_SNAPSHOT)
142+
}
143+
144+
@Test fun restoresSnapshot() {
145+
val restoreValues = mapOf(SNAPSHOT_KEY to EXPECTED_SNAPSHOT.toByteArray())
146+
val savedStateRegistry = UiSavedStateRegistry(restoreValues) { true }
147+
148+
composeRule.setContent {
149+
Providers(UiSavedStateRegistryAmbient provides savedStateRegistry) {
150+
WorkflowContainerImpl(
151+
SnapshottingWorkflow,
152+
props = Unit,
153+
onOutput = {},
154+
snapshotKey = "workflow-snapshot"
155+
) { (string) ->
156+
onActive {
157+
assertThat(string).isEqualTo("foo")
158+
}
159+
Text(string)
160+
}
161+
}
162+
}
163+
164+
findByText("foo").assertExists()
165+
}
166+
167+
private companion object {
168+
const val SNAPSHOT_KEY = "workflow-snapshot"
169+
val EXPECTED_SNAPSHOT = "AAAABwAAAANmb28AAAAA".decodeBase64()!!
170+
}
171+
172+
// Seems to be a problem accessing Workflow.stateful.
173+
private object SnapshottingWorkflow :
174+
StatefulWorkflow<Unit, String, Nothing, SnapshottedRendering>() {
175+
176+
data class SnapshottedRendering(
177+
val string: String,
178+
val updateString: (String) -> Unit
179+
)
180+
181+
override fun initialState(
182+
props: Unit,
183+
snapshot: Snapshot?
184+
): String = snapshot?.bytes?.parse { it.readUtf8WithLength() } ?: ""
185+
186+
override fun render(
187+
props: Unit,
188+
state: String,
189+
context: RenderContext<String, Nothing>
190+
) = SnapshottedRendering(
191+
string = state,
192+
updateString = { newString -> context.actionSink.send(updateString(newString)) }
193+
)
194+
195+
override fun snapshotState(state: String): Snapshot =
196+
Snapshot.write { it.writeUtf8WithLength(state) }
197+
198+
private fun updateString(newString: String) = action {
199+
nextState = newString
200+
}
201+
}
202+
}

0 commit comments

Comments
 (0)