Skip to content
This repository was archived by the owner on Feb 5, 2021. It is now read-only.

Commit e903936

Browse files
Introduce ComposeWorkflow, a self-rendering Workflow.
`ComposeWorkflow` is like a `StatelessWorkflow`, but it has a special `render` method: it's a `@Composable` function, and it functions like the body of a `bindCompose` lambda where the rendering is just the workflow's props and a `Sink` accepting `OutputT`s.
1 parent 62ce98e commit e903936

File tree

14 files changed

+574
-0
lines changed

14 files changed

+574
-0
lines changed

core-compose/api/core-compose.api

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
public final class com/squareup/workflow/compose/ComposeRendering {
2+
public static final field Companion Lcom/squareup/workflow/compose/ComposeRendering$Companion;
3+
public static final fun <clinit> ()V
4+
public fun <init> (Lkotlin/jvm/functions/Function2;)V
5+
}
6+
7+
public final class com/squareup/workflow/compose/ComposeRendering$Companion {
8+
public final fun getFactory ()Lcom/squareup/workflow/ui/ViewFactory;
9+
public final fun getNoopRendering ()Lcom/squareup/workflow/compose/ComposeRendering;
10+
}
11+
12+
public abstract class com/squareup/workflow/compose/ComposeWorkflow : com/squareup/workflow/Workflow {
13+
public fun <init> ()V
14+
public fun asStatefulWorkflow ()Lcom/squareup/workflow/StatefulWorkflow;
15+
public abstract fun render (Ljava/lang/Object;Lcom/squareup/workflow/Sink;Lcom/squareup/workflow/ui/ViewEnvironment;Landroidx/compose/Composer;)V
16+
}
17+
118
public final class com/squareup/workflow/ui/compose/ComposeViewFactory : com/squareup/workflow/ui/ViewFactory {
219
public fun <init> (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function3;)V
320
public fun buildView (Ljava/lang/Object;Lcom/squareup/workflow/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2020 Square Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
@file:Suppress("RemoveEmptyParenthesesFromAnnotationEntry")
17+
18+
package com.squareup.workflow.compose
19+
20+
import androidx.compose.Composable
21+
import com.squareup.workflow.compose.ComposeRendering.Companion.Factory
22+
import com.squareup.workflow.compose.ComposeRendering.Companion.NoopRendering
23+
import com.squareup.workflow.ui.ViewEnvironment
24+
import com.squareup.workflow.ui.ViewFactory
25+
import com.squareup.workflow.ui.compose.bindCompose
26+
27+
/**
28+
* A workflow rendering that renders itself using a [Composable] function.
29+
*
30+
* This is the rendering type of [ComposeWorkflow]. To stub out [ComposeWorkflow]s in `RenderTester`
31+
* tests, use [NoopRendering].
32+
*
33+
* To use this type, make sure your `ViewRegistry` registers [Factory].
34+
*/
35+
class ComposeRendering internal constructor(
36+
internal val render: @Composable() (ViewEnvironment) -> Unit
37+
) {
38+
companion object {
39+
/**
40+
* A [ViewFactory] that renders a [ComposeRendering].
41+
*/
42+
val Factory: ViewFactory<ComposeRendering> = bindCompose { rendering, environment ->
43+
rendering.render(environment)
44+
}
45+
46+
/**
47+
* A [ComposeRendering] that doesn't do anything. Useful for unit testing.
48+
*/
49+
val NoopRendering = ComposeRendering {}
50+
}
51+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2020 Square Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.squareup.workflow.compose
17+
18+
import androidx.compose.Composable
19+
import com.squareup.workflow.Sink
20+
import com.squareup.workflow.StatefulWorkflow
21+
import com.squareup.workflow.Workflow
22+
import com.squareup.workflow.ui.ViewEnvironment
23+
24+
/**
25+
* A stateless [Workflow][com.squareup.workflow.Workflow] that [renders][render] itself as
26+
* [Composable] function. Effectively defines an inline
27+
* [bindCompose][com.squareup.workflow.ui.compose.bindCompose].
28+
*
29+
* This workflow does not have access to a [RenderContext][com.squareup.workflow.RenderContext]
30+
* since render contexts are only valid during render passes, and this workflow's [render] method
31+
* is invoked after the render pass, when view bindings are being shown.
32+
*
33+
* While this workflow is "stateless" in the usual workflow sense (it doesn't have a `StateT` type),
34+
* since [render] is a Composable function, it can use all the usual Compose facilities for state
35+
* management.
36+
*/
37+
abstract class ComposeWorkflow<in PropsT, out OutputT : Any> :
38+
Workflow<PropsT, OutputT, ComposeRendering> {
39+
40+
/**
41+
* Renders [props] using Compose. This function will be called to update the UI whenever the
42+
* [props] or [viewEnvironment] changes.
43+
*
44+
* @param props The data to render.
45+
* @param outputSink A [Sink] that can be used from UI event handlers to send outputs to this
46+
* workflow's parent.
47+
* @param viewEnvironment The [ViewEnvironment] passed down through the `ViewBinding` pipeline.
48+
*/
49+
@Composable abstract fun render(
50+
props: PropsT,
51+
outputSink: Sink<OutputT>,
52+
viewEnvironment: ViewEnvironment
53+
)
54+
55+
override fun asStatefulWorkflow(): StatefulWorkflow<PropsT, *, OutputT, ComposeRendering> =
56+
ComposeWorkflowImpl(this)
57+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright 2020 Square Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.squareup.workflow.compose
17+
18+
import androidx.compose.MutableState
19+
import androidx.compose.StructurallyEqual
20+
import androidx.compose.mutableStateOf
21+
import com.squareup.workflow.RenderContext
22+
import com.squareup.workflow.Sink
23+
import com.squareup.workflow.Snapshot
24+
import com.squareup.workflow.StatefulWorkflow
25+
import com.squareup.workflow.action
26+
import com.squareup.workflow.compose.ComposeWorkflowImpl.State
27+
import com.squareup.workflow.contraMap
28+
29+
internal class ComposeWorkflowImpl<PropsT, OutputT : Any>(
30+
private val workflow: ComposeWorkflow<PropsT, OutputT>
31+
) : StatefulWorkflow<PropsT, State<PropsT, OutputT>, OutputT, ComposeRendering>() {
32+
33+
// This doesn't need to be a @Model, it only gets set once, before the composable ever runs.
34+
class SinkHolder<OutputT>(var sink: Sink<OutputT>? = null)
35+
36+
data class State<PropsT, OutputT>(
37+
val propsHolder: MutableState<PropsT>,
38+
val sinkHolder: SinkHolder<OutputT>,
39+
val rendering: ComposeRendering
40+
)
41+
42+
override fun initialState(
43+
props: PropsT,
44+
snapshot: Snapshot?
45+
): State<PropsT, OutputT> {
46+
val propsHolder = mutableStateOf(props, areEquivalent = StructurallyEqual)
47+
val sinkHolder = SinkHolder<OutputT>()
48+
return State(propsHolder, sinkHolder, ComposeRendering { environment ->
49+
// The sink will get set on the first render pass, so it should never be null.
50+
val sink = sinkHolder.sink!!
51+
// Important: Use the props from the MutableState, _not_ the one passed into render.
52+
workflow.render(propsHolder.value, sink, environment)
53+
})
54+
}
55+
56+
override fun onPropsChanged(
57+
old: PropsT,
58+
new: PropsT,
59+
state: State<PropsT, OutputT>
60+
): State<PropsT, OutputT> {
61+
state.propsHolder.value = new
62+
return state
63+
}
64+
65+
override fun render(
66+
props: PropsT,
67+
state: State<PropsT, OutputT>,
68+
context: RenderContext<State<PropsT, OutputT>, OutputT>
69+
): ComposeRendering {
70+
// The first render pass needs to cache the sink. The sink is reusable, so we can just pass the
71+
// same one every time.
72+
if (state.sinkHolder.sink == null) {
73+
state.sinkHolder.sink = context.actionSink.contraMap(::forwardOutput)
74+
}
75+
76+
// onPropsChanged will ensure the rendering is re-composed when the props changes.
77+
return state.rendering
78+
}
79+
80+
// Compiler bug doesn't let us call Snapshot.EMPTY.
81+
override fun snapshotState(state: State<PropsT, OutputT>): Snapshot = Snapshot.of("")
82+
83+
private fun forwardOutput(output: OutputT) = action { setOutput(output) }
84+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
2+
3+
/*
4+
* Copyright 2020 Square Inc.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
plugins {
19+
id("com.android.application")
20+
kotlin("android")
21+
}
22+
23+
apply(from = rootProject.file(".buildscript/configure-android-defaults.gradle"))
24+
apply(from = rootProject.file(".buildscript/android-sample-app.gradle"))
25+
26+
android {
27+
defaultConfig {
28+
applicationId = "com.squareup.sample.hellocomposerendering"
29+
30+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
31+
}
32+
}
33+
34+
apply(from = rootProject.file(".buildscript/configure-compose.gradle"))
35+
tasks.withType<KotlinCompile> {
36+
kotlinOptions.apiVersion = "1.3"
37+
}
38+
39+
dependencies {
40+
implementation(project(":core-compose"))
41+
implementation(Dependencies.AndroidX.appcompat)
42+
implementation(Dependencies.Compose.foundation)
43+
implementation(Dependencies.Compose.layout)
44+
implementation(Dependencies.Compose.material)
45+
implementation(Dependencies.Workflow.core)
46+
implementation(Dependencies.Workflow.runtime)
47+
48+
androidTestImplementation(Dependencies.Compose.test)
49+
androidTestImplementation(Dependencies.Test.AndroidX.junitExt)
50+
androidTestImplementation(Dependencies.Test.junit)
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2020 Square Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.squareup.sample.hellocomposerendering
17+
18+
import androidx.test.ext.junit.runners.AndroidJUnit4
19+
import androidx.ui.test.android.AndroidComposeTestRule
20+
import androidx.ui.test.assertIsDisplayed
21+
import androidx.ui.test.doClick
22+
import androidx.ui.test.findByText
23+
import org.junit.Rule
24+
import org.junit.Test
25+
import org.junit.runner.RunWith
26+
27+
@RunWith(AndroidJUnit4::class)
28+
class HelloComposeRenderingTest {
29+
30+
// Launches the activity.
31+
@Rule @JvmField val composeRule = AndroidComposeTestRule<HelloComposeRenderingActivity>()
32+
33+
@Test fun togglesBetweenStates() {
34+
findByText("Hello")
35+
.assertIsDisplayed()
36+
.doClick()
37+
findByText("Goodbye")
38+
.assertIsDisplayed()
39+
.doClick()
40+
findByText("Hello")
41+
.assertIsDisplayed()
42+
}
43+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?xml version="1.0" encoding="utf-8"?><!--
2+
~ Copyright 2020 Square Inc.
3+
~
4+
~ Licensed under the Apache License, Version 2.0 (the "License");
5+
~ you may not use this file except in compliance with the License.
6+
~ You may obtain a copy of the License at
7+
~
8+
~ http://www.apache.org/licenses/LICENSE-2.0
9+
~
10+
~ Unless required by applicable law or agreed to in writing, software
11+
~ distributed under the License is distributed on an "AS IS" BASIS,
12+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
~ See the License for the specific language governing permissions and
14+
~ limitations under the License.
15+
-->
16+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
17+
xmlns:tools="http://schemas.android.com/tools"
18+
package="com.squareup.sample.hellocomposerendering">
19+
20+
<application
21+
android:allowBackup="false"
22+
android:label="@string/app_name"
23+
android:theme="@style/AppTheme"
24+
tools:ignore="GoogleAppIndexingWarning,MissingApplicationIcon">
25+
26+
<activity android:name=".HelloComposeRenderingActivity">
27+
28+
<intent-filter>
29+
<action android:name="android.intent.action.MAIN" />
30+
<category android:name="android.intent.category.LAUNCHER" />
31+
</intent-filter>
32+
33+
</activity>
34+
35+
</application>
36+
</manifest>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2020 Square Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.squareup.sample.hellocomposerendering
17+
18+
import android.os.Bundle
19+
import androidx.appcompat.app.AppCompatActivity
20+
import com.squareup.workflow.compose.ComposeRendering
21+
import com.squareup.workflow.diagnostic.SimpleLoggingDiagnosticListener
22+
import com.squareup.workflow.ui.ViewRegistry
23+
import com.squareup.workflow.ui.WorkflowRunner
24+
import com.squareup.workflow.ui.setContentWorkflow
25+
26+
private val viewRegistry = ViewRegistry(ComposeRendering.Factory)
27+
28+
class HelloComposeRenderingActivity : AppCompatActivity() {
29+
override fun onCreate(savedInstanceState: Bundle?) {
30+
super.onCreate(savedInstanceState)
31+
setContentWorkflow(viewRegistry) {
32+
WorkflowRunner.Config(
33+
HelloWorkflow,
34+
diagnosticListener = SimpleLoggingDiagnosticListener()
35+
)
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)