Skip to content

Commit d085cbf

Browse files
Merge pull request #7 from square/zachklipp/compose-nesting
Add the ability to display nested renderings with bindCompose.
2 parents 46c8d87 + 430d476 commit d085cbf

File tree

18 files changed

+744
-8
lines changed

18 files changed

+744
-8
lines changed

build.gradle.kts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
import org.jetbrains.dokka.gradle.DokkaTask
1716
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
1817
import org.jlleitschuh.gradle.ktlint.KtlintExtension
1918
import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
@@ -38,6 +37,9 @@ buildscript {
3837
}
3938
}
4039

40+
// See https://stackoverflow.com/questions/25324880/detect-ide-environment-with-gradle
41+
val isRunningFromIde get() = project.properties["android.injected.invoked.from.ide"] == "true"
42+
4143
subprojects {
4244
repositories {
4345
google()
@@ -60,7 +62,10 @@ subprojects {
6062

6163
tasks.withType<KotlinCompile>() {
6264
kotlinOptions {
63-
kotlinOptions.allWarningsAsErrors = true
65+
// Allow warnings when running from IDE, makes it easier to experiment.
66+
if (!isRunningFromIde) {
67+
allWarningsAsErrors = true
68+
}
6469

6570
jvmTarget = "1.8"
6671

core-compose/api/core-compose.api

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ public final class com/squareup/workflow/ui/compose/ComposeViewFactory : com/squ
2121
public fun getType ()Lkotlin/reflect/KClass;
2222
}
2323

24+
public final class com/squareup/workflow/ui/compose/ViewEnvironmentsKt {
25+
public static final fun showRendering (Lcom/squareup/workflow/ui/ViewEnvironment;Ljava/lang/Object;Landroidx/ui/core/Modifier;Landroidx/compose/Composer;)V
26+
public static synthetic fun showRendering$default (Lcom/squareup/workflow/ui/ViewEnvironment;Ljava/lang/Object;Landroidx/ui/core/Modifier;Landroidx/compose/Composer;ILjava/lang/Object;)V
27+
}
28+
2429
public final class com/squareup/workflow/ui/core/compose/BuildConfig {
2530
public static final field BUILD_TYPE Ljava/lang/String;
2631
public static final field DEBUG Z

core-compose/src/main/java/com/squareup/workflow/ui/compose/ComposeViewFactory.kt

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,30 @@ import kotlin.reflect.KClass
5656
*
5757
* val viewRegistry = ViewRegistry(FooBinding, …)
5858
* ```
59+
*
60+
* ## Nesting child renderings
61+
*
62+
* Workflows can render other workflows, and renderings from one workflow can contain renderings
63+
* from other workflows. These renderings may all be bound to their own [ViewFactory]s. Regular
64+
* [ViewFactory]s and `LayoutRunner`s use
65+
* [WorkflowViewStub][com.squareup.workflow.ui.WorkflowViewStub] to recursively show nested
66+
* renderings using the [ViewRegistry][com.squareup.workflow.ui.ViewRegistry].
67+
*
68+
* View factories defined using this function may also show nested renderings. Doing so is as simple
69+
* as calling [ViewEnvironment.showRendering] and passing in the nested rendering. See the kdoc on
70+
* that function for an example.
5971
*/
6072
inline fun <reified RenderingT : Any> bindCompose(
61-
noinline showRendering: @Composable() (RenderingT, ViewEnvironment) -> Unit
62-
): ViewFactory<RenderingT> = ComposeViewFactory(RenderingT::class) { rendering, environment ->
63-
showRendering(rendering, environment)
64-
}
73+
noinline showRendering: @Composable() (
74+
rendering: RenderingT,
75+
environment: ViewEnvironment
76+
) -> Unit
77+
): ViewFactory<RenderingT> = ComposeViewFactory(RenderingT::class, showRendering)
6578

6679
@PublishedApi
6780
internal class ComposeViewFactory<RenderingT : Any>(
6881
override val type: KClass<RenderingT>,
69-
private val showRendering: @Composable() (RenderingT, ViewEnvironment) -> Unit
82+
internal val showRendering: @Composable() (RenderingT, ViewEnvironment) -> Unit
7083
) : ViewFactory<RenderingT> {
7184

7285
override fun buildView(
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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.ui.compose
17+
18+
import androidx.compose.Composable
19+
import androidx.compose.remember
20+
import androidx.ui.core.Modifier
21+
import com.squareup.workflow.ui.ViewEnvironment
22+
import com.squareup.workflow.ui.ViewRegistry
23+
24+
/**
25+
* Renders [rendering] into the composition using this [ViewEnvironment]'s
26+
* [ViewRegistry][com.squareup.workflow.ui.ViewRegistry] to generate the view.
27+
*
28+
* This function fulfills a similar role as
29+
* [WorkflowViewStub][com.squareup.workflow.ui.WorkflowViewStub], but is much more convenient to use
30+
* from Composable functions.
31+
*
32+
* ## Example
33+
*
34+
* ```
35+
* data class FramedRendering(
36+
* val borderColor: Color,
37+
* val child: Any
38+
* )
39+
*
40+
* val FramedContainerViewFactory = bindCompose<FramedRendering> { rendering, environment ->
41+
* Surface(border = Border(rendering.borderColor, 8.dp)) {
42+
* environment.showRendering(rendering.child)
43+
* }
44+
* }
45+
* ```
46+
*
47+
* @param rendering The workflow rendering to display. May be of any type for which a
48+
* [ViewFactory][com.squareup.workflow.ui.ViewFactory] has been registered in this
49+
* environment's [ViewRegistry].
50+
* @param modifier A [Modifier] that will be applied to composable used to show [rendering].
51+
*
52+
* @throws IllegalArgumentException if no factory can be found for [rendering]'s type.
53+
*/
54+
@Composable fun ViewEnvironment.showRendering(
55+
rendering: Any,
56+
modifier: Modifier = Modifier
57+
) {
58+
val viewRegistry = remember(this) { this[ViewRegistry] }
59+
viewRegistry.showRendering(rendering, this, modifier)
60+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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.ui.compose
17+
18+
import android.content.Context
19+
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
20+
import android.widget.FrameLayout
21+
import androidx.compose.Composable
22+
import androidx.ui.core.Modifier
23+
import androidx.ui.foundation.Box
24+
import com.squareup.workflow.ui.ViewEnvironment
25+
import com.squareup.workflow.ui.ViewFactory
26+
import com.squareup.workflow.ui.WorkflowViewStub
27+
import com.squareup.workflow.ui.compose.ComposableViewStubWrapper.Update
28+
29+
/**
30+
* Renders [rendering] into the composition using the `ViewRegistry` from the [ViewEnvironment] to
31+
* determine how to draw it.
32+
*
33+
* To display a nested rendering from a [Composable view binding][bindCompose], use
34+
* [ViewEnvironment.showRendering].
35+
*
36+
* @see ViewEnvironment.showRendering
37+
* @see com.squareup.workflow.ui.ViewRegistry.showRendering
38+
*/
39+
// Bug: IR compiler pukes on ViewFactory<RenderingT> here.
40+
@Composable internal fun <RenderingT : Any> ViewFactory<Any>.showRendering(
41+
rendering: RenderingT,
42+
viewEnvironment: ViewEnvironment,
43+
modifier: Modifier = Modifier
44+
) {
45+
Box(modifier = modifier) {
46+
// Fast path: If the child binding is also a Composable, we don't need to go through the legacy
47+
// view system and can just invoke the binding's composable function directly.
48+
if (this is ComposeViewFactory) {
49+
showRendering(rendering, viewEnvironment)
50+
} else {
51+
// IntelliJ currently complains very loudly about this function call, but it actually compiles.
52+
// The IDE tooling isn't currently able to recognize that the Compose compiler accepts this code.
53+
ComposableViewStubWrapper(update = Update(rendering, viewEnvironment))
54+
}
55+
}
56+
}
57+
58+
/**
59+
* Wraps a [WorkflowViewStub] with an API that is more Compose-friendly.
60+
*
61+
* In particular, Compose will only generate `Emittable`s for views with a single constructor
62+
* that takes a [Context].
63+
*
64+
* See [this slack message](https://kotlinlang.slack.com/archives/CJLTWPH7S/p1576264533012000?thread_ts=1576262311.008800&cid=CJLTWPH7S).
65+
*/
66+
private class ComposableViewStubWrapper(context: Context) : FrameLayout(context) {
67+
68+
data class Update(
69+
val rendering: Any,
70+
val viewEnvironment: ViewEnvironment
71+
)
72+
73+
private val viewStub = WorkflowViewStub(context)
74+
75+
init {
76+
addView(viewStub)
77+
}
78+
79+
// Compose turns this into a parameter when you invoke this class as a Composable.
80+
fun setUpdate(update: Update) {
81+
viewStub.update(update.rendering, update.viewEnvironment)
82+
}
83+
84+
override fun getLayoutParams(): LayoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT)
85+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.ui.compose
17+
18+
import androidx.compose.Composable
19+
import androidx.compose.remember
20+
import androidx.ui.core.Modifier
21+
import com.squareup.workflow.ui.ViewEnvironment
22+
import com.squareup.workflow.ui.ViewFactory
23+
import com.squareup.workflow.ui.ViewRegistry
24+
25+
/**
26+
* Renders [rendering] into the composition using this [ViewRegistry] to determine how to draw it.
27+
*
28+
* To display a nested rendering from a [Composable view binding][bindCompose], use
29+
* [ViewEnvironment.showRendering].
30+
*
31+
* @see ViewEnvironment.showRendering
32+
* @see ViewFactory.showRendering
33+
*/
34+
@Composable internal fun ViewRegistry.showRendering(
35+
rendering: Any,
36+
hints: ViewEnvironment,
37+
modifier: Modifier = Modifier
38+
) {
39+
val renderingType = rendering::class
40+
val viewFactory = remember(renderingType) { getFactoryFor(renderingType) }
41+
viewFactory.showRendering(rendering, hints, modifier)
42+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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/android-sample-app.gradle"))
24+
apply(from = rootProject.file(".buildscript/android-ui-tests.gradle"))
25+
26+
android {
27+
defaultConfig {
28+
applicationId = "com.squareup.sample.nestedrenderings"
29+
}
30+
}
31+
32+
apply(from = rootProject.file(".buildscript/configure-compose.gradle"))
33+
tasks.withType<KotlinCompile> {
34+
kotlinOptions.apiVersion = "1.3"
35+
}
36+
37+
dependencies {
38+
implementation(project(":core-compose"))
39+
implementation(Dependencies.AndroidX.appcompat)
40+
implementation(Dependencies.Compose.foundation)
41+
implementation(Dependencies.Compose.layout)
42+
implementation(Dependencies.Compose.material)
43+
implementation(Dependencies.Workflow.UI.coreAndroid)
44+
45+
androidTestImplementation(Dependencies.Compose.test)
46+
androidTestImplementation(Dependencies.Test.junit)
47+
androidTestImplementation(Dependencies.Test.truth)
48+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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.nestedrenderings
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.findAllByText
23+
import androidx.ui.test.findByText
24+
import com.google.common.truth.Truth.assertThat
25+
import org.junit.Rule
26+
import org.junit.Test
27+
import org.junit.runner.RunWith
28+
29+
private const val ADD_BUTTON_TEXT = "Add Child"
30+
31+
@RunWith(AndroidJUnit4::class)
32+
class NestedRenderingsTest {
33+
34+
// Launches the activity.
35+
@Rule @JvmField val composeRule = AndroidComposeTestRule<NestedRenderingsActivity>()
36+
37+
@Test fun childrenAreAddedAndRemoved() {
38+
val resetButton = findByText("Reset")
39+
40+
findByText(ADD_BUTTON_TEXT)
41+
.assertIsDisplayed()
42+
.doClick()
43+
44+
findAllByText(ADD_BUTTON_TEXT)
45+
.also { addButtons ->
46+
assertThat(addButtons).hasSize(2)
47+
addButtons.forEach { it.doClick() }
48+
}
49+
50+
findAllByText(ADD_BUTTON_TEXT)
51+
.also { addButtons ->
52+
assertThat(addButtons).hasSize(4)
53+
}
54+
55+
resetButton.doClick()
56+
assertThat(findAllByText(ADD_BUTTON_TEXT)).hasSize(1)
57+
}
58+
}

0 commit comments

Comments
 (0)