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

Commit 87afb4f

Browse files
wip
1 parent 8886833 commit 87afb4f

File tree

11 files changed

+396
-62
lines changed

11 files changed

+396
-62
lines changed

core-compose/src/androidTest/java/com/squareup/workflow/ui/compose/ComposeViewFactoryTest.kt

+54-3
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,24 @@ package com.squareup.workflow.ui.compose
1717

1818
import android.content.Context
1919
import android.widget.FrameLayout
20+
import android.widget.TextView
2021
import androidx.compose.FrameManager
22+
import androidx.compose.invalidate
2123
import androidx.compose.mutableStateOf
24+
import androidx.compose.remember
2225
import androidx.test.ext.junit.runners.AndroidJUnit4
26+
import androidx.ui.core.ContextAmbient
2327
import androidx.ui.foundation.Text
2428
import androidx.ui.layout.Column
2529
import androidx.ui.test.assertIsDisplayed
2630
import androidx.ui.test.createComposeRule
2731
import androidx.ui.test.findByText
32+
import androidx.ui.test.runOnIdleCompose
33+
import androidx.ui.viewinterop.AndroidView
2834
import com.squareup.workflow.ui.ViewEnvironment
2935
import com.squareup.workflow.ui.ViewRegistry
3036
import com.squareup.workflow.ui.WorkflowViewStub
37+
import com.squareup.workflow.ui.compose.internal.withParentComposition
3138
import org.junit.Rule
3239
import org.junit.Test
3340
import org.junit.runner.RunWith
@@ -50,21 +57,65 @@ class ComposeViewFactoryTest {
5057
composeRule.setContent {
5158
// This is valid Compose code, but the IDE doesn't know that yet so it will show an
5259
// unsuppressable error.
53-
RootView(viewEnvironment = viewEnvironment)
60+
RootView(update = Pair(TestRendering("two"), viewEnvironment))
5461
}
5562

5663
findByText("one\ntwo").assertIsDisplayed()
64+
5765
FrameManager.framed {
5866
wrapperText.value = "ENO"
5967
}
6068
findByText("ENO\ntwo").assertIsDisplayed()
6169
}
6270

71+
@Test fun showsChangesUnderWorkflowViewStub_withoutSubcomposition() {
72+
val childText = mutableStateOf("one")
73+
val viewEnvironment = ViewEnvironment(ViewRegistry(TestFactory))
74+
75+
composeRule.setContent {
76+
// This is valid Compose code, but the IDE doesn't know that yet so it will show an
77+
// unsuppressable error.
78+
RootView(update = Pair(TestRendering(childText.value), viewEnvironment))
79+
}
80+
81+
findByText("one").assertIsDisplayed()
82+
83+
FrameManager.framed {
84+
childText.value = "ENO"
85+
}
86+
findByText("ENO").assertIsDisplayed()
87+
}
88+
89+
@Test fun showsChangesUnderWorkflowViewStub_withSubcomposition() {
90+
val childText = mutableStateOf("one")
91+
val viewEnvironment = ViewEnvironment(ViewRegistry(TestFactory))
92+
93+
composeRule.setContent {
94+
// This is valid Compose code, but the IDE doesn't know that yet so it will show an
95+
// unsuppressable error.
96+
RootView(update = Pair(TestRendering(childText.value), viewEnvironment))
97+
// Text(childText.value)
98+
}
99+
100+
findByText("one").assertIsDisplayed()
101+
102+
FrameManager.framed {
103+
childText.value = "ENO"
104+
}
105+
findByText("ENO").assertIsDisplayed()
106+
107+
// println("OMG invalidating…")
108+
// runOnIdleCompose {
109+
// invalidater()
110+
// }
111+
// println("OMG invalidated")
112+
}
113+
63114
private class RootView(context: Context) : FrameLayout(context) {
64115
private val stub = WorkflowViewStub(context).also(::addView)
65116

66-
fun setViewEnvironment(viewEnvironment: ViewEnvironment) {
67-
stub.update(TestRendering("two"), viewEnvironment)
117+
fun setUpdate(update: Pair<TestRendering, ViewEnvironment>) {
118+
stub.update(update.first, update.second)
68119
}
69120
}
70121

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
@file:Suppress("RemoveEmptyParenthesesFromAnnotationEntry")
2+
3+
package com.squareup.workflow.ui.compose
4+
5+
import android.content.Context
6+
import androidx.compose.Composable
7+
import androidx.compose.Composition
8+
import androidx.compose.CompositionReference
9+
import androidx.compose.FrameManager
10+
import androidx.compose.Recomposer
11+
import androidx.compose.compositionReference
12+
import androidx.compose.currentComposer
13+
import androidx.compose.onDispose
14+
import androidx.compose.remember
15+
import androidx.ui.core.Constraints
16+
import androidx.ui.core.ContextAmbient
17+
import androidx.ui.core.LayoutDirection
18+
import androidx.ui.core.LayoutNode
19+
import androidx.ui.core.Measurable
20+
import androidx.ui.core.MeasureScope
21+
import androidx.ui.core.Modifier
22+
import androidx.ui.core.Ref
23+
import androidx.ui.core.WithConstraintsScope
24+
import androidx.ui.core.materialize
25+
import androidx.ui.core.subcomposeInto
26+
import androidx.ui.unit.Density
27+
import androidx.ui.unit.Dp
28+
import androidx.ui.unit.IntPx
29+
import androidx.ui.unit.ipx
30+
import androidx.ui.unit.max
31+
import androidx.ui.unit.min
32+
33+
@Composable
34+
fun WithConstraints(
35+
modifier: Modifier = Modifier,
36+
children: @Composable() WithConstraintsScope.() -> Unit
37+
) {
38+
val state = remember { WithConstrainsState() }
39+
state.children = children
40+
state.context = ContextAmbient.current
41+
state.recomposer = currentComposer.recomposer
42+
if (state.compositionRef == null) {
43+
state.compositionRef = compositionReference()
44+
}
45+
// if this code was executed subcomposition must be triggered as well
46+
state.forceRecompose = true
47+
48+
LayoutNode(
49+
modifier = currentComposer.materialize(modifier),
50+
ref = state.nodeRef,
51+
measureBlocks = state.measureBlocks
52+
)
53+
54+
// if LayoutNode scheduled the remeasuring no further steps are needed - subcomposition
55+
// will happen later on the measuring stage. otherwise we can assume the LayoutNode
56+
// already holds the final Constraints and we should subcompose straight away.
57+
// if owner is null this means we are not yet attached. once attached the remeasuring
58+
// will be scheduled which would cause subcomposition
59+
val layoutNode = state.nodeRef.value!!
60+
if (!layoutNode.needsRemeasure && layoutNode.owner != null) {
61+
// state.subcompose()
62+
state.forceRecompose = false
63+
}
64+
onDispose {
65+
state.composition?.dispose()
66+
}
67+
}
68+
69+
private class WithConstrainsState {
70+
lateinit var recomposer: Recomposer
71+
var compositionRef: CompositionReference? = null
72+
lateinit var context: Context
73+
val nodeRef = Ref<LayoutNode>()
74+
var children: @Composable() WithConstraintsScope.() -> Unit = { }
75+
var forceRecompose = false
76+
var composition: Composition? = null
77+
78+
private var scope: WithConstraintsScope = WithConstraintsScopeImpl(
79+
Density(1f),
80+
Constraints.fixed(0.ipx, 0.ipx),
81+
LayoutDirection.Ltr
82+
)
83+
84+
val measureBlocks = object : LayoutNode.NoIntrinsicsMeasureBlocks(
85+
error = "Intrinsic measurements are not supported by WithConstraints"
86+
) {
87+
override fun measure(
88+
measureScope: MeasureScope,
89+
measurables: List<Measurable>,
90+
constraints: Constraints,
91+
layoutDirection: LayoutDirection
92+
): MeasureScope.MeasureResult {
93+
val root = nodeRef.value!!
94+
if (scope.constraints != constraints ||
95+
scope.layoutDirection != measureScope.layoutDirection ||
96+
forceRecompose
97+
) {
98+
scope = WithConstraintsScopeImpl(measureScope, constraints, layoutDirection)
99+
root.ignoreModelReads { subcompose() }
100+
// subcompose()
101+
// if there were models created and read inside this subcomposition
102+
// and we are going to modify this models within the same frame
103+
// the composables which read this model will not be recomposed.
104+
// to make this possible we should switch to the next frame.
105+
FrameManager.nextFrame()
106+
}
107+
108+
// Measure the obtained children and compute our size.
109+
val layoutChildren = root.layoutChildren
110+
var maxWidth: IntPx = constraints.minWidth
111+
var maxHeight: IntPx = constraints.minHeight
112+
layoutChildren.forEach {
113+
it.measure(constraints, layoutDirection)
114+
maxWidth = max(maxWidth, it.width)
115+
maxHeight = max(maxHeight, it.height)
116+
}
117+
maxWidth = min(maxWidth, constraints.maxWidth)
118+
maxHeight = min(maxHeight, constraints.maxHeight)
119+
120+
return measureScope.layout(maxWidth, maxHeight) {
121+
layoutChildren.forEach { it.place(IntPx.Zero, IntPx.Zero) }
122+
}
123+
}
124+
}
125+
126+
fun subcompose() {
127+
// TODO(b/150390669): Review use of @Untracked
128+
composition =
129+
subcomposeInto(context, nodeRef.value!!, recomposer, compositionRef) {
130+
scope.children()
131+
}
132+
forceRecompose = false
133+
}
134+
135+
private data class WithConstraintsScopeImpl(
136+
private val density: Density,
137+
override val constraints: Constraints,
138+
override val layoutDirection: LayoutDirection
139+
) : WithConstraintsScope {
140+
override val minWidth: Dp
141+
get() = with(density) { constraints.minWidth.toDp() }
142+
override val maxWidth: Dp
143+
get() = with(density) { constraints.maxWidth.toDp() }
144+
override val minHeight: Dp
145+
get() = with(density) { constraints.minHeight.toDp() }
146+
override val maxHeight: Dp
147+
get() = with(density) { constraints.maxHeight.toDp() }
148+
}
149+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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.FrameManager
19+
import androidx.compose.getValue
20+
import androidx.compose.mutableStateOf
21+
import androidx.compose.setValue
22+
import androidx.test.ext.junit.runners.AndroidJUnit4
23+
import androidx.ui.foundation.Box
24+
import androidx.ui.foundation.Text
25+
import androidx.ui.layout.Column
26+
import androidx.ui.test.createComposeRule
27+
import androidx.ui.test.runOnIdleCompose
28+
import androidx.ui.unit.dp
29+
import org.junit.Rule
30+
import org.junit.Test
31+
import org.junit.runner.RunWith
32+
33+
@RunWith(AndroidJUnit4::class)
34+
class WithConstraintsTest {
35+
36+
@Rule @JvmField val composeRule = createComposeRule()
37+
38+
@Test fun withConstraintsTest() {
39+
var padding by mutableStateOf(10.dp)
40+
41+
composeRule.setContent {
42+
// Box(padding = padding.value.dp) {
43+
WithConstraints {
44+
Column {
45+
Text("padding= ${padding.value.dp}")
46+
Text("constraints = $constraints")
47+
Text("layoutDirection = $layoutDirection")
48+
}
49+
// padding = 15.dp
50+
// }
51+
}
52+
}
53+
54+
Thread.sleep(1000)
55+
runOnIdleCompose {
56+
padding = 5.dp
57+
}
58+
59+
Thread.sleep(10_000)
60+
}
61+
}

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

+30-14
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ import android.view.ViewGroup
2424
import android.widget.FrameLayout
2525
import androidx.compose.Composable
2626
import androidx.compose.FrameManager
27-
import androidx.compose.StructurallyEqual
28-
import androidx.compose.mutableStateOf
27+
import androidx.ui.core.Ref
2928
import com.squareup.workflow.ui.ViewEnvironment
3029
import com.squareup.workflow.ui.ViewFactory
3130
import com.squareup.workflow.ui.bindShowRendering
31+
import com.squareup.workflow.ui.compose.internal.ParentComposition
3232
import com.squareup.workflow.ui.compose.internal.setOrContinueContent
3333
import kotlin.reflect.KClass
3434

@@ -105,31 +105,47 @@ internal class ComposeViewFactory<in RenderingT : Any>(
105105
// Create a single MutableState to feed state updates into the composition.
106106
// We could also have two separate MutableStates, but using a Pair both makes it clear and
107107
// enforces that both values are always updated together.
108-
val renderState = mutableStateOf<Pair<RenderingT, ViewEnvironment>?>(
109-
// This will be updated immediately by bindShowRendering below.
110-
value = null,
111-
areEquivalent = StructurallyEqual
112-
)
108+
// val renderState = mutableStateOf<Pair<RenderingT, ViewEnvironment>?>(
109+
// // This will be updated immediately by bindShowRendering below.
110+
// value = null,
111+
// areEquivalent = StructurallyEqual
112+
// )
113+
val renderState = Ref<Pair<RenderingT, ViewEnvironment>>()
113114

114115
// Models will throw if their properties are accessed when there is no frame open. Currently,
115116
// that will be the case if the model is accessed before any other Compose infrastructure has
116117
// ran, i.e. if this view factory is the first compose code to run in the app.
117118
// I believe that eventually there will be a global frame that will make this unnecessary.
118119
FrameManager.ensureStarted()
119120

121+
val parentComposition = initialViewEnvironment[ParentComposition]
122+
123+
fun subcompose() {
124+
// Entry point to the world of Compose.
125+
println("OMG setting content…")
126+
composeContainer.setOrContinueContent(parentComposition.reference) {
127+
println("OMG composing content…")
128+
val (rendering, environment) = renderState.value!!
129+
showRenderingWrappedWithRoot(rendering, environment)
130+
println("OMG finished composing content")
131+
}
132+
println("OMG finished setting content")
133+
FrameManager.nextFrame()
134+
}
135+
120136
// Update the state whenever a new rendering is emitted.
121137
composeContainer.bindShowRendering(
122138
initialRendering,
123139
initialViewEnvironment
124140
) { rendering, environment ->
125141
// This lambda will be executed synchronously before bindShowRendering returns.
126-
renderState.value = Pair(rendering, environment)
127-
}
128-
129-
// Entry point to the world of Compose.
130-
composeContainer.setOrContinueContent(initialViewEnvironment) {
131-
val (rendering, environment) = renderState.value!!
132-
showRenderingWrappedWithRoot(rendering, environment)
142+
val oldState = renderState.value
143+
val newState = Pair(rendering, environment)
144+
if (oldState != newState) {
145+
if (oldState != null) error("\nold: $oldState\nnew: $newState")
146+
renderState.value = newState
147+
subcompose()
148+
}
133149
}
134150

135151
return composeContainer

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ private fun doSetContent(
8282
// val original = compositionFor(context, owner.root, recomposer)
8383
val container = ownerRoot(owner)
8484
val original = compositionFor(
85-
container = container,
85+
container = androidOwnerView(owner) /*container*/,
8686
recomposer = recomposer,
8787
parent = parent,
8888
composerFactory = { slotTable, factoryRecomposer ->

0 commit comments

Comments
 (0)