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 10 commits into
base: zachklipp/workflownode-poly
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -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()
}
}
26 changes: 26 additions & 0 deletions samples/hello-compose-workflow/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
plugins {
id("com.android.application")
id("kotlin-android")
id("android-sample-app")
id("android-ui-tests")
alias(libs.plugins.compose.compiler)
}

android {
defaultConfig {
applicationId = "com.squareup.sample.hellocomposeworkflow"
}
namespace = "com.squareup.sample.hellocomposeworkflow"
}

dependencies {
debugImplementation(libs.squareup.leakcanary.android)

implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.viewmodel.savedstate)
implementation(libs.androidx.viewbinding)

implementation(project(":workflow-ui:core-android"))
implementation(project(":workflow-ui:core-common"))
}
15 changes: 15 additions & 0 deletions samples/hello-compose-workflow/lint-baseline.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 7.4.1" type="baseline" client="gradle" dependencies="false" name="AGP (7.4.1)" variant="all" version="7.4.1">

<issue
id="DataExtractionRules"
message="The attribute `android:allowBackup` is deprecated from Android 12 and higher and may be removed in future versions. Consider adding the attribute `android:dataExtractionRules` specifying an `@xml` resource which configures cloud backups and device transfers on Android 12 and higher."
errorLine1=" android:allowBackup=&quot;false&quot;"
errorLine2=" ~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="6"
column="28"/>
</issue>

</issues>
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.squareup.sample.hellocomposeworkflow

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule
import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class HelloComposeWorkflowAppTest {

private val scenarioRule = ActivityScenarioRule(HelloComposeWorkflowActivity::class.java)

@get:Rule val rules: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
.around(scenarioRule)
.around(IdlingDispatcherRule)

@Test fun togglesHelloAndGoodbye() {
onView(withText("Hello"))
.check(matches(isDisplayed()))
.perform(click())

onView(withText("Goodbye"))
.check(matches(isDisplayed()))
.perform(click())

onView(withText("Hello"))
.check(matches(isDisplayed()))
}
}
23 changes: 23 additions & 0 deletions samples/hello-compose-workflow/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application
android:allowBackup="false"
android:label="@string/app_name"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning,MissingApplicationIcon"
>

<activity android:name="com.squareup.sample.hellocomposeworkflow.HelloComposeWorkflowActivity"
android:exported="true">

<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

</activity>

</application>
</manifest>
Loading
Loading