Repository: cashapp/redwood
Pull request #622 (2022-12-12 16:47:13)

The diff consuming factory produces nodes, not widgets
Naming debt from a previous change.


Patch:
diff --git a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/GenerateCommand.kt b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/GenerateCommand.kt
index 6232cd2b9..712979837 100644
--- a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/GenerateCommand.kt
+++ b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/GenerateCommand.kt
@@ -70,7 +70,6 @@ internal class GenerateCommand : CliktCommand(name = "generate") {
 
     when (type) {
       Compose -> {
-        generateComposableTargetMarker(schema).writeTo(out)
         for (dependency in schema.allSchemas) {
           generateLayoutModifierImpls(dependency)?.writeTo(out)
           for (scope in dependency.scopes) {
@@ -102,8 +101,8 @@ internal class GenerateCommand : CliktCommand(name = "generate") {
         }
       }
       WidgetProtocol -> {
+        generateDiffConsumingNodeFactory(schema).writeTo(out)
         for (dependency in schema.allSchemas) {
-          generateDiffConsumingWidgetFactory(dependency, host = schema).writeTo(out)
           generateDiffConsumingLayoutModifiers(dependency, host = schema).writeTo(out)
           for (widget in dependency.widgets) {
             generateDiffConsumingWidget(dependency, widget, host = schema).writeTo(out)
diff --git a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/composeGeneration.kt b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/composeGeneration.kt
index ae9b26ca0..a2df8e120 100644
--- a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/composeGeneration.kt
+++ b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/composeGeneration.kt
@@ -39,42 +39,6 @@ import com.squareup.kotlinpoet.asTypeName
 import com.squareup.kotlinpoet.joinToCode
 import kotlin.reflect.KClass
 
-/*
-@Retention(AnnotationRetention.BINARY)
-@ComposableTargetMarker(description = "Example Composable")
-@Target(
-  AnnotationTarget.FUNCTION,
-  AnnotationTarget.PROPERTY_GETTER,
-  AnnotationTarget.TYPE,
-  AnnotationTarget.TYPE_PARAMETER,
-)
-public annotation class SunspotComposable
-*/
-internal fun generateComposableTargetMarker(schema: Schema): FileSpec {
-  val name = schema.composeTargetMarker
-  return FileSpec.builder(schema.composePackage(), name.simpleName)
-    .addType(
-      TypeSpec.annotationBuilder(name)
-        .addAnnotation(
-          AnnotationSpec.builder(Retention::class)
-            .addMember("%T.BINARY", AnnotationRetention::class)
-            .build(),
-        )
-        .addAnnotation(
-          AnnotationSpec.builder(ComposeRuntime.ComposableTargetMarker)
-            .addMember("description = %S", schema.name + " Composable")
-            .build(),
-        )
-        .addAnnotation(
-          AnnotationSpec.builder(Target::class)
-            .addMember("%1T.FUNCTION, %1T.PROPERTY_GETTER, %1T.TYPE, %1T.TYPE_PARAMETER", AnnotationTarget::class)
-            .build(),
-        )
-        .build(),
-    )
-    .build()
-}
-
 /*
 @Composable
 @SunspotComposable
@@ -107,14 +71,12 @@ internal fun generateComposable(
 ): FileSpec {
   val widgetType = schema.widgetType(widget).parameterizedBy(STAR)
   val widgetFactoryType = host.getWidgetFactoryType().parameterizedBy(STAR)
-  val composeTargetMarker = host.composeTargetMarker
   val flatName = widget.type.flatName
   return FileSpec.builder(schema.composePackage(), flatName)
     .addFunction(
       FunSpec.builder(flatName)
         .addModifiers(PUBLIC)
         .addAnnotation(ComposeRuntime.Composable)
-        .addAnnotation(composeTargetMarker)
         .addAnnotation(
           AnnotationSpec.builder(Stdlib.OptIn)
             .addMember("%T::class", Redwood.RedwoodCodegenApi)
@@ -154,7 +116,7 @@ internal fun generateComposable(
                 }
                 is Children -> {
                   val scope = trait.scope?.let { ClassName(schema.composePackage(), it.simpleName!!) }
-                  ParameterSpec.builder(trait.name, composableLambda(scope, composeTargetMarker))
+                  ParameterSpec.builder(trait.name, composableLambda(scope))
                     .apply {
                       trait.defaultExpression?.let { defaultValue(it) }
                     }
diff --git a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffConsumingGeneration.kt b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffConsumingGeneration.kt
index e2cd037f7..cabb85cf1 100644
--- a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffConsumingGeneration.kt
+++ b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffConsumingGeneration.kt
@@ -27,7 +27,6 @@ import com.squareup.kotlinpoet.FunSpec
 import com.squareup.kotlinpoet.KModifier.INTERNAL
 import com.squareup.kotlinpoet.KModifier.OVERRIDE
 import com.squareup.kotlinpoet.KModifier.PRIVATE
-import com.squareup.kotlinpoet.KModifier.PUBLIC
 import com.squareup.kotlinpoet.LIST
 import com.squareup.kotlinpoet.ParameterSpec
 import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
@@ -37,52 +36,40 @@ import com.squareup.kotlinpoet.TypeSpec
 import com.squareup.kotlinpoet.asTypeName
 
 /*
-public class DiffConsumingSunspotWidgetFactory<W : Any>(
-  private val delegate: SunspotWidgetFactory<W>,
+public class SunspotDiffConsumingNodeFactory<W : Any>(
+  private val widgets: SunspotWidgetFactory<W>,
   private val json: Json = Json.Default,
   private val mismatchHandler: ProtocolMismatchHandler = ProtocolMismatchHandler.Throwing,
 ) : DiffConsumingNode.Factory<W> {
-  override val RedwoodLayout = DiffConsumingRedwoodLayoutWidgetFactory(delegate.RedwoodLayout, json, mismatchHandler)
-
   override fun create(
     parentId: Id,
     parentChildren: Widget.Children<W>,
     tag: WidgetTag,
-  ): DiffConsumingWidget<W>? = when (tag.value) {
-    1 -> SunspotBox(parentId, parentChildren)
-    2 -> SunspotText(parentId, parentChildren)
-    3 -> SunspotButton(parentId, parentChildren)
-    1_000_001 -> RedwoodLayout.Row(parentId, parentChildren)
-    1_000_002 -> RedwoodLayout.Column(parentId, parentChildren)
+  ): DiffConsumingNode<W>? = when (tag.value) {
+    1 -> DiffConsumingTextNode(parentId, parentChildren, delegate.Text(), json, mismatchHandler)
+    2 -> DiffConsumingButtonNode(parentId, parentChildren, delegate.Button(), json, mismatchHandler)
+    1_000_001 -> DiffConsumingRedwoodLayoutRowNode(parentId, parentChildren, delegate.RedwoodLayout.Row(), json, mismatchHandler)
+    1_000_002 -> DiffConsumingRedwoodLayoutColumnNode(parentId, parentChildren, delegate.RedwoodLayout.Column(), json, mismatchHandler)
     else -> {
       mismatchHandler.onUnknownWidget(tag)
       null
     }
   }
-
-  private fun SunspotBox(
-    parentId: Id,
-    parentChildren: Widget.Children<W>,
-  ): DiffConsumingSunspotBox<W> {
-    return DiffConsumingSunspotBox(parentId, parentChildren, delegate.SunspotBox(), json, mismatchHandler)
-  }
-  etc.
 }
 */
-internal fun generateDiffConsumingWidgetFactory(
+internal fun generateDiffConsumingNodeFactory(
   schema: ProtocolSchema,
-  host: ProtocolSchema = schema,
 ): FileSpec {
   val widgetFactory = schema.getWidgetFactoryType().parameterizedBy(typeVariableW)
-  val type = schema.diffConsumingWidgetFactoryType(host)
+  val type = schema.diffConsumingNodeFactoryType()
   return FileSpec.builder(type.packageName, type.simpleName)
     .addType(
       TypeSpec.classBuilder(type)
-        .addModifiers(if (schema === host) PUBLIC else INTERNAL)
         .addTypeVariable(typeVariableW)
+        .addSuperinterface(WidgetProtocol.DiffConsumingNodeFactory.parameterizedBy(typeVariableW))
         .primaryConstructor(
           FunSpec.constructorBuilder()
-            .addParameter("delegate", widgetFactory)
+            .addParameter("widgets", widgetFactory)
             .addParameter(
               ParameterSpec.builder("json", KotlinxSerialization.Json)
                 .defaultValue("%T", KotlinxSerialization.JsonDefault)
@@ -96,8 +83,8 @@ internal fun generateDiffConsumingWidgetFactory(
             .build(),
         )
         .addProperty(
-          PropertySpec.builder("delegate", widgetFactory, PRIVATE)
-            .initializer("delegate")
+          PropertySpec.builder("widgets", widgetFactory, PRIVATE)
+            .initializer("widgets")
             .build(),
         )
         .addProperty(
@@ -110,74 +97,46 @@ internal fun generateDiffConsumingWidgetFactory(
             .initializer("mismatchHandler")
             .build(),
         )
-        .apply {
-          if (schema === host) {
-            // Only conform to entrypoint interface if we are the host. If we are not the host,
-            // the host will handle the entire transitive dependency set of tags itself.
-            addSuperinterface(
-              WidgetProtocol.DiffConsumingNodeFactory.parameterizedBy(typeVariableW),
+        .addFunction(
+          FunSpec.builder("create")
+            .addModifiers(OVERRIDE)
+            .addParameter("parentId", Protocol.Id)
+            .addParameter("parentChildren", RedwoodWidget.WidgetChildrenOfW)
+            .addParameter("tag", Protocol.WidgetTag)
+            .returns(
+              WidgetProtocol.DiffConsumingNode.parameterizedBy(typeVariableW)
+                .copy(nullable = true),
             )
-            addFunction(
-              FunSpec.builder("create")
-                .addModifiers(OVERRIDE)
-                .addParameter("parentId", Protocol.Id)
-                .addParameter("parentChildren", RedwoodWidget.WidgetChildrenOfW)
-                .addParameter("tag", Protocol.WidgetTag)
-                .returns(
-                  WidgetProtocol.DiffConsumingNode.parameterizedBy(typeVariableW)
-                    .copy(nullable = true),
+            .beginControlFlow("return when (tag.value)")
+            .apply {
+              for (widget in schema.widgets.sortedBy { it.tag }) {
+                addStatement(
+                  "%L -> %T(parentId, parentChildren, widgets.%N(), json, mismatchHandler)",
+                  widget.tag,
+                  schema.diffConsumingNodeType(widget, schema),
+                  widget.type.flatName,
                 )
-                .beginControlFlow("return when (tag.value)")
-                .apply {
-                  for (widget in schema.widgets.sortedBy { it.tag }) {
-                    addStatement("%L -> %N(parentId, parentChildren)", widget.tag, widget.type.flatName)
-                  }
-                  for (dependency in schema.dependencies.sortedBy { it.widgets.firstOrNull()?.tag ?: 0 }) {
-                    for (widget in dependency.widgets.sortedBy { it.tag }) {
-                      addStatement("%L -> %N.%N(parentId, parentChildren)", widget.tag, dependency.name, widget.type.flatName)
-                    }
-                  }
-                }
-                .beginControlFlow("else ->")
-                .addStatement("mismatchHandler.onUnknownWidget(tag)")
-                .addStatement("null")
-                .endControlFlow()
-                .endControlFlow()
-                .build(),
-            )
+              }
 
-            for (dependency in schema.dependencies.sortedBy { it.name }) {
-              val dependencyType = dependency.diffConsumingWidgetFactoryType(host)
-              addProperty(
-                PropertySpec.builder(dependency.name, dependencyType.parameterizedBy(typeVariableW))
-                  .addModifiers(PRIVATE)
-                  .initializer(
-                    "%T(delegate.%N, json, mismatchHandler)",
-                    dependencyType,
+              for (dependency in schema.dependencies.sortedBy { it.widgets.firstOrNull()?.tag ?: 0 }) {
+                for (widget in dependency.widgets.sortedBy { it.tag }) {
+                  addStatement(
+                    "%L -> %T(parentId, parentChildren, widgets.%N.%N(), json, mismatchHandler)",
+                    widget.tag,
+                    dependency.diffConsumingNodeType(widget, schema),
                     dependency.name,
+                    widget.type.flatName,
                   )
-                  .build(),
-              )
+                }
+              }
             }
-          }
-
-          for (widget in schema.widgets.sortedBy { it.type.flatName }) {
-            val diffConsumingWidgetType = schema.diffConsumingWidgetType(widget, host)
-            addFunction(
-              FunSpec.builder(widget.type.flatName)
-                .addModifiers(if (schema === host) PRIVATE else INTERNAL)
-                .addParameter("parentId", Protocol.Id)
-                .addParameter("parentChildren", RedwoodWidget.WidgetChildrenOfW)
-                .returns(diffConsumingWidgetType.parameterizedBy(typeVariableW))
-                .addStatement(
-                  "return %T(parentId, parentChildren, delegate.%N(), json, mismatchHandler)",
-                  diffConsumingWidgetType,
-                  widget.type.flatName,
-                )
-                .build(),
-            )
-          }
-        }
+            .beginControlFlow("else ->")
+            .addStatement("mismatchHandler.onUnknownWidget(tag)")
+            .addStatement("null")
+            .endControlFlow()
+            .endControlFlow()
+            .build(),
+        )
         .build(),
     )
     .build()
@@ -220,7 +179,7 @@ internal fun generateDiffConsumingWidget(
   widget: ProtocolWidget,
   host: ProtocolSchema = schema,
 ): FileSpec {
-  val type = schema.diffConsumingWidgetType(widget, host)
+  val type = schema.diffConsumingNodeType(widget, host)
   val widgetType = schema.widgetType(widget).parameterizedBy(typeVariableW)
   val protocolType = WidgetProtocol.DiffConsumingNode.parameterizedBy(typeVariableW)
   return FileSpec.builder(type.packageName, type.simpleName)
diff --git a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/sharedHelpers.kt b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/sharedHelpers.kt
index 5e09b0a06..86458e176 100644
--- a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/sharedHelpers.kt
+++ b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/sharedHelpers.kt
@@ -64,8 +64,6 @@ internal fun Schema.composePackage(host: Schema = this): String {
   }
 }
 
-internal val Schema.composeTargetMarker get() = ClassName(composePackage(this), "${name}Composable")
-
 internal fun Schema.diffProducingWidgetFactoryType(host: Schema): ClassName {
   return ClassName(composePackage(host), "DiffProducing${name}WidgetFactory")
 }
@@ -74,12 +72,12 @@ internal fun Schema.diffProducingWidgetType(widget: Widget, host: Schema): Class
   return ClassName(composePackage(host), "DiffProducing${widget.type.flatName}")
 }
 
-internal fun Schema.diffConsumingWidgetFactoryType(host: Schema): ClassName {
-  return ClassName(widgetPackage(host), "DiffConsuming${name}WidgetFactory")
+internal fun Schema.diffConsumingNodeFactoryType(): ClassName {
+  return ClassName(widgetPackage(), "${name}DiffConsumingNodeFactory")
 }
 
-internal fun Schema.diffConsumingWidgetType(widget: Widget, host: Schema): ClassName {
-  return ClassName(widgetPackage(host), "DiffConsuming${widget.type.flatName}")
+internal fun Schema.diffConsumingNodeType(widget: Widget, host: Schema): ClassName {
+  return ClassName(widgetPackage(host), "DiffConsuming${widget.type.flatName}Node")
 }
 
 internal fun Schema.widgetType(widget: Widget): ClassName {
diff --git a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/types.kt b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/types.kt
index 3cb175e42..05a28a720 100644
--- a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/types.kt
+++ b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/types.kt
@@ -74,13 +74,11 @@ internal object RedwoodCompose {
 
 internal object ComposeRuntime {
   val Composable = ClassName("androidx.compose.runtime", "Composable")
-  val ComposableTargetMarker = ClassName("androidx.compose.runtime", "ComposableTargetMarker")
   val Stable = ClassName("androidx.compose.runtime", "Stable")
 }
 
 internal fun composableLambda(
   receiver: TypeName?,
-  composeTargetMarker: ClassName,
 ): TypeName {
   return LambdaTypeName.get(
     returnType = UNIT,
@@ -88,7 +86,6 @@ internal fun composableLambda(
   ).copy(
     annotations = listOf(
       AnnotationSpec.builder(ComposeRuntime.Composable).build(),
-      AnnotationSpec.builder(composeTargetMarker).build(),
     ),
   )
 }
diff --git a/redwood-treehouse/src/androidMain/kotlin/app/cash/redwood/treehouse/TreehouseWidgetView.kt b/redwood-treehouse/src/androidMain/kotlin/app/cash/redwood/treehouse/TreehouseWidgetView.kt
index d72841538..fb1944c64 100644
--- a/redwood-treehouse/src/androidMain/kotlin/app/cash/redwood/treehouse/TreehouseWidgetView.kt
+++ b/redwood-treehouse/src/androidMain/kotlin/app/cash/redwood/treehouse/TreehouseWidgetView.kt
@@ -58,10 +58,16 @@ public class TreehouseWidgetView<A : AppService>(
 
   private var content: TreehouseView.Content<A>? = null
 
+  /**
+   * Like [View.isAttachedToWindow]. We'd prefer that property but it's false until
+   * [onAttachedToWindow] returns and true until [onDetachedFromWindow] returns.
+   */
+  private var immediateIsAttachedToWindow = false
+
   override val boundContent: TreehouseView.Content<A>?
     get() {
       return when {
-        isAttachedToWindow -> content
+        immediateIsAttachedToWindow -> content
         else -> null
       }
     }
@@ -89,11 +95,13 @@ public class TreehouseWidgetView<A : AppService>(
 
   override fun onAttachedToWindow() {
     super.onAttachedToWindow()
+    immediateIsAttachedToWindow = true
     stateChangeListener?.onStateChanged(this)
   }
 
   override fun onDetachedFromWindow() {
     super.onDetachedFromWindow()
+    immediateIsAttachedToWindow = false
     stateChangeListener?.onStateChanged(this)
   }
 
diff --git a/samples/emoji-search/android-composeui/src/main/java/app/cash/zipline/samples/emojisearch/composeui/EmojiSearchActivity.kt b/samples/emoji-search/android-composeui/src/main/java/app/cash/zipline/samples/emojisearch/composeui/EmojiSearchActivity.kt
index 2b8bfffe9..ae3ce5905 100644
--- a/samples/emoji-search/android-composeui/src/main/java/app/cash/zipline/samples/emojisearch/composeui/EmojiSearchActivity.kt
+++ b/samples/emoji-search/android-composeui/src/main/java/app/cash/zipline/samples/emojisearch/composeui/EmojiSearchActivity.kt
@@ -17,7 +17,6 @@ package app.cash.zipline.samples.emojisearch.composeui
 
 import android.os.Bundle
 import androidx.activity.ComponentActivity
-import androidx.compose.runtime.Composable
 import androidx.compose.runtime.NoLiveLiterals
 import androidx.compose.ui.platform.ComposeView
 import app.cash.redwood.compose.AndroidUiDispatcher.Companion.Main
@@ -29,7 +28,7 @@ import app.cash.redwood.treehouse.composeui.TreehouseContent
 import app.cash.zipline.loader.ManifestVerifier
 import app.cash.zipline.samples.emojisearch.EmojiSearchAppSpec
 import app.cash.zipline.samples.emojisearch.EmojiSearchPresenter
-import example.schema.widget.DiffConsumingEmojiSearchWidgetFactory
+import example.schema.widget.EmojiSearchDiffConsumingNodeFactory
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.cancel
 import kotlinx.serialization.json.Json
@@ -50,8 +49,8 @@ class EmojiSearchActivity : ComponentActivity() {
         app: TreehouseApp<EmojiSearchPresenter>,
         json: Json,
         protocolMismatchHandler: ProtocolMismatchHandler,
-      ) = DiffConsumingEmojiSearchWidgetFactory<@Composable () -> Unit>(
-        delegate = AndroidEmojiSearchWidgetFactory(app, this),
+      ) = EmojiSearchDiffConsumingNodeFactory(
+        widgets = AndroidEmojiSearchWidgetFactory(app, this),
         json = json,
         mismatchHandler = protocolMismatchHandler,
       )
diff --git a/samples/emoji-search/android-views/src/main/java/app/cash/zipline/samples/emojisearch/views/EmojiSearchActivity.kt b/samples/emoji-search/android-views/src/main/java/app/cash/zipline/samples/emojisearch/views/EmojiSearchActivity.kt
index 0e95176df..73525e993 100644
--- a/samples/emoji-search/android-views/src/main/java/app/cash/zipline/samples/emojisearch/views/EmojiSearchActivity.kt
+++ b/samples/emoji-search/android-views/src/main/java/app/cash/zipline/samples/emojisearch/views/EmojiSearchActivity.kt
@@ -27,7 +27,7 @@ import app.cash.redwood.treehouse.TreehouseWidgetView
 import app.cash.zipline.loader.ManifestVerifier
 import app.cash.zipline.samples.emojisearch.EmojiSearchAppSpec
 import app.cash.zipline.samples.emojisearch.EmojiSearchPresenter
-import example.schema.widget.DiffConsumingEmojiSearchWidgetFactory
+import example.schema.widget.EmojiSearchDiffConsumingNodeFactory
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.cancel
 import kotlinx.serialization.json.Json
@@ -48,8 +48,8 @@ class EmojiSearchActivity : ComponentActivity() {
         json: Json,
         protocolMismatchHandler: ProtocolMismatchHandler,
       ): DiffConsumingNode.Factory<*> {
-        return DiffConsumingEmojiSearchWidgetFactory(
-          delegate = AndroidViewEmojiSearchWidgetFactory(
+        return EmojiSearchDiffConsumingNodeFactory(
+          widgets = AndroidViewEmojiSearchWidgetFactory(
             context = this@EmojiSearchActivity,
             treehouseApp = treehouseApp,
             widgetSystem = this,
diff --git a/samples/emoji-search/ios/app/EmojiSearchApp/EmojiSearchViewController.swift b/samples/emoji-search/ios/app/EmojiSearchApp/EmojiSearchViewController.swift
index 357e837d5..8d8a8e7b5 100644
--- a/samples/emoji-search/ios/app/EmojiSearchApp/EmojiSearchViewController.swift
+++ b/samples/emoji-search/ios/app/EmojiSearchApp/EmojiSearchViewController.swift
@@ -53,8 +53,8 @@ class EmojiSearchContent : Redwood_treehouseTreehouseViewContent {
 
 class EmojiSearchWidgetSystem : Redwood_treehouseTreehouseViewWidgetSystem {
     func widgetFactory(app: Redwood_treehouseTreehouseApp<Redwood_treehouseAppService>, json: Kotlinx_serialization_jsonJson, protocolMismatchHandler: Redwood_protocol_widgetProtocolMismatchHandler) -> Redwood_protocol_widgetDiffConsumingNodeFactory {
-        return ExposedKt.diffConsumingEmojiSearchWidgetFactory(
-            delegate: IosEmojiSearchWidgetFactory(treehouseApp: app, widgetSystem: self),
+        return ProtocolEmojiSearchDiffConsumingNodeFactory<UIView>(
+            widgets: IosEmojiSearchWidgetFactory(treehouseApp: app, widgetSystem: self),
             json: json,
             mismatchHandler: protocolMismatchHandler
         );
diff --git a/samples/emoji-search/ios/shared/src/commonMain/kotlin/app/cash/zipline/samples/emojisearch/exposed.kt b/samples/emoji-search/ios/shared/src/commonMain/kotlin/app/cash/zipline/samples/emojisearch/exposed.kt
index 40baf9539..84e597302 100644
--- a/samples/emoji-search/ios/shared/src/commonMain/kotlin/app/cash/zipline/samples/emojisearch/exposed.kt
+++ b/samples/emoji-search/ios/shared/src/commonMain/kotlin/app/cash/zipline/samples/emojisearch/exposed.kt
@@ -19,13 +19,11 @@ package app.cash.zipline.samples.emojisearch
 
 import app.cash.redwood.LayoutModifier
 import app.cash.redwood.layout.uiview.UIViewRedwoodLayoutWidgetFactory
-import app.cash.redwood.protocol.widget.ProtocolMismatchHandler
 import app.cash.redwood.treehouse.TreehouseUIKitView
 import app.cash.redwood.treehouse.TreehouseView
 import app.cash.redwood.treehouse.lazylayout.uiview.UIViewRedwoodTreehouseLazyLayoutWidgetFactory
-import example.schema.widget.DiffConsumingEmojiSearchWidgetFactory
+import example.schema.widget.EmojiSearchDiffConsumingNodeFactory
 import example.schema.widget.EmojiSearchWidgetFactory
-import kotlinx.serialization.json.Json
 import okio.ByteString
 import okio.toByteString
 import platform.Foundation.NSData
@@ -39,6 +37,7 @@ fun exposedTypes(
   uiViewRedwoodLayoutWidgetFactory: UIViewRedwoodLayoutWidgetFactory,
   uiViewRedwoodTreehouseLazyLayoutWidgetFactory: UIViewRedwoodTreehouseLazyLayoutWidgetFactory<*>,
   widgetSystem: TreehouseView.WidgetSystem<*>,
+  diffConsumingNodeFactory: EmojiSearchDiffConsumingNodeFactory<*>,
 ) {
   throw AssertionError()
 }
@@ -46,9 +45,3 @@ fun exposedTypes(
 fun byteStringOf(data: NSData): ByteString = data.toByteString()
 
 fun layoutModifier(): LayoutModifier = LayoutModifier
-
-fun diffConsumingEmojiSearchWidgetFactory(
-  delegate: EmojiSearchWidgetFactory<*>,
-  json: Json = Json.Default,
-  mismatchHandler: ProtocolMismatchHandler,
-) = DiffConsumingEmojiSearchWidgetFactory(delegate, json, mismatchHandler)


Test patch:
diff --git a/redwood-cli/src/test/kotlin/app/cash/redwood/generator/ComposeGenerationTest.kt b/redwood-cli/src/test/kotlin/app/cash/redwood/generator/ComposeGenerationTest.kt
index 49d1e0882..2033f0b6c 100644
--- a/redwood-cli/src/test/kotlin/app/cash/redwood/generator/ComposeGenerationTest.kt
+++ b/redwood-cli/src/test/kotlin/app/cash/redwood/generator/ComposeGenerationTest.kt
@@ -40,27 +40,13 @@ class ComposeGenerationTest {
     @Children(2) val unscoped: () -> Unit,
   )
 
-  @Test fun `function target marker`() {
-    val schema = parseSchema(ScopedAndUnscopedSchema::class)
-
-    val fileSpec = generateComposable(schema, schema.widgets.single())
-    assertThat(fileSpec.toString()).contains(
-      """
-      |@Composable
-      |@ScopedAndUnscopedSchemaComposable
-      |@OptIn(RedwoodCodegenApi::class)
-      |public fun
-      """.trimMargin(),
-    )
-  }
-
-  @Test fun `scoped and unscoped children with target marker`() {
+  @Test fun `scoped and unscoped children`() {
     val schema = parseSchema(ScopedAndUnscopedSchema::class)
 
     val fileSpec = generateComposable(schema, schema.widgets.single())
     assertThat(fileSpec.toString()).apply {
-      contains("scoped: @Composable @ScopedAndUnscopedSchemaComposable RowScope.() -> Unit")
-      contains("unscoped: @Composable @ScopedAndUnscopedSchemaComposable () -> Unit")
+      contains("scoped: @Composable RowScope.() -> Unit")
+      contains("unscoped: @Composable () -> Unit")
     }
   }
 
@@ -100,7 +86,7 @@ class ComposeGenerationTest {
     assertThat(fileSpec.toString()).apply {
       contains("trait: String = \"test\"")
       contains("onEvent: (() -> Unit)? = { error(\"test\") }")
-      contains("block: @Composable @DefaultSchemaComposable () -> Unit = {}")
+      contains("block: @Composable () -> Unit = {}")
     }
   }
 
@@ -124,8 +110,8 @@ class ComposeGenerationTest {
     assertThat(fileSpec.toString()).contains(
       """
       |  layoutModifier: LayoutModifier = LayoutModifier,
-      |  top: @Composable @MultipleChildSchemaComposable () -> Unit,
-      |  bottom: @Composable @MultipleChildSchemaComposable () -> Unit,
+      |  top: @Composable () -> Unit,
+      |  bottom: @Composable () -> Unit,
       """.trimMargin(),
     )
   }
diff --git a/redwood-cli/src/test/kotlin/app/cash/redwood/generator/DiffConsumingGenerationTest.kt b/redwood-cli/src/test/kotlin/app/cash/redwood/generator/DiffConsumingGenerationTest.kt
index cec16eaf6..15cd12dec 100644
--- a/redwood-cli/src/test/kotlin/app/cash/redwood/generator/DiffConsumingGenerationTest.kt
+++ b/redwood-cli/src/test/kotlin/app/cash/redwood/generator/DiffConsumingGenerationTest.kt
@@ -50,7 +50,7 @@ class DiffConsumingGenerationTest {
   @Test fun `names are sorted by their node tags`() {
     val schema = parseSchema(SortedByTagSchema::class)
 
-    val fileSpec = generateDiffConsumingWidgetFactory(schema)
+    val fileSpec = generateDiffConsumingNodeFactory(schema)
     assertThat(fileSpec.toString()).containsMatch(
       Pattern.compile("1 ->[^2]+2 ->[^3]+3 ->[^1]+12 ->", MULTILINE),
     )
diff --git a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidgetFactoryTest.kt b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/DiffConsumingNodeFactoryTest.kt
similarity index 86%
rename from redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidgetFactoryTest.kt
rename to redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/DiffConsumingNodeFactoryTest.kt
index 3451dffda..1e3dbcab8 100644
--- a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidgetFactoryTest.kt
+++ b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/DiffConsumingNodeFactoryTest.kt
@@ -27,7 +27,7 @@ import app.cash.redwood.protocol.PropertyDiff
 import app.cash.redwood.protocol.PropertyTag
 import app.cash.redwood.protocol.WidgetTag
 import example.redwood.compose.TestScope
-import example.redwood.widget.DiffConsumingExampleSchemaWidgetFactory
+import example.redwood.widget.ExampleSchemaDiffConsumingNodeFactory
 import example.redwood.widget.TextInput
 import kotlin.test.Test
 import kotlin.test.assertEquals
@@ -42,9 +42,9 @@ import kotlinx.serialization.json.buildJsonArray
 import kotlinx.serialization.json.buildJsonObject
 import kotlinx.serialization.modules.SerializersModule
 
-class DiffConsumingWidgetFactoryTest {
+class DiffConsumingNodeFactoryTest {
   @Test fun unknownWidgetThrowsDefault() {
-    val factory = DiffConsumingExampleSchemaWidgetFactory(EmptyExampleSchemaWidgetFactory())
+    val factory = ExampleSchemaDiffConsumingNodeFactory(EmptyExampleSchemaWidgetFactory())
 
     val t = assertFailsWith<IllegalArgumentException> {
       factory.create(Id.Root, ThrowingWidgetChildren(), WidgetTag(345432))
@@ -54,8 +54,8 @@ class DiffConsumingWidgetFactoryTest {
 
   @Test fun unknownWidgetCallsHandler() {
     val handler = RecordingProtocolMismatchHandler()
-    val factory = DiffConsumingExampleSchemaWidgetFactory(
-      delegate = EmptyExampleSchemaWidgetFactory(),
+    val factory = ExampleSchemaDiffConsumingNodeFactory(
+      widgets = EmptyExampleSchemaWidgetFactory(),
       mismatchHandler = handler,
     )
 
@@ -71,8 +71,8 @@ class DiffConsumingWidgetFactoryTest {
       }
     }
     val recordingTextInput = RecordingTextInput()
-    val factory = DiffConsumingExampleSchemaWidgetFactory(
-      delegate = object : EmptyExampleSchemaWidgetFactory() {
+    val factory = ExampleSchemaDiffConsumingNodeFactory(
+      widgets = object : EmptyExampleSchemaWidgetFactory() {
         override fun TextInput() = recordingTextInput
       },
       json = json,
@@ -102,8 +102,8 @@ class DiffConsumingWidgetFactoryTest {
       }
     }
     val recordingTextInput = RecordingTextInput()
-    val factory = DiffConsumingExampleSchemaWidgetFactory(
-      delegate = object : EmptyExampleSchemaWidgetFactory() {
+    val factory = ExampleSchemaDiffConsumingNodeFactory(
+      widgets = object : EmptyExampleSchemaWidgetFactory() {
         override fun TextInput() = recordingTextInput
       },
       json = json,
@@ -127,7 +127,7 @@ class DiffConsumingWidgetFactoryTest {
   }
 
   @Test fun unknownLayoutModifierThrowsDefault() {
-    val factory = DiffConsumingExampleSchemaWidgetFactory(EmptyExampleSchemaWidgetFactory())
+    val factory = ExampleSchemaDiffConsumingNodeFactory(EmptyExampleSchemaWidgetFactory())
     val button = factory.create(Id.Root, ThrowingWidgetChildren(), WidgetTag(4))!!
 
     val t = assertFailsWith<IllegalArgumentException> {
@@ -151,8 +151,8 @@ class DiffConsumingWidgetFactoryTest {
     }
     val handler = RecordingProtocolMismatchHandler()
     val recordingTextInput = RecordingTextInput()
-    val factory = DiffConsumingExampleSchemaWidgetFactory(
-      delegate = object : EmptyExampleSchemaWidgetFactory() {
+    val factory = ExampleSchemaDiffConsumingNodeFactory(
+      widgets = object : EmptyExampleSchemaWidgetFactory() {
         override fun TextInput() = recordingTextInput
       },
       json = json,
@@ -185,7 +185,7 @@ class DiffConsumingWidgetFactoryTest {
   }
 
   @Test fun unknownChildrenThrowsDefault() {
-    val factory = DiffConsumingExampleSchemaWidgetFactory(EmptyExampleSchemaWidgetFactory())
+    val factory = ExampleSchemaDiffConsumingNodeFactory(EmptyExampleSchemaWidgetFactory())
     val button = factory.create(Id.Root, ThrowingWidgetChildren(), WidgetTag(4))!!
 
     val t = assertFailsWith<IllegalArgumentException> {
@@ -196,8 +196,8 @@ class DiffConsumingWidgetFactoryTest {
 
   @Test fun unknownChildrenCallsHandler() {
     val handler = RecordingProtocolMismatchHandler()
-    val factory = DiffConsumingExampleSchemaWidgetFactory(
-      delegate = EmptyExampleSchemaWidgetFactory(),
+    val factory = ExampleSchemaDiffConsumingNodeFactory(
+      widgets = EmptyExampleSchemaWidgetFactory(),
       mismatchHandler = handler,
     )
 
@@ -214,8 +214,8 @@ class DiffConsumingWidgetFactoryTest {
       }
     }
     val recordingTextInput = RecordingTextInput()
-    val factory = DiffConsumingExampleSchemaWidgetFactory(
-      delegate = object : EmptyExampleSchemaWidgetFactory() {
+    val factory = ExampleSchemaDiffConsumingNodeFactory(
+      widgets = object : EmptyExampleSchemaWidgetFactory() {
         override fun TextInput() = recordingTextInput
       },
       json = json,
@@ -229,7 +229,7 @@ class DiffConsumingWidgetFactoryTest {
   }
 
   @Test fun unknownPropertyThrowsDefaults() {
-    val factory = DiffConsumingExampleSchemaWidgetFactory(EmptyExampleSchemaWidgetFactory())
+    val factory = ExampleSchemaDiffConsumingNodeFactory(EmptyExampleSchemaWidgetFactory())
     val button = factory.create(Id.Root, ThrowingWidgetChildren(), WidgetTag(4))!!
 
     val diff = PropertyDiff(Id(1), PropertyTag(345432))
@@ -242,8 +242,8 @@ class DiffConsumingWidgetFactoryTest {
 
   @Test fun unknownPropertyCallsHandler() {
     val handler = RecordingProtocolMismatchHandler()
-    val factory = DiffConsumingExampleSchemaWidgetFactory(
-      delegate = EmptyExampleSchemaWidgetFactory(),
+    val factory = ExampleSchemaDiffConsumingNodeFactory(
+      widgets = EmptyExampleSchemaWidgetFactory(),
       mismatchHandler = handler,
     )
     val button = factory.create(Id.Root, ThrowingWidgetChildren(), WidgetTag(4))!!
@@ -260,8 +260,8 @@ class DiffConsumingWidgetFactoryTest {
       }
     }
     val recordingTextInput = RecordingTextInput()
-    val factory = DiffConsumingExampleSchemaWidgetFactory(
-      delegate = object : EmptyExampleSchemaWidgetFactory() {
+    val factory = ExampleSchemaDiffConsumingNodeFactory(
+      widgets = object : EmptyExampleSchemaWidgetFactory() {
         override fun TextInput() = recordingTextInput
       },
       json = json,
diff --git a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolBridgeTest.kt b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolBridgeTest.kt
index 6ee248a24..ef2a736d2 100644
--- a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolBridgeTest.kt
+++ b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolBridgeTest.kt
@@ -21,7 +21,7 @@ import app.cash.redwood.protocol.Diff
 import app.cash.redwood.protocol.Id
 import app.cash.redwood.protocol.WidgetTag
 import app.cash.redwood.widget.MutableListChildren
-import example.redwood.widget.DiffConsumingExampleSchemaWidgetFactory
+import example.redwood.widget.ExampleSchemaDiffConsumingNodeFactory
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
@@ -30,7 +30,7 @@ class ProtocolBridgeTest {
   @Test fun insertRootIdThrows() {
     val bridge = ProtocolBridge(
       container = MutableListChildren(),
-      factory = DiffConsumingExampleSchemaWidgetFactory(EmptyExampleSchemaWidgetFactory()),
+      factory = ExampleSchemaDiffConsumingNodeFactory(EmptyExampleSchemaWidgetFactory()),
       eventSink = ::error,
     )
     val diff = Diff(
@@ -53,7 +53,7 @@ class ProtocolBridgeTest {
   @Test fun duplicateIdThrows() {
     val bridge = ProtocolBridge(
       container = MutableListChildren(),
-      factory = DiffConsumingExampleSchemaWidgetFactory(EmptyExampleSchemaWidgetFactory()),
+      factory = ExampleSchemaDiffConsumingNodeFactory(EmptyExampleSchemaWidgetFactory()),
       eventSink = ::error,
     )
     val diff = Diff(