Skip to content

Commit 2b3d497

Browse files
Rendering Workflows from ComposeWorkflow.
1 parent e5f40c9 commit 2b3d497

File tree

13 files changed

+710
-214
lines changed

13 files changed

+710
-214
lines changed

samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/NestedRenderingsActivity.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import androidx.compose.ui.graphics.Color
1010
import androidx.lifecycle.SavedStateHandle
1111
import androidx.lifecycle.ViewModel
1212
import androidx.lifecycle.viewModelScope
13+
import com.squareup.workflow1.SimpleLoggingWorkflowInterceptor
1314
import com.squareup.workflow1.WorkflowExperimentalRuntime
1415
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
1516
import com.squareup.workflow1.mapRendering
@@ -47,7 +48,8 @@ class NestedRenderingsActivity : AppCompatActivity() {
4748
workflow = RecursiveWorkflow.mapRendering { it.withEnvironment(viewEnvironment) },
4849
scope = viewModelScope,
4950
savedStateHandle = savedState,
50-
runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig()
51+
runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig(),
52+
interceptors = listOf(SimpleLoggingWorkflowInterceptor())
5153
)
5254
}
5355
}
Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package com.squareup.sample.compose.nestedrenderings
22

3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.getValue
5+
import androidx.compose.runtime.mutableStateOf
6+
import androidx.compose.runtime.saveable.rememberSaveable
7+
import androidx.compose.runtime.setValue
38
import com.squareup.sample.compose.databinding.LegacyViewBinding
49
import com.squareup.sample.compose.nestedrenderings.RecursiveWorkflow.LegacyRendering
510
import com.squareup.sample.compose.nestedrenderings.RecursiveWorkflow.Rendering
6-
import com.squareup.sample.compose.nestedrenderings.RecursiveWorkflow.State
7-
import com.squareup.workflow1.Snapshot
8-
import com.squareup.workflow1.StatefulWorkflow
9-
import com.squareup.workflow1.action
10-
import com.squareup.workflow1.renderChild
11+
import com.squareup.workflow1.WorkflowExperimentalApi
12+
import com.squareup.workflow1.compose.ComposeWorkflow
13+
import com.squareup.workflow1.compose.renderChild
1114
import com.squareup.workflow1.ui.AndroidScreen
1215
import com.squareup.workflow1.ui.Screen
1316
import com.squareup.workflow1.ui.ScreenViewFactory
@@ -20,9 +23,8 @@ import com.squareup.workflow1.ui.ScreenViewFactory
2023
* to force it to go through the legacy view layer. This way this sample both demonstrates pass-
2124
* through Composable renderings as well as adapting in both directions.
2225
*/
23-
object RecursiveWorkflow : StatefulWorkflow<Unit, State, Nothing, Screen>() {
24-
25-
data class State(val children: Int = 0)
26+
@OptIn(WorkflowExperimentalApi::class)
27+
object RecursiveWorkflow : ComposeWorkflow<Unit, Nothing, Screen>() {
2628

2729
/**
2830
* A rendering from a [RecursiveWorkflow].
@@ -49,33 +51,18 @@ object RecursiveWorkflow : StatefulWorkflow<Unit, State, Nothing, Screen>() {
4951
)
5052
}
5153

52-
override fun initialState(
54+
@Composable override fun produceRendering(
5355
props: Unit,
54-
snapshot: Snapshot?
55-
): State = State()
56-
57-
override fun render(
58-
renderProps: Unit,
59-
renderState: State,
60-
context: RenderContext<Unit, State, Nothing>
61-
): Rendering {
56+
emitOutput: (Nothing) -> Unit
57+
): Screen {
58+
var children by rememberSaveable { mutableStateOf(0) }
6259
return Rendering(
63-
children = List(renderState.children) { i ->
64-
val child = context.renderChild(RecursiveWorkflow, key = i.toString())
60+
children = List(children) { i ->
61+
val child = renderChild(RecursiveWorkflow)
6562
if (i % 2 == 0) child else LegacyRendering(child)
6663
},
67-
onAddChildClicked = { context.actionSink.send(addChild()) },
68-
onResetClicked = { context.actionSink.send(reset()) }
64+
onAddChildClicked = { children++ },
65+
onResetClicked = { children = 0 }
6966
)
7067
}
71-
72-
override fun snapshotState(state: State): Snapshot? = null
73-
74-
private fun addChild() = action("addChild") {
75-
state = state.copy(children = state.children + 1)
76-
}
77-
78-
private fun reset() = action("reset") {
79-
state = State()
80-
}
8168
}

workflow-core/src/commonMain/kotlin/com/squareup/workflow1/compose/ComposeWorkflow.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,38 @@ public abstract class ComposeWorkflow<PropsT, OutputT, out RenderingT> :
3838
)
3939
}
4040
}
41+
42+
@WorkflowExperimentalApi
43+
@WorkflowComposable
44+
@Composable
45+
public fun <PropsT, OutputT, RenderingT> renderChild(
46+
workflow: Workflow<PropsT, OutputT, RenderingT>,
47+
props: PropsT,
48+
onOutput: ((OutputT) -> Unit)?
49+
): RenderingT {
50+
val renderer = LocalWorkflowComposableRenderer.current
51+
return renderer.renderChild(workflow, props, onOutput)
52+
}
53+
54+
@WorkflowExperimentalApi
55+
@WorkflowComposable
56+
@Composable
57+
inline fun <OutputT, RenderingT> renderChild(
58+
workflow: Workflow<Unit, OutputT, RenderingT>,
59+
noinline onOutput: ((OutputT) -> Unit)?
60+
): RenderingT = renderChild(workflow, props = Unit, onOutput)
61+
62+
@WorkflowExperimentalApi
63+
@WorkflowComposable
64+
@Composable
65+
inline fun <PropsT, RenderingT> renderChild(
66+
workflow: Workflow<PropsT, Nothing, RenderingT>,
67+
props: PropsT,
68+
): RenderingT = renderChild(workflow, props, onOutput = null)
69+
70+
@WorkflowExperimentalApi
71+
@WorkflowComposable
72+
@Composable
73+
inline fun <RenderingT> renderChild(
74+
workflow: Workflow<Unit, Nothing, RenderingT>,
75+
): RenderingT = renderChild(workflow, props = Unit, onOutput = null)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.squareup.workflow1.compose
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.staticCompositionLocalOf
5+
import com.squareup.workflow1.Workflow
6+
import com.squareup.workflow1.WorkflowExperimentalApi
7+
8+
// TODO mark these with a separate InternalWorkflow annotation
9+
10+
@WorkflowExperimentalApi
11+
public val LocalWorkflowComposableRenderer =
12+
staticCompositionLocalOf<WorkflowComposableRenderer> { error("No renderer") }
13+
14+
@WorkflowExperimentalApi
15+
public interface WorkflowComposableRenderer {
16+
17+
@Composable
18+
fun <PropsT, OutputT, RenderingT> renderChild(
19+
childWorkflow: Workflow<PropsT, OutputT, RenderingT>,
20+
props: PropsT,
21+
onOutput: ((OutputT) -> Unit)?
22+
): RenderingT
23+
}

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

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package com.squareup.workflow1
33
import androidx.compose.runtime.Composable
44
import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor
55
import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
6+
import com.squareup.workflow1.compose.LocalWorkflowComposableRenderer
7+
import com.squareup.workflow1.compose.WorkflowComposableRenderer
8+
import com.squareup.workflow1.internal.compose.withCompositionLocals
69
import kotlinx.coroutines.CoroutineScope
710
import kotlinx.coroutines.Job
811

@@ -57,8 +60,39 @@ public open class SimpleLoggingWorkflowInterceptor : WorkflowInterceptor {
5760
emitOutput: (O) -> Unit,
5861
proceed: @Composable (P, (O) -> Unit) -> R,
5962
session: WorkflowSession
60-
): R = logMethod("onRenderComposeWorkflow", session) {
61-
proceed(renderProps, emitOutput)
63+
): R = logMethod("onRenderComposeWorkflow", session, "renderProps" to renderProps) {
64+
val childRenderer = LocalWorkflowComposableRenderer.current
65+
val loggingRenderer = androidx.compose.runtime.remember(childRenderer) {
66+
SimpleLoggingWorkflowComposableRenderer(session, childRenderer)
67+
}
68+
withCompositionLocals(LocalWorkflowComposableRenderer provides loggingRenderer) {
69+
proceed(renderProps, /* emitOutput= */{ output ->
70+
logMethod("onEmitOutput", session, "output" to output) {
71+
emitOutput(output)
72+
}
73+
})
74+
}
75+
}
76+
77+
@OptIn(WorkflowExperimentalApi::class)
78+
private inner class SimpleLoggingWorkflowComposableRenderer(
79+
val session: WorkflowSession,
80+
val childRenderer: WorkflowComposableRenderer
81+
) : WorkflowComposableRenderer {
82+
@Composable
83+
override fun <PropsT, OutputT, RenderingT> renderChild(
84+
childWorkflow: Workflow<PropsT, OutputT, RenderingT>,
85+
props: PropsT,
86+
onOutput: ((OutputT) -> Unit)?
87+
): RenderingT {
88+
logMethod("onRenderChild", session, "workflow" to childWorkflow, "props" to props) {
89+
return childRenderer.renderChild(childWorkflow, props, onOutput = { output ->
90+
logMethod("onOutput", session, "output" to output) {
91+
onOutput?.invoke(output)
92+
}
93+
})
94+
}
95+
}
6296
}
6397

6498
override fun <S> onSnapshotState(

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,3 +482,14 @@ private class InterceptedRenderContext<P, S, O>(
482482
return coroutineContext
483483
}
484484
}
485+
486+
internal fun WorkflowSession.workflowSessionToString(): String {
487+
val parentDescription = parent?.let { "WorkflowInstance(…)" }
488+
return "WorkflowInstance(" +
489+
"identifier=$identifier, " +
490+
"renderKey=$renderKey, " +
491+
"instanceId=$sessionId, " +
492+
"parent=$parentDescription" +
493+
")"
494+
}
495+

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

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import com.squareup.workflow1.WorkflowInterceptor
1313
import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
1414
import com.squareup.workflow1.WorkflowTracer
1515
import com.squareup.workflow1.compose.ComposeWorkflow
16-
import com.squareup.workflow1.internal.compose.ComposeWorkflowNode
16+
import com.squareup.workflow1.internal.compose.ComposeWorkflowNodeAdapter
17+
import com.squareup.workflow1.workflowSessionToString
1718
import kotlinx.coroutines.CancellationException
1819
import kotlinx.coroutines.CoroutineName
1920
import kotlinx.coroutines.CoroutineScope
@@ -37,7 +38,7 @@ internal fun <PropsT, OutputT, RenderingT> createWorkflowNode(
3738
interceptor: WorkflowInterceptor = NoopWorkflowInterceptor,
3839
idCounter: IdCounter? = null
3940
): AbstractWorkflowNode<PropsT, OutputT, RenderingT> = when (workflow) {
40-
is ComposeWorkflow<*, *, *> -> ComposeWorkflowNode(
41+
is ComposeWorkflow<*, *, *> -> ComposeWorkflowNodeAdapter(
4142
id = id,
4243
workflow = workflow as ComposeWorkflow,
4344
initialProps = initialProps,
@@ -91,15 +92,7 @@ internal abstract class AbstractWorkflowNode<PropsT, OutputT, RenderingT>(
9192
final override val renderKey: String get() = id.name
9293
final override val sessionId: Long = idCounter.createId()
9394

94-
final override fun toString(): String {
95-
val parentDescription = parent?.let { "WorkflowInstance(…)" }
96-
return "WorkflowInstance(" +
97-
"identifier=$identifier, " +
98-
"renderKey=$renderKey, " +
99-
"instanceId=$sessionId, " +
100-
"parent=$parentDescription" +
101-
")"
102-
}
95+
final override fun toString(): String = workflowSessionToString()
10396

10497
/**
10598
* Walk the tree of workflows, rendering each one and using
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.squareup.workflow1.internal.compose
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.currentCompositeKeyHash
5+
import com.squareup.workflow1.ActionProcessingResult
6+
import com.squareup.workflow1.TreeSnapshot
7+
import com.squareup.workflow1.Workflow
8+
import com.squareup.workflow1.internal.WorkflowNodeId
9+
import kotlinx.coroutines.selects.SelectBuilder
10+
11+
internal interface ComposeChildNode<PropsT, OutputT, RenderingT> {
12+
13+
val id: WorkflowNodeId
14+
15+
@Composable fun produceRendering(
16+
workflow: Workflow<PropsT, OutputT, RenderingT>,
17+
props: PropsT
18+
): RenderingT
19+
20+
fun snapshot(): TreeSnapshot
21+
22+
fun onNextAction(selector: SelectBuilder<ActionProcessingResult>): Boolean
23+
}
24+
25+
@OptIn(ExperimentalStdlibApi::class)
26+
@Composable
27+
internal fun rememberChildRenderKey(): String {
28+
return currentCompositeKeyHash.toHexString(HexFormat.Default)
29+
}

0 commit comments

Comments
 (0)