Skip to content

Commit

Permalink
BREAKING: Replaces ComposeScreenViewFactory with `ScreenComposableF…
Browse files Browse the repository at this point in the history
…actory`

This commit takes advantage of the new `ViewRegistry.Key`, which allows renderings to be bound to multiple UI factories, to fix a long standing problem where wrapper screens -- things like `NamedScreen` and `EnvironmentScreen` -- used in a Compose context would cause needless calls to `@Composeable fun AndroidView()` and `ComposeView()`.

For example, consider this rendering:

```
BodyAndOverlaysScreen(
  body = SomeComposeScreen(
    EnvironmentScreen(
      SomeOtherComposeScreen
    )
  )
)
```

Before this change, that would create a View hierarchy something like this:

```
BodyAndOverlaysContainer : FrameLayout {
  mChildren[0] = ComposeView {
    // compose land
    SomeComposeScreen.Content {
      AndroidView {
        ComposeView {
          // nested compose land
            SomeOtherComposeScreen.Content()
```

Now it will look this way:

```
BodyAndOverlaysContainer : FrameLayout {
  mChildren[0] = ComposeView {
    // compose land
    SomeComposeScreen.Content {
      SomeOtherComposeScreen.Content()
```

`ScreenComposableFactory` replaces `ComposeScreenViewFactory`, and `ComposeScreen` no longer extends `AndroidScreen`. Compose support is now a first class citizen, instead of a hack bolted on to View support.

Unfortunately, `ViewEnvironment.withComposeInteropSupport()` (see below) now must be called near the root of a Workflow app to enable the seamless Compose > Classic > Compose handling that used to be built in to `WorkflowRendering` and `ComposeScreenViewFactory`. This means that call is required for Compose support for built in rendering types like `BodyAndOverlaysScreen` and `BackStackScreen`, which so far are backed only by classic View implementations.

Other introductions, changes:

- `Screen.toComposableFactory()`, used by `WorkflowRendering()` in the same way that `WorkflowViewStub` uses `Screen.toViewFactory()`

- `ScreenComposableFactoryFinder`, a `ViewEnvironment`-based strategy object used by `Screen.toComposableFactory()` the same way that `Screen.toViewFactory()` uses `ScreenViewFactoryFinder`. The default implementation provides Compose bindings for `NamedScreen` and `EnvironmentScreen`, fixing #546.

- `ScreenViewFactoryFinder.getViewFactoryForRendering()` can now return `null`. A `requireViewFactoryForRendering()` extension is introduced for use when `null` is not acceptable.

- `ViewEnvironment.withComposeInteropSupport()`, which wraps the found `ScreenComposableFactoryFinder` and `ScreenViewFactoryFinder` with implementations that allow Compose contexts to handle renderings bound only to `ScreenViewFactory`, and classic contexts to handle renderings bound only to `ScreenComposableFactory`. Replaces the logic that used to be in the private `ScreenViewFactory.asComposeViewFactory()` extension in `WorkflowRendering()`.

Fixes #546
  • Loading branch information
rjrjr committed Jan 9, 2024
1 parent 6d51239 commit 03738d5
Show file tree
Hide file tree
Showing 35 changed files with 827 additions and 674 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.squareup.sample.compose.hellocompose.HelloWorkflow.Rendering
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.compose.composeScreenViewFactory
import com.squareup.workflow1.ui.compose.screenComposableFactory

@OptIn(WorkflowUiExperimentalApi::class)
val HelloBinding = composeScreenViewFactory<Rendering> { rendering, _ ->
val HelloBinding = screenComposableFactory<Rendering> { rendering, _ ->
Text(
rendering.message,
modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.squareup.sample.compose.hellocomposebinding.HelloWorkflow.Rendering
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.compose.composeScreenViewFactory
import com.squareup.workflow1.ui.compose.screenComposableFactory
import com.squareup.workflow1.ui.compose.tooling.Preview

@OptIn(WorkflowUiExperimentalApi::class)
val HelloBinding = composeScreenViewFactory<Rendering> { rendering, _ ->
val HelloBinding = screenComposableFactory<Rendering> { rendering, _ ->
Text(
rendering.message,
modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.ViewRegistry
import com.squareup.workflow1.ui.WorkflowLayout
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.compose.withComposeInteropSupport
import com.squareup.workflow1.ui.compose.withCompositionRoot
import com.squareup.workflow1.ui.container.withEnvironment
import com.squareup.workflow1.ui.plus
Expand All @@ -25,13 +26,15 @@ import kotlinx.coroutines.flow.StateFlow

@OptIn(WorkflowUiExperimentalApi::class)
private val viewEnvironment =
(ViewEnvironment.EMPTY + ViewRegistry(HelloBinding)).withCompositionRoot { content ->
MaterialTheme(content = content)
}
(ViewEnvironment.EMPTY + ViewRegistry(HelloBinding))
.withCompositionRoot { content ->
MaterialTheme(content = content)
}
.withComposeInteropSupport()

/**
* Demonstrates how to create and display a view factory with
* [composeScreenViewFactory][com.squareup.workflow1.ui.compose.composeScreenViewFactory].
* [screenComposableFactory][com.squareup.workflow1.ui.compose.screenComposableFactory].
*/
class HelloBindingActivity : AppCompatActivity() {
@OptIn(WorkflowUiExperimentalApi::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.squareup.workflow1.WorkflowExperimentalRuntime
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
import com.squareup.workflow1.mapRendering
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.WorkflowLayout
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.compose.withComposeInteropSupport
import com.squareup.workflow1.ui.container.withEnvironment
import com.squareup.workflow1.ui.renderWorkflowIn
import kotlinx.coroutines.flow.StateFlow

Expand All @@ -30,7 +34,9 @@ class HelloComposeWorkflowActivity : AppCompatActivity() {
@OptIn(WorkflowUiExperimentalApi::class)
val renderings: StateFlow<Screen> by lazy {
renderWorkflowIn(
workflow = HelloWorkflow,
workflow = HelloWorkflow.mapRendering {
it.withEnvironment(ViewEnvironment.EMPTY.withComposeInteropSupport())
},
scope = viewModelScope,
savedStateHandle = savedState,
runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.squareup.workflow1.WorkflowExperimentalRuntime
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
import com.squareup.workflow1.mapRendering
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.WorkflowLayout
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.compose.withComposeInteropSupport
import com.squareup.workflow1.ui.container.withEnvironment
import com.squareup.workflow1.ui.renderWorkflowIn
import kotlinx.coroutines.flow.StateFlow

Expand All @@ -34,7 +38,9 @@ class InlineRenderingActivity : AppCompatActivity() {
@OptIn(WorkflowUiExperimentalApi::class)
val renderings: StateFlow<Screen> by lazy {
renderWorkflowIn(
workflow = InlineRenderingWorkflow,
workflow = InlineRenderingWorkflow.mapRendering {
it.withEnvironment(ViewEnvironment.EMPTY.withComposeInteropSupport())
},
scope = viewModelScope,
savedStateHandle = savedState,
runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.WorkflowExperimentalRuntime
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
import com.squareup.workflow1.parse
import com.squareup.workflow1.ui.AndroidScreen
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.compose.ComposeScreen
import com.squareup.workflow1.ui.compose.WorkflowRendering
import com.squareup.workflow1.ui.compose.renderAsState

object InlineRenderingWorkflow : StatefulWorkflow<Unit, Int, Nothing, AndroidScreen<*>>() {
object InlineRenderingWorkflow : StatefulWorkflow<Unit, Int, Nothing, Screen>() {

override fun initialState(
props: Unit,
Expand All @@ -39,7 +39,7 @@ object InlineRenderingWorkflow : StatefulWorkflow<Unit, Int, Nothing, AndroidScr
renderProps: Unit,
renderState: Int,
context: RenderContext
): AndroidScreen<*> = ComposeScreen {
) = ComposeScreen {
Box {
Button(onClick = context.eventHandler { state += 1 }) {
Text("Counter: ")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ val samples = listOf(
Sample(
"Hello Compose Binding",
HelloBindingActivity::class,
"Creates a ViewFactory using composeViewFactory."
"Creates a ScreenComposableFactory using screenComposableFactory()."
) { DrawHelloRenderingPreview() },
Sample(
"Nested Renderings",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromViewBinding
import com.squareup.workflow1.ui.ScreenViewRunner
import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.compose.asComposableFactory
import com.squareup.workflow1.ui.compose.tooling.Preview

/**
Expand All @@ -36,7 +37,7 @@ class LegacyRunner(private val binding: LegacyViewBinding) : ScreenViewRunner<Le
@Preview(widthDp = 200, heightDp = 150, showBackground = true)
@Composable
private fun LegacyRunnerPreview() {
LegacyRunner.Preview(
LegacyRunner.asComposableFactory().Preview(
rendering = LegacyRendering(StringRendering("child")),
placeholderModifier = Modifier.fillMaxSize()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.ViewRegistry
import com.squareup.workflow1.ui.WorkflowLayout
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.compose.withComposeInteropSupport
import com.squareup.workflow1.ui.compose.withCompositionRoot
import com.squareup.workflow1.ui.container.withEnvironment
import com.squareup.workflow1.ui.plus
Expand All @@ -32,11 +33,13 @@ private val viewRegistry = ViewRegistry(

@OptIn(WorkflowUiExperimentalApi::class)
private val viewEnvironment =
(ViewEnvironment.EMPTY + viewRegistry).withCompositionRoot { content ->
CompositionLocalProvider(LocalBackgroundColor provides Color.Green) {
content()
(ViewEnvironment.EMPTY + viewRegistry)
.withCompositionRoot { content ->
CompositionLocalProvider(LocalBackgroundColor provides Color.Green) {
content()
}
}
}
.withComposeInteropSupport()

@WorkflowUiExperimentalApi
class NestedRenderingsActivity : AppCompatActivity() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.compose.WorkflowRendering
import com.squareup.workflow1.ui.compose.composeScreenViewFactory
import com.squareup.workflow1.ui.compose.screenComposableFactory
import com.squareup.workflow1.ui.compose.tooling.Preview

/**
Expand All @@ -39,7 +39,7 @@ val LocalBackgroundColor = compositionLocalOf<Color> { error("No background colo
* A `ViewFactory` that renders [RecursiveWorkflow.Rendering]s.
*/
@OptIn(WorkflowUiExperimentalApi::class)
val RecursiveViewFactory = composeScreenViewFactory<Rendering> { rendering, viewEnvironment ->
val RecursiveViewFactory = screenComposableFactory<Rendering> { rendering, viewEnvironment ->
// Every child should be drawn with a slightly-darker background color.
val color = LocalBackgroundColor.current
val childColor = remember(color) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import androidx.compose.ui.unit.dp
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.compose.WorkflowRendering
import com.squareup.workflow1.ui.compose.composeScreenViewFactory
import com.squareup.workflow1.ui.compose.screenComposableFactory
import com.squareup.workflow1.ui.compose.tooling.Preview

class PreviewActivity : AppCompatActivity() {
Expand Down Expand Up @@ -65,7 +65,7 @@ data class ContactDetailsRendering(
) : Screen

private val contactViewFactory =
composeScreenViewFactory<ContactRendering> { rendering, environment ->
screenComposableFactory<ContactRendering> { rendering, environment ->
Card(
modifier = Modifier
.padding(8.dp)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ import com.squareup.sample.compose.textinput.TextInputWorkflow.Rendering
import com.squareup.workflow1.ui.TextController
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.compose.asMutableState
import com.squareup.workflow1.ui.compose.composeScreenViewFactory
import com.squareup.workflow1.ui.compose.screenComposableFactory
import com.squareup.workflow1.ui.compose.tooling.Preview

@OptIn(WorkflowUiExperimentalApi::class)
val TextInputViewFactory = composeScreenViewFactory<Rendering> { rendering, _ ->
val TextInputViewFactory = screenComposableFactory<Rendering> { rendering, _ ->
Column(
modifier = Modifier
.fillMaxSize()
Expand Down
3 changes: 0 additions & 3 deletions workflow-ui/compose-tooling/README.md

This file was deleted.

2 changes: 1 addition & 1 deletion workflow-ui/compose-tooling/api/compose-tooling.api
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ public final class com/squareup/workflow1/ui/compose/tooling/ComposableSingleton
}

public final class com/squareup/workflow1/ui/compose/tooling/ViewFactoriesKt {
public static final fun Preview (Lcom/squareup/workflow1/ui/ScreenViewFactory;Lcom/squareup/workflow1/ui/Screen;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V
public static final fun Preview (Lcom/squareup/workflow1/ui/compose/ScreenComposableFactory;Lcom/squareup/workflow1/ui/Screen;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V
}

Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.ViewEnvironmentKey
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.compose.WorkflowRendering
import com.squareup.workflow1.ui.compose.composeScreenViewFactory
import com.squareup.workflow1.ui.compose.screenComposableFactory
import com.squareup.workflow1.ui.internal.test.IdleAfterTestRule
import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule
import leakcanary.DetectLeaksAfterTestSuccess
Expand Down Expand Up @@ -96,7 +96,7 @@ internal class PreviewViewFactoryTest {
}

private val ParentWithOneChild =
composeScreenViewFactory<TwoStrings> { rendering, environment ->
screenComposableFactory<TwoStrings> { rendering, environment ->
Column {
BasicText(rendering.first.text)
WorkflowRendering(rendering.second, environment)
Expand All @@ -109,7 +109,7 @@ internal class PreviewViewFactoryTest {
}

private val ParentWithTwoChildren =
composeScreenViewFactory<ThreeStrings> { rendering, environment ->
screenComposableFactory<ThreeStrings> { rendering, environment ->
Column {
WorkflowRendering(rendering.first, environment)
BasicText(rendering.second.text)
Expand Down Expand Up @@ -156,7 +156,7 @@ internal class PreviewViewFactoryTest {
) : Screen

private val ParentRecursive =
composeScreenViewFactory<RecursiveRendering> { rendering, environment ->
screenComposableFactory<RecursiveRendering> { rendering, environment ->
Column {
BasicText(rendering.text)
rendering.child?.let { child ->
Expand Down Expand Up @@ -198,7 +198,7 @@ internal class PreviewViewFactoryTest {
override val default: String get() = error("Not specified")
}

private val ParentConsumesCustomKey = composeScreenViewFactory<TwoStrings> { _, environment ->
private val ParentConsumesCustomKey = screenComposableFactory<TwoStrings> { _, environment ->
BasicText(environment[TestEnvironmentKey])
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,18 @@ import androidx.compose.ui.unit.dp
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.ScreenViewFactory
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.compose.composeScreenViewFactory
import com.squareup.workflow1.ui.compose.ScreenComposableFactory
import com.squareup.workflow1.ui.compose.screenComposableFactory

/**
* A [ScreenViewFactory] that will be used any time a [PreviewScreenViewFactoryFinder]
* is asked to show a rendering. It displays a placeholder graphic and the rendering's
* `toString()` result.
*/
internal fun placeholderScreenViewFactory(modifier: Modifier): ScreenViewFactory<Screen> =
composeScreenViewFactory { rendering, _ ->
internal fun placeholderScreenComposableFactory(
modifier: Modifier
): ScreenComposableFactory<Screen> =
screenComposableFactory { rendering, _ ->
BoxWithConstraints {
BasicText(
modifier = modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,52 +7,56 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.ScreenViewFactory
import com.squareup.workflow1.ui.ScreenViewFactoryFinder
import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.compose.ScreenComposableFactory
import com.squareup.workflow1.ui.compose.ScreenComposableFactoryFinder

/**
* Creates and [remember]s a [ViewEnvironment] that has a special [ScreenViewFactoryFinder]
* and any additional elements as configured by [viewEnvironmentUpdater].
*
* The [ScreenViewFactoryFinder] will contain [mainFactory] if specified, as well as a
* [placeholderScreenViewFactory] that will be used to show any renderings that don't match
* [placeholderScreenComposableFactory] that will be used to show any renderings that don't match
* [mainFactory]'s type. All placeholders will have [placeholderModifier] applied.
*/
@Composable internal fun rememberPreviewViewEnvironment(
placeholderModifier: Modifier,
viewEnvironmentUpdater: ((ViewEnvironment) -> ViewEnvironment)? = null,
mainFactory: ScreenViewFactory<*>? = null
mainFactory: ScreenComposableFactory<*>? = null
): ViewEnvironment {
val finder = remember(mainFactory, placeholderModifier) {
PreviewScreenViewFactoryFinder(mainFactory, placeholderScreenViewFactory(placeholderModifier))
PreviewScreenComposableFactoryFinder(
mainFactory,
placeholderScreenComposableFactory(placeholderModifier)
)
}
return remember(finder, viewEnvironmentUpdater) {
(ViewEnvironment.EMPTY + (ScreenViewFactoryFinder to finder)).let { environment ->
(ViewEnvironment.EMPTY + (ScreenComposableFactoryFinder to finder)).let { environment ->
// Give the preview a chance to add its own elements to the ViewEnvironment.
viewEnvironmentUpdater?.let { it(environment) } ?: environment
}
}
}

/**
* A [ScreenViewFactoryFinder] that uses [mainFactory] for rendering [RenderingT]s,
* A [ScreenComposableFactoryFinder] that uses [mainFactory] for rendering [RenderingT]s,
* and [placeholderFactory] for all other
* [WorkflowRendering][com.squareup.workflow1.ui.compose.WorkflowRendering] calls.
*/
@Immutable
private class PreviewScreenViewFactoryFinder<RenderingT : Screen>(
private val mainFactory: ScreenViewFactory<RenderingT>? = null,
private val placeholderFactory: ScreenViewFactory<Screen>
) : ScreenViewFactoryFinder {
override fun <ScreenT : Screen> getViewFactoryForRendering(
private class PreviewScreenComposableFactoryFinder<RenderingT : Screen>(
private val mainFactory: ScreenComposableFactory<RenderingT>? = null,
private val placeholderFactory: ScreenComposableFactory<Screen>
) : ScreenComposableFactoryFinder {
override fun <ScreenT : Screen> getComposableFactoryForRendering(
environment: ViewEnvironment,
rendering: ScreenT
): ScreenViewFactory<ScreenT> =
): ScreenComposableFactory<ScreenT> =
@Suppress("UNCHECKED_CAST")
if (rendering::class == mainFactory?.type) {
mainFactory as ScreenViewFactory<ScreenT>
mainFactory as ScreenComposableFactory<ScreenT>
} else {
placeholderFactory
}
Expand Down
Loading

0 comments on commit 03738d5

Please sign in to comment.