Skip to content

Commit 40cb5ff

Browse files
Introduce renderComposable.
1 parent cbcb12e commit 40cb5ff

File tree

14 files changed

+406
-5
lines changed

14 files changed

+406
-5
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
}

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.7.3"
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: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99
package com.squareup.workflow1
1010

11+
import androidx.compose.runtime.Composable
1112
import com.squareup.workflow1.WorkflowAction.Companion.noAction
13+
import com.squareup.workflow1.compose.WorkflowComposable
1214
import kotlinx.coroutines.CoroutineScope
1315
import kotlin.jvm.JvmMultifileClass
1416
import kotlin.jvm.JvmName
@@ -86,6 +88,27 @@ public interface BaseRenderContext<PropsT, StateT, OutputT> {
8688
handler: (ChildOutputT) -> WorkflowAction<PropsT, StateT, OutputT>
8789
): ChildRenderingT
8890

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+
// * The `emitOutput` function passed to [content] should be used to trigger [WorkflowAction]s in
98+
// * this workflow via [handler]. Every invocation of `emitOutput` will result [handler]s action
99+
// * being sent to this context's [actionSink]. However, it's important for the composable never to
100+
// * send to [actionSink] directly because we need to ensure that any state writes the composable
101+
// * does invalidate their composables before sending into the [actionSink].
102+
// */
103+
// @WorkflowExperimentalApi
104+
// public fun <ChildOutputT, ChildRenderingT> renderComposable(
105+
// key: String = "",
106+
// handler: (ChildOutputT) -> WorkflowAction<PropsT, StateT, OutputT>,
107+
// content: @WorkflowComposable @Composable (
108+
// emitOutput: (ChildOutputT) -> Unit
109+
// ) -> ChildRenderingT
110+
// ): ChildRenderingT
111+
89112
/**
90113
* Ensures [sideEffect] is running with the given [key].
91114
*
@@ -209,6 +232,20 @@ public fun <PropsT, StateT, OutputT, ChildRenderingT>
209232
key: String = ""
210233
): ChildRenderingT = renderChild(child, Unit, key) { noAction() }
211234

235+
/**
236+
* TODO
237+
*/
238+
@WorkflowExperimentalApi
239+
public fun <PropsT, StateT, OutputT, ChildRenderingT>
240+
BaseRenderContext<PropsT, StateT, OutputT>.renderComposable(
241+
key: String = "",
242+
content: @WorkflowComposable @Composable () -> ChildRenderingT
243+
): ChildRenderingT = renderComposable<Nothing, ChildRenderingT>(
244+
key = key,
245+
handler = { noAction() },
246+
content = { content() }
247+
)
248+
212249
/**
213250
* Ensures a [LifecycleWorker] is running. Since [worker] can't emit anything,
214251
* it can't trigger any [WorkflowAction]s.
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.7.3"
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/WorkflowInterceptor.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
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
6+
import com.squareup.workflow1.compose.WorkflowComposable
57
import kotlinx.coroutines.CoroutineScope
68
import kotlinx.coroutines.Job
79
import kotlin.coroutines.CoroutineContext
@@ -322,6 +324,15 @@ public interface WorkflowInterceptor {
322324
calculation: () -> CResult
323325
) -> CResult
324326
): CResult = proceed(key, resultType, inputs, calculation)
327+
328+
public fun <CO, CR> onRenderComposable(
329+
key: String,
330+
content: @Composable (CO) -> CR,
331+
proceed: (
332+
key: String,
333+
content: @Composable (CO) -> CR
334+
) -> CR
335+
): CR = proceed(key, content)
325336
}
326337
}
327338

@@ -459,6 +470,23 @@ private class InterceptedRenderContext<P, S, O>(
459470
}
460471
}
461472

473+
@OptIn(WorkflowExperimentalApi::class)
474+
override fun <ChildOutputT, ChildRenderingT> renderComposable(
475+
key: String,
476+
handler: (ChildOutputT) -> WorkflowAction<P, S, O>,
477+
content: @WorkflowComposable @Composable (emitOutput: (ChildOutputT) -> Unit) -> ChildRenderingT
478+
): ChildRenderingT = interceptor.onRenderComposable(
479+
key = key,
480+
content = content,
481+
proceed = { iKey, iContent ->
482+
baseRenderContext.renderComposable(
483+
key = iKey,
484+
handler = handler,
485+
content = iContent
486+
)
487+
}
488+
)
489+
462490
/**
463491
* In a block with a CoroutineScope receiver, calls to `coroutineContext` bind
464492
* to `CoroutineScope.coroutineContext` instead of `suspend val coroutineContext`.

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
@file:OptIn(WorkflowExperimentalApi::class)
2+
13
package com.squareup.workflow1.internal
24

5+
import androidx.compose.runtime.Composable
36
import com.squareup.workflow1.BaseRenderContext
47
import com.squareup.workflow1.RuntimeConfig
58
import com.squareup.workflow1.Sink
69
import com.squareup.workflow1.Workflow
710
import com.squareup.workflow1.WorkflowAction
11+
import com.squareup.workflow1.WorkflowExperimentalApi
812
import com.squareup.workflow1.WorkflowTracer
13+
import com.squareup.workflow1.compose.WorkflowComposable
914
import com.squareup.workflow1.identifier
1015
import kotlinx.coroutines.CoroutineScope
1116
import kotlinx.coroutines.channels.SendChannel
@@ -27,6 +32,12 @@ internal class RealRenderContext<PropsT, StateT, OutputT>(
2732
key: String,
2833
handler: (ChildOutputT) -> WorkflowAction<PropsT, StateT, OutputT>
2934
): ChildRenderingT
35+
36+
fun <ChildOutputT, ChildRenderingT> renderComposable(
37+
key: String,
38+
handler: (ChildOutputT) -> WorkflowAction<PropsT, StateT, OutputT>,
39+
content: @Composable (emitOutput: (ChildOutputT) -> Unit) -> ChildRenderingT
40+
): ChildRenderingT
3041
}
3142

3243
interface SideEffectRunner {
@@ -78,6 +89,15 @@ internal class RealRenderContext<PropsT, StateT, OutputT>(
7889
return renderer.render(child, props, key, handler)
7990
}
8091

92+
override fun <ChildOutputT, ChildRenderingT> renderComposable(
93+
key: String,
94+
handler: (ChildOutputT) -> WorkflowAction<PropsT, StateT, OutputT>,
95+
content: @WorkflowComposable @Composable (emitOutput: (ChildOutputT) -> Unit) -> ChildRenderingT
96+
): ChildRenderingT {
97+
checkNotFrozen()
98+
return renderer.renderComposable(key, handler, content)
99+
}
100+
81101
override fun runningSideEffect(
82102
key: String,
83103
sideEffect: suspend CoroutineScope.() -> Unit
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.squareup.workflow1.internal
2+
3+
import com.squareup.workflow1.WorkflowAction
4+
5+
/**
6+
* This action doesn't actually update state, but it's special-cased inside WorkflowNode to always
7+
* act like it updated state, to force a re-render and thus a recomposition.
8+
*/
9+
internal class RecomposeAction<PropsT, StateT, OutputT> : WorkflowAction<PropsT, StateT, OutputT>() {
10+
override fun Updater.apply() {
11+
// Noop
12+
}
13+
}

0 commit comments

Comments
 (0)