Skip to content

Commit 6586687

Browse files
Introduce renderComposable.
1 parent 56e6ee0 commit 6586687

File tree

18 files changed

+485
-73
lines changed

18 files changed

+485
-73
lines changed
Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
package com.squareup.sample.compose.hellocompose
22

3-
import com.squareup.sample.compose.hellocompose.HelloComposeWorkflow.State
4-
import com.squareup.sample.compose.hellocompose.HelloComposeWorkflow.State.Goodbye
3+
import androidx.compose.runtime.getValue
4+
import androidx.compose.runtime.mutableStateOf
5+
import androidx.compose.runtime.remember
6+
import androidx.compose.runtime.setValue
57
import com.squareup.sample.compose.hellocompose.HelloComposeWorkflow.State.Hello
6-
import com.squareup.workflow1.Snapshot
7-
import com.squareup.workflow1.StatefulWorkflow
8-
import com.squareup.workflow1.action
9-
import com.squareup.workflow1.parse
8+
import com.squareup.workflow1.StatelessWorkflow
9+
import com.squareup.workflow1.WorkflowExperimentalApi
1010

11-
object HelloComposeWorkflow : StatefulWorkflow<Unit, State, Nothing, HelloComposeScreen>() {
11+
object HelloComposeWorkflow : StatelessWorkflow<Unit, Nothing, HelloComposeScreen>() {
1212
enum class State {
1313
Hello,
1414
Goodbye;
@@ -19,24 +19,15 @@ object HelloComposeWorkflow : StatefulWorkflow<Unit, State, Nothing, HelloCompos
1919
}
2020
}
2121

22-
private val helloAction = action("hello") {
23-
state = state.theOtherState()
24-
}
25-
26-
override fun initialState(
27-
props: Unit,
28-
snapshot: Snapshot?
29-
): State = snapshot?.bytes?.parse { source -> if (source.readInt() == 1) Hello else Goodbye }
30-
?: Hello
31-
22+
@OptIn(WorkflowExperimentalApi::class)
3223
override fun render(
3324
renderProps: Unit,
34-
renderState: State,
3525
context: RenderContext
36-
): HelloComposeScreen = HelloComposeScreen(
37-
message = renderState.name,
38-
onClick = { context.actionSink.send(helloAction) }
39-
)
40-
41-
override fun snapshotState(state: State): Snapshot = Snapshot.of(if (state == Hello) 1 else 0)
26+
): HelloComposeScreen = context.renderComposable {
27+
var state by remember { mutableStateOf(Hello) }
28+
HelloComposeScreen(
29+
message = state.name,
30+
onClick = { state = state.theOtherState() }
31+
)
32+
}
4233
}

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
@@ -59,7 +60,8 @@ class NestedRenderingsActivity : AppCompatActivity() {
5960
workflow = RecursiveWorkflow.mapRendering { it.withEnvironment(viewEnvironment) },
6061
scope = viewModelScope,
6162
savedStateHandle = savedState,
62-
runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig()
63+
runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig(),
64+
interceptors = listOf(SimpleLoggingWorkflowInterceptor())
6365
)
6466
}
6567
}
Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
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.mutableIntStateOf
6+
import androidx.compose.runtime.remember
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.StatelessWorkflow
12+
import com.squareup.workflow1.WorkflowExperimentalApi
1113
import com.squareup.workflow1.ui.AndroidScreen
1214
import com.squareup.workflow1.ui.Screen
1315
import com.squareup.workflow1.ui.ScreenViewFactory
@@ -22,9 +24,7 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
2224
* through Composable renderings as well as adapting in both directions.
2325
*/
2426
@OptIn(WorkflowUiExperimentalApi::class)
25-
object RecursiveWorkflow : StatefulWorkflow<Unit, State, Nothing, Screen>() {
26-
27-
data class State(val children: Int = 0)
27+
object RecursiveWorkflow : StatelessWorkflow<Unit, Nothing, Screen>() {
2828

2929
/**
3030
* A rendering from a [RecursiveWorkflow].
@@ -51,33 +51,29 @@ object RecursiveWorkflow : StatefulWorkflow<Unit, State, Nothing, Screen>() {
5151
)
5252
}
5353

54-
override fun initialState(
55-
props: Unit,
56-
snapshot: Snapshot?
57-
): State = State()
58-
54+
@OptIn(WorkflowExperimentalApi::class)
5955
override fun render(
6056
renderProps: Unit,
61-
renderState: State,
6257
context: RenderContext
63-
): Rendering {
64-
return Rendering(
65-
children = List(renderState.children) { i ->
66-
val child = context.renderChild(RecursiveWorkflow, key = i.toString())
67-
if (i % 2 == 0) child else LegacyRendering(child)
68-
},
69-
onAddChildClicked = { context.actionSink.send(addChild()) },
70-
onResetClicked = { context.actionSink.send(reset()) }
71-
)
58+
): Rendering = context.renderComposable {
59+
produceRendering()
7260
}
61+
}
7362

74-
override fun snapshotState(state: State): Snapshot? = null
75-
76-
private fun addChild() = action("addChild") {
77-
state = state.copy(children = state.children + 1)
78-
}
63+
@OptIn(WorkflowUiExperimentalApi::class)
64+
@Composable
65+
private fun produceRendering(): Rendering {
66+
var children by remember { mutableIntStateOf(0) }
7967

80-
private fun reset() = action("reset") {
81-
state = State()
82-
}
68+
return Rendering(
69+
children = List(children) { i ->
70+
val child = produceRendering()
71+
if ((i % 2) == 0) child else LegacyRendering(child)
72+
},
73+
onAddChildClicked = {
74+
println("OMG onAddChildClicked")
75+
children++
76+
},
77+
onResetClicked = { children = 0 }
78+
)
8379
}

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
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

Lines changed: 3 additions & 0 deletions
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.6.11"
67
}
78

89
kotlin {
@@ -23,6 +24,8 @@ 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")
28+
commonMainApi("org.jetbrains.compose.runtime:runtime-saveable:1.7.3")
2629

2730
commonTestImplementation(libs.kotlinx.atomicfu)
2831
commonTestImplementation(libs.kotlinx.coroutines.test.common)

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99

1010
package com.squareup.workflow1
1111

12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.runtime.saveable.rememberSaveable
1214
import com.squareup.workflow1.WorkflowAction.Companion.noAction
15+
import com.squareup.workflow1.compose.WorkflowComposable
1316
import kotlinx.coroutines.CoroutineScope
1417
import kotlin.jvm.JvmMultifileClass
1518
import kotlin.jvm.JvmName
@@ -85,6 +88,21 @@ public interface BaseRenderContext<out PropsT, StateT, in OutputT> {
8588
handler: (ChildOutputT) -> WorkflowAction<PropsT, StateT, OutputT>
8689
): ChildRenderingT
8790

91+
/**
92+
* Synchronously composes a [content] function and returns its rendering. Whenever [content] is
93+
* invalidated (i.e. a compose snapshot state object is changed that was previously read by
94+
* [content] or any functions it calls), this workflow will be re-rendered and the relevant
95+
* composables will be recomposed.
96+
*
97+
* Any state saved using Compose's state restoration mechanism (e.g. [rememberSaveable]) will be
98+
* saved and restored using the workflow snapshot mechanism.
99+
*/
100+
@WorkflowExperimentalApi
101+
public fun <ChildRenderingT> renderComposable(
102+
key: String = "",
103+
content: @WorkflowComposable @Composable () -> ChildRenderingT
104+
): ChildRenderingT
105+
88106
/**
89107
* Ensures [sideEffect] is running with the given [key].
90108
*
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.squareup.workflow1.compose
2+
3+
import androidx.compose.runtime.ComposableTargetMarker
4+
import com.squareup.workflow1.WorkflowExperimentalApi
5+
import kotlin.annotation.AnnotationRetention.BINARY
6+
import kotlin.annotation.AnnotationTarget.FILE
7+
import kotlin.annotation.AnnotationTarget.FUNCTION
8+
import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER
9+
import kotlin.annotation.AnnotationTarget.TYPE
10+
import kotlin.annotation.AnnotationTarget.TYPE_PARAMETER
11+
12+
/**
13+
* An annotation that can be used to mark a composable function as being expected to be use in a
14+
* composable function that is also marked or inferred to be marked as a [WorkflowComposable], i.e.
15+
* that can be called from [BaseRenderContext.renderComposable].
16+
*
17+
* Using this annotation explicitly is rarely necessary as the Compose compiler plugin will infer
18+
* the necessary equivalent annotations automatically. See
19+
* [androidx.compose.runtime.ComposableTarget] for details.
20+
*/
21+
@WorkflowExperimentalApi
22+
@ComposableTargetMarker(description = "Workflow Composable")
23+
@Target(FILE, FUNCTION, PROPERTY_GETTER, TYPE, TYPE_PARAMETER)
24+
@Retention(BINARY)
25+
public annotation class WorkflowComposable

workflow-runtime/build.gradle.kts

Lines changed: 8 additions & 0 deletions
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.6.11"
67
}
78

89
kotlin {
@@ -16,6 +17,13 @@ kotlin {
1617
if (targets == "kmp" || targets == "js") {
1718
js(IR) { browser() }
1819
}
20+
sourceSets {
21+
getByName("commonMain") {
22+
dependencies {
23+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
24+
}
25+
}
26+
}
1927
}
2028

2129
dependencies {

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.squareup.workflow1
22

3+
import androidx.compose.runtime.Composable
34
import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor
45
import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
56
import kotlinx.coroutines.CoroutineScope
@@ -150,5 +151,15 @@ public open class SimpleLoggingWorkflowInterceptor : WorkflowInterceptor {
150151
}
151152
}
152153
}
154+
155+
override fun <CR> onRenderComposable(
156+
key: String,
157+
content: @Composable () -> CR,
158+
proceed: (key: String, content: @Composable () -> CR) -> CR
159+
): CR = proceed(key) {
160+
logMethod("onRenderComposable", session, "key" to key, "content" to content) {
161+
content()
162+
}
163+
}
153164
}
154165
}

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import kotlin.LazyThreadSafetyMode.NONE
2121
*/
2222
public class TreeSnapshot internal constructor(
2323
workflowSnapshot: Snapshot?,
24-
childTreeSnapshots: () -> Map<WorkflowNodeId, TreeSnapshot>
24+
childTreeSnapshots: () -> Map<ByteString, TreeSnapshot>
2525
) {
2626
/**
2727
* The [Snapshot] for the root workflow, or null if that snapshot was empty or unspecified.
@@ -35,7 +35,7 @@ public class TreeSnapshot internal constructor(
3535
* The map of child snapshots by child [WorkflowNodeId]. Computed lazily so the entire snapshot
3636
* tree isn't parsed upfront.
3737
*/
38-
internal val childTreeSnapshots: Map<WorkflowNodeId, TreeSnapshot>
38+
internal val childTreeSnapshots: Map<ByteString, TreeSnapshot>
3939
by lazy(NONE, childTreeSnapshots)
4040

4141
/**
@@ -49,11 +49,10 @@ public class TreeSnapshot internal constructor(
4949
sink.writeByteStringWithLength(workflowSnapshot?.bytes ?: ByteString.EMPTY)
5050
val childBytes: List<Pair<ByteString, ByteString>> =
5151
childTreeSnapshots.mapNotNull { (childId, childSnapshot) ->
52-
val childIdBytes = childId.toByteStringOrNull() ?: return@mapNotNull null
5352
val childSnapshotBytes = childSnapshot.toByteString()
5453
.takeUnless { it.size == 0 }
5554
?: return@mapNotNull null
56-
return@mapNotNull Pair(childIdBytes, childSnapshotBytes)
55+
return@mapNotNull Pair(childId, childSnapshotBytes)
5756
}
5857
sink.writeInt(childBytes.size)
5958
childBytes.forEach { (childIdBytes, childSnapshotBytes) ->
@@ -104,9 +103,8 @@ public class TreeSnapshot internal constructor(
104103
buildMap(childSnapshotCount) {
105104
for (i in 0 until childSnapshotCount) {
106105
val idBytes = source.readByteStringWithLength()
107-
val id = WorkflowNodeId.parse(idBytes)
108106
val childSnapshot = source.readByteStringWithLength()
109-
this[id] = parse(childSnapshot)
107+
this[idBytes] = parse(childSnapshot)
110108
}
111109
}
112110
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.squareup.workflow1
22

3+
import androidx.compose.runtime.Composable
34
import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor
45
import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
56
import kotlinx.coroutines.CoroutineScope
@@ -259,6 +260,15 @@ public interface WorkflowInterceptor {
259260
handler: (CO) -> WorkflowAction<P, S, O>
260261
) -> CR
261262
): CR = proceed(child, childProps, key, handler)
263+
264+
public fun <CR> onRenderComposable(
265+
key: String,
266+
content: @Composable () -> CR,
267+
proceed: (
268+
key: String,
269+
content: @Composable () -> CR
270+
) -> CR
271+
): CR = proceed(key, content)
262272
}
263273
}
264274

@@ -384,6 +394,21 @@ private class InterceptedRenderContext<P, S, O>(
384394
}
385395
}
386396

397+
@OptIn(WorkflowExperimentalApi::class)
398+
override fun <ChildRenderingT> renderComposable(
399+
key: String,
400+
content: @Composable () -> ChildRenderingT
401+
): ChildRenderingT = interceptor.onRenderComposable(
402+
key = key,
403+
content = content,
404+
proceed = { iKey, iContent ->
405+
baseRenderContext.renderComposable(
406+
key = iKey,
407+
content = iContent
408+
)
409+
}
410+
)
411+
387412
/**
388413
* In a block with a CoroutineScope receiver, calls to `coroutineContext` bind
389414
* to `CoroutineScope.coroutineContext` instead of `suspend val coroutineContext`.

0 commit comments

Comments
 (0)