Skip to content

Introduce ComposeWorkflow. #1357

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 20 commits into
base: zachklipp/workflownode-poly
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3f1dca8
Introduce ComposeWorkflow.
zach-klippenstein Feb 26, 2025
78662ad
Add WorkflowInterceptor API to intercept ComposeWorkflow render passes.
zach-klippenstein Jun 24, 2025
17958c2
Simplify compose workflow interception, implement in all built-in int…
zach-klippenstein Jun 24, 2025
4e4029a
Basic support for rememberSaveable.
zach-klippenstein Jun 25, 2025
e5f40c9
Move compose runtime code into dedicated package.
zach-klippenstein Jun 25, 2025
c327c61
Rendering Workflows from ComposeWorkflow.
zach-klippenstein Jun 25, 2025
159aae8
Try harder to get a frame from the recomposer on render, regardless o…
zach-klippenstein Jun 27, 2025
c3b70ae
Add some comments.
zach-klippenstein Jun 27, 2025
eeb366b
Refactor RecomposerDriver a bit.
zach-klippenstein Jun 27, 2025
72d58a8
Rename PreemptingDispatcher -> WorkStealingDispatcher and rewrite it.
zach-klippenstein Jun 28, 2025
60716fd
More comments.
zach-klippenstein Jun 28, 2025
c4b6756
Fix ComposeWorkflowChildNode rendering the wrong props.
zach-klippenstein Jun 29, 2025
af9f971
Keep frameTime at 0 always.
zach-klippenstein Jun 29, 2025
912b1c7
Refactor RecomposerDriver -> SynchronizedMolecule.
zach-klippenstein Jun 30, 2025
e565108
Dispatch WorkflowInterceptor.onPropsChanged from Compose workflow nodes.
zach-klippenstein Jun 30, 2025
44dc3ce
Move UnitApplier to compose runtime package.
zach-klippenstein Jun 30, 2025
718344d
Clean up the AbstractWorkflowNode refactor.
zach-klippenstein Jul 1, 2025
d151db8
Try converting more workflows to Compose.
zach-klippenstein Jul 1, 2025
7b86421
WIP Sketching out ComposeWorkflow RenderTester API.
zach-klippenstein Jul 3, 2025
bf96d62
Apply changes from dependencyGuardBaseline --refresh-dependencies
zach-klippenstein Jul 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.squareup.benchmarks.performance.complex.poetry.instrumentation

import androidx.compose.runtime.Composable
import androidx.tracing.Trace
import com.squareup.benchmarks.performance.complex.poetry.PerformancePoemWorkflow
import com.squareup.benchmarks.performance.complex.poetry.PerformancePoemsBrowserWorkflow
import com.squareup.benchmarks.performance.complex.poetry.instrumentation.PerformanceTracingInterceptor.Companion.NODES_TO_TRACE
import com.squareup.workflow1.BaseRenderContext
import com.squareup.workflow1.WorkflowExperimentalApi
import com.squareup.workflow1.WorkflowInterceptor
import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor
import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
Expand All @@ -27,6 +30,24 @@ class PerformanceTracingInterceptor(
context: BaseRenderContext<P, S, O>,
proceed: (P, S, RenderContextInterceptor<P, S, O>?) -> R,
session: WorkflowSession
): R = traceRender(session) {
proceed(renderProps, renderState, null)
}

@OptIn(WorkflowExperimentalApi::class)
@Composable
override fun <P, O, R> onRenderComposeWorkflow(
renderProps: P,
emitOutput: (O) -> Unit,
proceed: @Composable (P, (O) -> Unit) -> R,
session: WorkflowSession
): R = traceRender(session) {
proceed(renderProps, emitOutput)
}

private inline fun <R> traceRender(
session: WorkflowSession,
render: () -> R
): R {
val isRoot = session.parent == null
val traceIdIndex = NODES_TO_TRACE.indexOfFirst { it.second == session.identifier }
Expand All @@ -45,7 +66,7 @@ class PerformanceTracingInterceptor(
Trace.beginSection(sectionName)
}

return proceed(renderProps, renderState, null).also {
return render().also {
if (traceIdIndex > -1 && !sample) {
Trace.endSection()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,36 +14,32 @@ import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.tooling.preview.Preview
import com.squareup.workflow1.Snapshot
import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.WorkflowExperimentalApi
import com.squareup.workflow1.WorkflowExperimentalRuntime
import com.squareup.workflow1.compose.ComposeWorkflow
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
import com.squareup.workflow1.parse
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.compose.ComposeScreen
import com.squareup.workflow1.ui.compose.WorkflowRendering
import com.squareup.workflow1.ui.compose.renderAsState

object InlineRenderingWorkflow : StatefulWorkflow<Unit, Int, Nothing, Screen>() {
@OptIn(WorkflowExperimentalApi::class)
object InlineRenderingWorkflow : ComposeWorkflow<Unit, Nothing, Screen>() {

override fun initialState(
@Composable
override fun produceRendering(
props: Unit,
snapshot: Snapshot?
): Int = snapshot?.bytes?.parse { it.readInt() } ?: 0

override fun render(
renderProps: Unit,
renderState: Int,
context: RenderContext<Unit, Int, Nothing>
): ComposeScreen {
val onClick = context.eventHandler("increment") { state += 1 }
emitOutput: (Nothing) -> Unit
): Screen {
var state by rememberSaveable { mutableIntStateOf(0) }
return ComposeScreen {
Content(renderState, onClick)
Content(state, onClick = { state++ })
}
}

override fun snapshotState(state: Int): Snapshot = Snapshot.of(state)
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import androidx.compose.ui.graphics.Color
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.squareup.workflow1.SimpleLoggingWorkflowInterceptor
import com.squareup.workflow1.WorkflowExperimentalRuntime
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
import com.squareup.workflow1.mapRendering
Expand Down Expand Up @@ -47,7 +48,8 @@ class NestedRenderingsActivity : AppCompatActivity() {
workflow = RecursiveWorkflow.mapRendering { it.withEnvironment(viewEnvironment) },
scope = viewModelScope,
savedStateHandle = savedState,
runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig()
runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig(),
interceptors = listOf(SimpleLoggingWorkflowInterceptor())
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

package com.squareup.sample.compose.nestedrenderings

import androidx.compose.animation.Animatable
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.keyframes
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Arrangement.SpaceEvenly
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
Expand All @@ -13,12 +21,17 @@ import androidx.compose.material.Card
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.tooling.preview.Preview
import com.squareup.sample.compose.R
Expand All @@ -27,6 +40,7 @@ import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.compose.ScreenComposableFactory
import com.squareup.workflow1.ui.compose.WorkflowRendering
import com.squareup.workflow1.ui.compose.tooling.Preview
import kotlin.time.DurationUnit.MILLISECONDS

/**
* Composition local of [Color] to use as the background color for a [RecursiveComposableFactory].
Expand All @@ -44,7 +58,26 @@ val RecursiveComposableFactory = ScreenComposableFactory<Rendering> { rendering
.compositeOver(Color.Black)
}

Card(backgroundColor = color) {
var lastFlashedTrigger by remember { mutableIntStateOf(rendering.flashTrigger) }
val flashAlpha = remember { Animatable(Color(0x00FFFFFF)) }

// Flash the card white when asked.
LaunchedEffect(rendering.flashTrigger) {
if (rendering.flashTrigger != 0) {
lastFlashedTrigger = rendering.flashTrigger
flashAlpha.animateTo(Color(0x00FFFFFF), animationSpec = keyframes {
Color.White at (rendering.flashTime / 7).toInt(MILLISECONDS) using FastOutLinearInEasing
Color(0x00FFFFFF) at rendering.flashTime.toInt(MILLISECONDS) using LinearOutSlowInEasing
})
}
}

Card(
backgroundColor = flashAlpha.value.compositeOver(color),
modifier = Modifier.pointerInput(rendering) {
detectTapGestures(onPress = { rendering.onSelfClicked() })
}
) {
Column(
Modifier
.padding(dimensionResource(R.dimen.recursive_padding))
Expand Down Expand Up @@ -76,10 +109,14 @@ fun RecursiveViewFactoryPreview() {
StringRendering("foo"),
Rendering(
children = listOf(StringRendering("bar")),
flashTrigger = 0,
onSelfClicked = {},
onAddChildClicked = {},
onResetClicked = {}
)
),
flashTrigger = 0,
onSelfClicked = {},
onAddChildClicked = {},
onResetClicked = {}
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
package com.squareup.sample.compose.nestedrenderings

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import com.squareup.sample.compose.databinding.LegacyViewBinding
import com.squareup.sample.compose.nestedrenderings.RecursiveWorkflow.LegacyRendering
import com.squareup.sample.compose.nestedrenderings.RecursiveWorkflow.Rendering
import com.squareup.sample.compose.nestedrenderings.RecursiveWorkflow.State
import com.squareup.workflow1.Snapshot
import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.action
import com.squareup.workflow1.renderChild
import com.squareup.workflow1.WorkflowExperimentalApi
import com.squareup.workflow1.compose.ComposeWorkflow
import com.squareup.workflow1.compose.renderChild
import com.squareup.workflow1.ui.AndroidScreen
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.ScreenViewFactory
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.time.Duration
import kotlin.time.Duration.Companion.ZERO
import kotlin.time.Duration.Companion.seconds

/**
* A simple workflow that produces [Rendering]s of zero or more children.
Expand All @@ -20,9 +34,8 @@ import com.squareup.workflow1.ui.ScreenViewFactory
* to force it to go through the legacy view layer. This way this sample both demonstrates pass-
* through Composable renderings as well as adapting in both directions.
*/
object RecursiveWorkflow : StatefulWorkflow<Unit, State, Nothing, Screen>() {

data class State(val children: Int = 0)
@OptIn(WorkflowExperimentalApi::class)
object RecursiveWorkflow : ComposeWorkflow<Unit, Unit, Screen>() {

/**
* A rendering from a [RecursiveWorkflow].
Expand All @@ -33,8 +46,11 @@ object RecursiveWorkflow : StatefulWorkflow<Unit, State, Nothing, Screen>() {
*/
data class Rendering(
val children: List<Screen>,
val onAddChildClicked: () -> Unit,
val onResetClicked: () -> Unit
val flashTrigger: Int = 0,
val flashTime: Duration = ZERO,
val onSelfClicked: () -> Unit = {},
val onAddChildClicked: () -> Unit = {},
val onResetClicked: () -> Unit = {}
) : Screen

/**
Expand All @@ -49,33 +65,45 @@ object RecursiveWorkflow : StatefulWorkflow<Unit, State, Nothing, Screen>() {
)
}

override fun initialState(
@OptIn(ExperimentalStdlibApi::class)
@Composable override fun produceRendering(
props: Unit,
snapshot: Snapshot?
): State = State()
emitOutput: (Unit) -> Unit
): Screen {
var children by rememberSaveable { mutableStateOf(0) }
var flashTrigger by remember { mutableIntStateOf(0) }
val coroutineScope = rememberCoroutineScope()

DisposableEffect(Unit) {
println("OMG coroutineScope dispatcher: ${coroutineScope.coroutineContext[CoroutineDispatcher]}")
onDispose {}
}

LaunchedEffect(Unit) {
println("OMG LaunchedEffect dispatcher: ${coroutineScope.coroutineContext[CoroutineDispatcher]}")
}

override fun render(
renderProps: Unit,
renderState: State,
context: RenderContext<Unit, State, Nothing>
): Rendering {
return Rendering(
children = List(renderState.children) { i ->
val child = context.renderChild(RecursiveWorkflow, key = i.toString())
children = List(children) { i ->
val child = renderChild(RecursiveWorkflow, onOutput = {
// When a child is clicked, cascade the flash up.
coroutineScope.launch {
delay(0.1.seconds)
flashTrigger++
emitOutput(Unit)
}
})
if (i % 2 == 0) child else LegacyRendering(child)
},
onAddChildClicked = { context.actionSink.send(addChild()) },
onResetClicked = { context.actionSink.send(reset()) }
flashTrigger = flashTrigger,
flashTime = 0.5.seconds,
// Trigger a cascade of flashes when clicked.
onSelfClicked = {
flashTrigger++
emitOutput(Unit)
},
onAddChildClicked = { children++ },
onResetClicked = { children = 0 }
)
}

override fun snapshotState(state: State): Snapshot? = null

private fun addChild() = action("addChild") {
state = state.copy(children = state.children + 1)
}

private fun reset() = action("reset") {
state = State()
}
}
1 change: 1 addition & 0 deletions samples/dungeon/common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id("kotlin-jvm")
id("kotlinx-serialization")
alias(libs.plugins.compose.compiler)
}

dependencies {
Expand Down
Loading
Loading