Skip to content

Commit c007b73

Browse files
1 parent 56e6ee0 commit c007b73

File tree

7 files changed

+234
-0
lines changed

7 files changed

+234
-0
lines changed

settings.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pluginManagement {
77
google()
88
// For binary compatibility validator.
99
maven { url = uri("https://kotlin.bintray.com/kotlinx") }
10+
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
1011
}
1112
includeBuild("build-logic")
1213
}

workflow-core/build.gradle.kts

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import com.squareup.workflow1.buildsrc.iosWithSimulatorArm64
33
plugins {
44
id("kotlin-multiplatform")
55
id("published")
6+
// id("org.jetbrains.compose") version "1.7.3"
67
}
78

89
kotlin {
@@ -23,6 +24,7 @@ dependencies {
2324
commonMainApi(libs.kotlinx.coroutines.core)
2425
// For Snapshot.
2526
commonMainApi(libs.squareup.okio)
27+
commonMainApi("org.jetbrains.compose.runtime:runtime:1.7.3")
2628

2729
commonTestImplementation(libs.kotlinx.atomicfu)
2830
commonTestImplementation(libs.kotlinx.coroutines.test.common)

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

+46
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,17 @@
99

1010
package com.squareup.workflow1
1111

12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.runtime.Composition
14+
import androidx.compose.runtime.CompositionLocalProvider
15+
import androidx.compose.runtime.MonotonicFrameClock
16+
import androidx.compose.runtime.Recomposer
17+
import androidx.compose.runtime.mutableStateOf
1218
import com.squareup.workflow1.WorkflowAction.Companion.noAction
1319
import kotlinx.coroutines.CoroutineScope
20+
import kotlinx.coroutines.CoroutineStart
21+
import kotlinx.coroutines.launch
22+
import kotlin.coroutines.EmptyCoroutineContext
1423
import kotlin.jvm.JvmMultifileClass
1524
import kotlin.jvm.JvmName
1625
import kotlin.reflect.KType
@@ -85,6 +94,43 @@ public interface BaseRenderContext<out PropsT, StateT, in OutputT> {
8594
handler: (ChildOutputT) -> WorkflowAction<PropsT, StateT, OutputT>
8695
): ChildRenderingT
8796

97+
/**
98+
* Synchronously composes a [content] function and returns its rendering. Whenever [content] is
99+
* invalidated, this workflow will be re-rendered and the [content] recomposed to return its new
100+
* value.
101+
*
102+
* @see ComposeWorkflow
103+
*/
104+
public fun <ChildRenderingT> renderComposable(
105+
key: String = "",
106+
content: @WorkflowComposable @Composable () -> ChildRenderingT
107+
): ChildRenderingT {
108+
val renderer: WorkflowComposableRenderer = TODO()
109+
val frameClock: MonotonicFrameClock = TODO()
110+
val coroutineContext = EmptyCoroutineContext + frameClock
111+
val recomposer = Recomposer(coroutineContext)
112+
val composition = Composition(UnitApplier, recomposer)
113+
114+
// TODO I think we need more than a simple UNDISPATCHED start to make this work – we have to
115+
// pump the dispatcher until the composition is finished.
116+
CoroutineScope(coroutineContext).launch(start = CoroutineStart.UNDISPATCHED) {
117+
try {
118+
recomposer.runRecomposeAndApplyChanges()
119+
} finally {
120+
composition.dispose()
121+
}
122+
}
123+
124+
val rendering = mutableStateOf<ChildRenderingT?>(null)
125+
composition.setContent {
126+
CompositionLocalProvider(LocalWorkflowComposableRenderer provides renderer) {
127+
rendering.value = content()
128+
}
129+
}
130+
@Suppress("UNCHECKED_CAST")
131+
return rendering.value as ChildRenderingT
132+
}
133+
88134
/**
89135
* Ensures [sideEffect] is running with the given [key].
90136
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.squareup.workflow1
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.Stable
5+
import androidx.compose.runtime.remember
6+
7+
/**
8+
* A [Workflow]-like interface that participates in a workflow tree via its [Rendering] composable.
9+
*/
10+
@Stable
11+
public interface ComposeWorkflow<
12+
in PropsT,
13+
out OutputT,
14+
out RenderingT
15+
> {
16+
17+
/**
18+
* The main composable of this workflow that consumes some [props] from its parent and may emit
19+
* an output via [emitOutput].
20+
*
21+
* Equivalent to [StatefulWorkflow.render].
22+
*/
23+
@WorkflowComposable
24+
@Composable
25+
fun Rendering(
26+
props: PropsT,
27+
emitOutput: (OutputT) -> Unit
28+
): RenderingT
29+
}
30+
31+
fun <
32+
PropsT, StateT, OutputT,
33+
ChildPropsT, ChildOutputT, ChildRenderingT
34+
> BaseRenderContext<PropsT, StateT, OutputT>.renderChild(
35+
child: ComposeWorkflow<ChildPropsT, ChildOutputT, ChildRenderingT>,
36+
props: ChildPropsT,
37+
key: String = "",
38+
handler: (ChildOutputT) -> WorkflowAction<PropsT, StateT, OutputT>
39+
): ChildRenderingT = renderComposable(key = key) {
40+
// Explicitly remember the output function since we know that actionSink is stable even though
41+
// Compose might not know that.
42+
val emitOutput: (ChildOutputT) -> Unit = remember(actionSink) {
43+
{ output ->
44+
val action = handler(output)
45+
actionSink.send(action)
46+
}
47+
}
48+
child.Rendering(
49+
props = props,
50+
emitOutput = emitOutput
51+
)
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.squareup.workflow1
2+
3+
import androidx.compose.runtime.Applier
4+
5+
internal object UnitApplier : Applier<Unit> {
6+
override val current: Unit
7+
get() = Unit
8+
9+
override fun clear() {
10+
}
11+
12+
override fun down(node: Unit) {
13+
}
14+
15+
override fun insertBottomUp(
16+
index: Int,
17+
instance: Unit
18+
) {
19+
}
20+
21+
override fun insertTopDown(
22+
index: Int,
23+
instance: Unit
24+
) {
25+
}
26+
27+
override fun move(
28+
from: Int,
29+
to: Int,
30+
count: Int
31+
) {
32+
}
33+
34+
override fun remove(
35+
index: Int,
36+
count: Int
37+
) {
38+
}
39+
40+
override fun up() {
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.squareup.workflow1
2+
3+
import androidx.compose.runtime.ComposableTargetMarker
4+
import kotlin.annotation.AnnotationRetention.BINARY
5+
import kotlin.annotation.AnnotationTarget.FILE
6+
import kotlin.annotation.AnnotationTarget.FUNCTION
7+
import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER
8+
import kotlin.annotation.AnnotationTarget.TYPE
9+
import kotlin.annotation.AnnotationTarget.TYPE_PARAMETER
10+
11+
/**
12+
* An annotation that can be used to mark a composable function as being expected to be use in a
13+
* composable function that is also marked or inferred to be marked as a [WorkflowComposable], i.e.
14+
* that can be called from [BaseRenderContext.renderComposable].
15+
*
16+
* Using this annotation explicitly is rarely necessary as the Compose compiler plugin will infer
17+
* the necessary equivalent annotations automatically. See
18+
* [androidx.compose.runtime.ComposableTarget] for details.
19+
*/
20+
@ComposableTargetMarker(description = "Workflow Composable")
21+
@Target(FILE, FUNCTION, PROPERTY_GETTER, TYPE, TYPE_PARAMETER)
22+
@Retention(BINARY)
23+
annotation class WorkflowComposable
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.squareup.workflow1
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.MutableState
5+
import androidx.compose.runtime.mutableStateOf
6+
import androidx.compose.runtime.remember
7+
import androidx.compose.runtime.staticCompositionLocalOf
8+
9+
internal val LocalWorkflowComposableRenderer =
10+
staticCompositionLocalOf<WorkflowComposableRenderer> { error("No renderer") }
11+
12+
internal interface WorkflowComposableRenderer {
13+
@Composable
14+
fun <ChildPropsT, ChildOutputT, ChildRenderingT> Child(
15+
workflow: Workflow<ChildPropsT, ChildOutputT, ChildRenderingT>,
16+
props: ChildPropsT,
17+
onOutput: ((ChildOutputT) -> Unit)?
18+
): ChildRenderingT
19+
}
20+
21+
/**
22+
* Renders a child [Workflow] from any [WorkflowComposable] (e.g. a [ComposeWorkflow.Rendering] or
23+
* [BaseRenderContext.renderComposable]) and returns its rendering.
24+
*
25+
* @param handler An optional function that, if non-null, will be called when the child emits an
26+
* output. If null, the child's outputs will be ignored.
27+
*/
28+
@WorkflowComposable
29+
@Composable
30+
fun <ChildPropsT, ChildOutputT, ChildRenderingT> Child(
31+
workflow: Workflow<ChildPropsT, ChildOutputT, ChildRenderingT>,
32+
props: ChildPropsT,
33+
onOutput: ((ChildOutputT) -> Unit)?
34+
): ChildRenderingT {
35+
val renderer = LocalWorkflowComposableRenderer.current
36+
return renderer.Child(workflow, props, onOutput)
37+
}
38+
39+
@WorkflowComposable
40+
@Composable
41+
fun <ChildPropsT, ChildOutputT, ChildRenderingT> Child(
42+
workflow: ComposeWorkflow<ChildPropsT, ChildOutputT, ChildRenderingT>,
43+
props: ChildPropsT,
44+
handler: ((ChildOutputT) -> Unit)?
45+
): ChildRenderingT {
46+
val childRendering = remember { mutableStateOf<ChildRenderingT?>(null) }
47+
// Since this function returns a value, it can't restart without also restarting its parent.
48+
// IsolateRecomposeScope allows the subtree to restart and only restarts us if the rendering value
49+
// actually changed.
50+
IsolateRecomposeScope(
51+
child = workflow,
52+
props = props,
53+
handler = handler,
54+
result = childRendering
55+
)
56+
@Suppress("UNCHECKED_CAST")
57+
return childRendering.value as ChildRenderingT
58+
}
59+
60+
@Composable
61+
private fun <PropsT, OutputT, RenderingT> IsolateRecomposeScope(
62+
child: ComposeWorkflow<PropsT, OutputT, RenderingT>,
63+
props: PropsT,
64+
handler: ((OutputT) -> Unit)?,
65+
result: MutableState<RenderingT>,
66+
) {
67+
result.value = child.Rendering(props, handler ?: {})
68+
}

0 commit comments

Comments
 (0)