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

Commit 0ebc2ba

Browse files
Cleanup how ComposableViewFactory handles initial rendering.
Duplicate of square/workflow#1146
1 parent 2ecfa22 commit 0ebc2ba

File tree

1 file changed

+38
-11
lines changed

1 file changed

+38
-11
lines changed

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

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import android.widget.FrameLayout
2525
import androidx.compose.Composable
2626
import androidx.compose.Recomposer
2727
import androidx.compose.StructurallyEqual
28+
import androidx.compose.frames.commit
29+
import androidx.compose.frames.inFrame
30+
import androidx.compose.frames.open
2831
import androidx.compose.mutableStateOf
2932
import androidx.ui.core.setContent
3033
import com.squareup.workflow.ui.ViewEnvironment
@@ -78,26 +81,50 @@ internal class ComposeViewFactory<RenderingT : Any>(
7881
// Composable function, so we need to use ViewGroup.setContent.
7982
val composeContainer = FrameLayout(contextForNewView)
8083

81-
// This model will be used to feed state updates into the composition.
84+
// Create a single MutableState to feed state updates into the composition.
85+
// We could also have two separate MutableStates, but using a Pair both makes it clear and
86+
// enforces that both values are always updated together.
8287
val renderState = mutableStateOf<Pair<RenderingT, ViewEnvironment>?>(
88+
// This will be updated immediately by bindShowRendering below.
8389
value = null,
8490
areEquivalent = StructurallyEqual
8591
)
8692

87-
// Entry point to the composition.
93+
// If this is the first ever compose call in this process, we need to manually open a frame
94+
// before updating the MutableState.
95+
ensureComposeFrame {
96+
// Update the state whenever a new rendering is emitted.
97+
composeContainer.bindShowRendering(
98+
initialRendering,
99+
initialViewEnvironment
100+
) { rendering, environment ->
101+
// This will be called synchronously before bindShowRendering returns.
102+
renderState.value = Pair(rendering, environment)
103+
}
104+
}
105+
106+
// Entry point to the world of Compose.
88107
composeContainer.setContent(Recomposer.current()) {
89-
// Don't compose anything until we have the first value (which should happen in the initial
90-
// frame).
91-
val (rendering, environment) = renderState.value ?: return@setContent
108+
val (rendering, environment) = renderState.value!!
92109
showRendering(rendering, environment)
93110
}
94111

95-
composeContainer.bindShowRendering(
96-
initialRendering,
97-
initialViewEnvironment
98-
) { rendering, environment ->
99-
renderState.value = Pair(rendering, environment)
100-
}
101112
return composeContainer
102113
}
114+
115+
/**
116+
* Currently, if we try setting a Model's value before any other Compose code has started running
117+
* (e.g. by calling setContent), there will not be a frame open so the model set will throw. This
118+
* function detects that situation and creates a temporary frame if necessary. To run [block] in.
119+
* This should be removed once Compose gets support for global frames.
120+
*/
121+
private fun ensureComposeFrame(block: () -> Unit) {
122+
if (!inFrame) {
123+
val frame = open(readOnly = false)
124+
block()
125+
commit(frame)
126+
} else {
127+
block()
128+
}
129+
}
103130
}

0 commit comments

Comments
 (0)