You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingActivity.kt
Copy file name to clipboardExpand all lines: samples/compose-samples/src/main/java/com/squareup/sample/compose/inlinerendering/InlineRenderingWorkflow.kt
Copy file name to clipboardExpand all lines: workflow-ui/compose/README.md
+59-36Lines changed: 59 additions & 36 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -157,20 +157,30 @@ renderWorkflowIn(
157
157
158
158
#### Defining Compose-based UI factories
159
159
160
-
The most straightforward and common way to tie a `Screen` rendering type to a `@Composable` function is to implement [`ComposeScreen`](https://github.com/square/workflow-kotlin/blob/9bfd5119fabd0a3dfbc25bf7d93e52c7b31bb4cd/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeScreen.kt), the Compose-friendly analog to [`AndroidScreen`](https://github.com/square/workflow-kotlin/blob/v1.12.1-beta06/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidScreen.kt).
160
+
The most straightforward and common way to tie a `Screen` rendering type to a `@Composable` function
161
+
is to implement [`ComposeScreen`](https://github.com/square/workflow-kotlin/blob/9bfd5119fabd0a3dfbc25bf7d93e52c7b31bb4cd/workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ComposeScreen.kt), the Compose-friendly analog to [`AndroidScreen`](https://github.com/square/workflow-kotlin/blob/v1.12.1-beta06/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidScreen.kt).
`ComposeScreen` is a convenience that automates creating a `ScreenComposableFactory` implementation responsible for expressing, say, `HelloScreen` instances by calling `HelloScreen.Content()`.
@@ -186,10 +196,10 @@ data class ContactScreen(
186
196
): Screen
187
197
```
188
198
```kotlin
189
-
val contactUiFactory =ScreenComposableFactory<ContactScreen> { rendering, viewEnvironment->
199
+
val contactUiFactory =ScreenComposableFactory<ContactScreen> { screen->
190
200
Column {
191
-
Text(rendering.name)
192
-
Text(rendering.phoneNumber)
201
+
Text(screen.name)
202
+
Text(screen.phoneNumber)
193
203
}
194
204
}
195
205
@@ -215,7 +225,6 @@ Aka, `WorkflowViewStub` — Compose Edition! The idea of “view stub” is nons
215
225
```kotlin
216
226
@Composable funWorkflowRendering(
217
227
rendering:Screen,
218
-
viewEnvironment:ViewEnvironment,
219
228
modifier:Modifier = Modifier
220
229
)
221
230
```
@@ -230,13 +239,12 @@ data class ContactScreen(
230
239
valdetails:Screen
231
240
): Screen
232
241
233
-
val contactUiFactory =ScreenComposableFactory<ContactScreen> { rendering, viewEnvironment->
242
+
val contactUiFactory =ScreenComposableFactory<ContactScreen> { screen->
234
243
Column {
235
-
Text(rendering.name)
244
+
Text(screen.name)
236
245
237
246
WorkflowRendering(
238
-
rendering.details,
239
-
viewEnvironment,
247
+
screen.details,
240
248
Modifier.fillMaxWidth()
241
249
)
242
250
}
@@ -307,36 +315,51 @@ Here’s an example:
307
315
```kotlin
308
316
@Composable funApp(rootWorkflow:Workflow<...>) {
309
317
var rootProps by remember { mutableStateOf(...) }
310
-
val viewEnvironment =...
311
318
312
319
val rootRendering by rootWorkflow.renderAsState(
313
320
props = rootProps
314
321
) { output ->
315
322
handleOutput(output)
316
323
}
317
324
318
-
WorkflowRendering(rootRendering, viewEnvironment)
325
+
WorkflowRendering(rootRendering)
319
326
}
320
327
```
321
328
322
329
----
323
330
324
331
## Potential risk: Data model
325
332
326
-
Passing both the rendering and view environment down as parameters through the entire UI tree means that every time a rendering updates, we’ll recompose a lot of composables. This is how Workflow was designed, and because compose does some automatic deduping we’ll automatically avoid recomposing the leaves of the UI for a particular view factory unless the data for those bits of ui actually change. However, any time a leaf rendering changes, we’ll also be recomposing all the parent view factories just in order to propagate that leaf to its composable. That means we’re not able to take advantage of a lot of the other optimizations that compose tries to do both now and potentially in the future.
333
+
Passing both rendering down as a parameter through the entire UI tree means that
334
+
every time a rendering updates, we’ll recompose a lot of composables.
335
+
This is how Workflow was designed, and because compose does some automatic deduping
336
+
we’ll automatically avoid recomposing the leaves of the UI for a particular view factory
337
+
unless the data for those bits of ui actually change. However, any time a leaf rendering changes,
338
+
we’ll also be recomposing all the parent view factories just in order to propagate that leaf to its composable.
339
+
That means we’re not able to take advantage of a lot of the other optimizations that compose tries to do both now and potentially in the future.
327
340
328
341
In other words: “Workflow+views” < “Workflow+compose” < “data model designed specifically for compose + compose”.
329
342
330
-
It should be straightforward to address this issue for view environments - see the _Alternative design_ section for more information. However, it’s not clear how to solve this for renderings without abandoning our current rendering data model. Today, renderings are an immutable tree of immutable value types that require the entire tree to be recreated any time any single piece of data changes. The reason for this design is that it was the only way to safely propagate changes without adding a bunch of reactive streams to renderings everywhere. The key word in that sentence is “was”: Compose’s snapshot state system makes it possible to expose simple mutable properties and still get change notifications that will ensure that the UI stays up-to-date (For an example of how this system can be used to model complex state systems with dependencies, see [this blog post](https://dev.to/zachklipp/plumbing-data-with-derived-state-in-compose-53ka)).
331
-
332
-
Workflow could take advantage of this by allowing renderings to actually be mutable, so that when one Workflow deep in the tree wants to change something, it can do so independently and without requiring every rendering above it in the tree to also change. Making such a change to such a fundamental piece of Workflow design could have significant implications on other aspects of Workflow design, and doing so is very far outside the scope of this post.
333
-
334
-
We want to call this out because it seems like we’ll be losing out on one of Compose’s optimization tricks, but we’re not sure how much of a problem this will turn out to be in the real world. The only performance issues that we’re aware of that we’ve run into with Workflow UI so far are issues with recreating leaf views on every rerender, and that in particular _*is*_ something Compose will automatically win at, even with our current data model.
335
-
336
-
## Alternative design: Propagating `ViewEnvironment`s through `CompositionLocal`s
337
-
338
-
You’ll notice that all the APIs described above explicitly pass `ViewEnvironment` objects around. This mirrors how other Workflow UI code works, as well as the Mosaic integration. Compose has the concept of “composition local” — which is similar in spirit to `ViewEnvironment` itself (and SwiftUI’s [`Environment`](https://developer.apple.com/documentation/swiftui/environment)). So why not just pass view environments implicitly through composition locals?
339
-
340
-
This is what we did at first, but it made the API awkward for testing and other cases. Google advises against using composition locals in most cases for a reason. Because Workflow UI requires a `ViewRegistry` to be provided through the `ViewEnvironment`, there’s no obvious default value — what is the correct behavior when no `ViewEnvironment` local has been specified? Crashing at runtime is not ideal. We could provide an empty `ViewRegistry`, but that’s just another way to crash at runtime a few levels deeper in the call stack. Requiring explicit parameters for `ViewEnvironment` solves all these problems at the expense of a little more typing, and matches how the existing `ViewFactory` APIs work.
341
-
342
-
On the other hand, providing an API to access individual view environment elements from a composable that hides the actual mechanism and uses composition locals under the hood would let us take much better advantage of Compose’s fine-grained UI updates. We could ensure that, when a view environment changes, only the parts of the UI that actually care about the modified part of the environment are recomposed. However, renderings typically change an order of magnitude more frequently than view environments, so there’s probably not much point solving this problem until we’ve solved the same problem with renderings (discussed above under _Potential risk: Data model_).
343
+
It’s not clear how to solve this for renderings without abandoning our current rendering data model.
344
+
Today, renderings are an immutable tree of immutable value types
345
+
that require the entire tree to be recreated any time any single piece of data changes.
346
+
The reason for this design is that it was the only way to safely propagate changes
347
+
without adding a bunch of reactive streams to renderings everywhere.
348
+
349
+
The key word in that sentence is “was”: Compose’s snapshot state system makes it possible
350
+
to expose simple mutable properties and still get change notifications that will ensure
351
+
that the UI stays up-to-date.
352
+
For an example of how this system can be used to model complex state systems with dependencies,
353
+
see [this blog post](https://dev.to/zachklipp/plumbing-data-with-derived-state-in-compose-53ka).
354
+
355
+
Workflow could take advantage of this by allowing renderings to actually be mutable,
356
+
so that when one Workflow deep in the tree wants to change something, it can do so independently
357
+
and without requiring every rendering above it in the tree to also change.
358
+
Making such a change to such a fundamental piece of Workflow design could have significant implications
359
+
on other aspects of Workflow design, and doing so is very far outside the scope of this post.
360
+
361
+
We want to call this out because it seems like we’ll be losing out on one of Compose’s optimization tricks,
362
+
but we’re not sure how much of a problem this will turn out to be in the real world.
363
+
The only performance issues that we’re aware of that we’ve run into with Workflow UI so far are issues
364
+
with recreating leaf views on every rerender, and that in particular _*is*_ something Compose will automatically win at,
0 commit comments