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()`.

- `Screen.Preview()` is introduced. The existing `Preview()` extension functions were tied to `ScreenViewFactory`, making them much less useful. It is still the case that previews work for non-Compose UI code just fine. Which is pretty cool, really.

Fixes #546
  • Loading branch information
rjrjr committed Jan 25, 2024
1 parent 09bf09f commit 13968d1
Show file tree
Hide file tree
Showing 40 changed files with 1,076 additions and 832 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,16 @@ import androidx.compose.ui.unit.dp
import com.squareup.workflow1.WorkflowExperimentalRuntime
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.ViewRegistry
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.compose.WorkflowRendering
import com.squareup.workflow1.ui.compose.renderAsState
import com.squareup.workflow1.ui.plus
import com.squareup.workflow1.ui.compose.withComposeInteropSupport

private val viewEnvironment = ViewEnvironment.EMPTY + ViewRegistry(HelloBinding)
private val viewEnvironment = ViewEnvironment.EMPTY.withComposeInteropSupport()

@Composable fun App() {
MaterialTheme {
val rendering by HelloWorkflow.renderAsState(
val rendering by HelloComposeWorkflow.renderAsState(
props = Unit,
runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig(),
onOutput = {}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.squareup.sample.compose.hellocompose

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
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.tooling.Preview

@OptIn(WorkflowUiExperimentalApi::class)
data class HelloComposeScreen(
val message: String,
val onClick: () -> Unit
) : ComposeScreen {
@Composable override fun Content(viewEnvironment: ViewEnvironment) {
Text(
message,
modifier = Modifier
.clickable(onClick = onClick)
.fillMaxSize()
.wrapContentSize(Alignment.Center)
)
}
}

@OptIn(WorkflowUiExperimentalApi::class)
@Preview(heightDp = 150, showBackground = true)
@Composable
private fun HelloPreview() {
HelloComposeScreen(
"Hello!",
onClick = {}
).Preview()
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
package com.squareup.sample.compose.hellocompose

import com.squareup.sample.compose.hellocompose.HelloWorkflow.Rendering
import com.squareup.sample.compose.hellocompose.HelloWorkflow.State
import com.squareup.sample.compose.hellocompose.HelloWorkflow.State.Goodbye
import com.squareup.sample.compose.hellocompose.HelloWorkflow.State.Hello
import com.squareup.sample.compose.hellocompose.HelloComposeWorkflow.State
import com.squareup.sample.compose.hellocompose.HelloComposeWorkflow.State.Goodbye
import com.squareup.sample.compose.hellocompose.HelloComposeWorkflow.State.Hello
import com.squareup.workflow1.Snapshot
import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.action
import com.squareup.workflow1.parse
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi

object HelloWorkflow : StatefulWorkflow<Unit, State, Nothing, Rendering>() {
object HelloComposeWorkflow : StatefulWorkflow<Unit, State, Nothing, HelloComposeScreen>() {
enum class State {
Hello,
Goodbye;
Expand All @@ -22,12 +19,6 @@ object HelloWorkflow : StatefulWorkflow<Unit, State, Nothing, Rendering>() {
}
}

@OptIn(WorkflowUiExperimentalApi::class)
data class Rendering(
val message: String,
val onClick: () -> Unit
) : Screen

private val helloAction = action {
state = state.theOtherState()
}
Expand All @@ -42,7 +33,7 @@ object HelloWorkflow : StatefulWorkflow<Unit, State, Nothing, Rendering>() {
renderProps: Unit,
renderState: State,
context: RenderContext
): Rendering = Rendering(
): HelloComposeScreen = HelloComposeScreen(
message = renderState.name,
onClick = { context.actionSink.send(helloAction) }
)
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.plus
import com.squareup.workflow1.ui.renderWorkflowIn
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,10 +10,14 @@ 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.renderWorkflowIn
import com.squareup.workflow1.ui.withEnvironment
import kotlinx.coroutines.flow.StateFlow

class HelloComposeWorkflowActivity : AppCompatActivity() {
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,10 +10,14 @@ 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.renderWorkflowIn
import com.squareup.workflow1.ui.withEnvironment
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."
"Binds a Screen to a UI factory using ScreenComposableFactory()."
) { DrawHelloRenderingPreview() },
Sample(
"Nested Renderings",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.squareup.sample.compose.databinding.LegacyViewBinding
import com.squareup.sample.compose.nestedrenderings.RecursiveWorkflow.LegacyRendering
import com.squareup.workflow1.ui.ScreenViewFactory
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
Expand All @@ -25,19 +23,13 @@ class LegacyRunner(private val binding: LegacyViewBinding) : ScreenViewRunner<Le
) {
binding.stub.show(rendering.rendering, environment)
}

companion object : ScreenViewFactory<LegacyRendering> by fromViewBinding(
LegacyViewBinding::inflate,
::LegacyRunner
)
}

@OptIn(WorkflowUiExperimentalApi::class)
@Preview(widthDp = 200, heightDp = 150, showBackground = true)
@Composable
private fun LegacyRunnerPreview() {
LegacyRunner.Preview(
rendering = LegacyRendering(StringRendering("child")),
LegacyRendering(StringRendering("child")).Preview(
placeholderModifier = Modifier.fillMaxSize()
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,25 @@ 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.plus
import com.squareup.workflow1.ui.renderWorkflowIn
import com.squareup.workflow1.ui.withEnvironment
import kotlinx.coroutines.flow.StateFlow

@OptIn(WorkflowUiExperimentalApi::class)
private val viewRegistry = ViewRegistry(
RecursiveViewFactory,
LegacyRunner
)
private val viewRegistry = ViewRegistry(RecursiveViewFactory)

@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
@@ -1,13 +1,16 @@
package com.squareup.sample.compose.nestedrenderings

import com.squareup.sample.compose.databinding.LegacyViewBinding
import com.squareup.sample.compose.nestedrenderings.RecursiveWorkflow.LegacyRendering
import com.squareup.sample.compose.nestedrenderings.RecursiveWorkflow.Rendering
import com.squareup.sample.compose.nestedrenderings.RecursiveWorkflow.State
import com.squareup.workflow1.Snapshot
import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.action
import com.squareup.workflow1.renderChild
import com.squareup.workflow1.ui.AndroidScreen
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.ScreenViewFactory
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi

/**
Expand Down Expand Up @@ -39,7 +42,14 @@ object RecursiveWorkflow : StatefulWorkflow<Unit, State, Nothing, Screen>() {
/**
* Wrapper around a [Rendering] that will be implemented using a legacy view.
*/
data class LegacyRendering(val rendering: Screen) : Screen
data class LegacyRendering(
val rendering: Screen
) : AndroidScreen<LegacyRendering> {
override val viewFactory = ScreenViewFactory.fromViewBinding(
LegacyViewBinding::inflate,
::LegacyRunner
)
}

override fun initialState(
props: Unit,
Expand Down
Loading

0 comments on commit 13968d1

Please sign in to comment.