Repository: cashapp/redwood
Pull request #550 (2022-11-23 21:37:12)

Do not double wrap native widgets for diff consumption
Move responsibilities up into a base type for the generated diff consuming node. Additionally, there is no reason for this generated node to actually implement `Widget` and so that is dropped.

This is, admittedly, a little sloppy. It's merely a step on a larger refactoring that I wanted to fire off before the long weekend.


Patch:
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 2ffb36fcc..0f0356177 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -25,6 +25,12 @@ jobs:
 
       - run: ./gradlew build dokkaHtmlMultiModule --parallel -PredwoodNoSamples
 
+      - uses: actions/upload-artifact@v3
+        with:
+          name: site
+          path: build/dokka/htmlMultiModule/
+          if-no-files-found: error
+
   paparazzi:
     runs-on: ubuntu-latest
     steps:
@@ -102,13 +108,19 @@ jobs:
       - success
     steps:
       - uses: actions/checkout@v3
+
+      - uses: actions/download-artifact@v3
+        with:
+          name: site
+          path: site
+
       - uses: actions/setup-java@v3.6.0
         with:
           distribution: 'zulu'
           java-version: 18
       - uses: gradle/gradle-build-action@v2
 
-      - run: ./gradlew publish dokkaHtmlMultiModule
+      - run: ./gradlew publish
         env:
           ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
           ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
@@ -118,6 +130,6 @@ jobs:
         with:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           BRANCH: site
-          FOLDER: build/dokka/htmlMultiModule
+          FOLDER: site
           TARGET_FOLDER: docs/latest/
           CLEAN: true
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 8c442e716..a21b98600 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
@@ -42,23 +42,30 @@ public class DiffConsumingSunspotWidgetFactory<W : Any>(
   private val delegate: SunspotWidgetFactory<W>,
   private val json: Json = Json.Default,
   private val mismatchHandler: ProtocolMismatchHandler = ProtocolMismatchHandler.Throwing,
-) : DiffConsumingWidget.Factory<W> {
+) : DiffConsumingNode.Factory<W> {
   override val RedwoodLayout = DiffConsumingRedwoodLayoutWidgetFactory(delegate.RedwoodLayout, json, mismatchHandler)
 
-  override fun create(kind: Int): DiffConsumingWidget<W>? = when (kind) {
-    1 -> SunspotBox()
-    2 -> SunspotText()
-    3 -> SunspotButton()
-    1_000_001 -> RedwoodLayout.Row()
-    1_000_002 -> RedwoodLayout.Column()
+  override fun create(
+    parentId: Id,
+    parentChildren: Widget.Children<W>,
+    kind: Int,
+  ): DiffConsumingWidget<W>? = when (kind) {
+    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)
     else -> {
       mismatchHandler.onUnknownWidget(kind)
       null
     }
   }
 
-  private fun SunspotBox(): ProtocolSunspotBox<W> {
-    return ProtocolSunspotBox(delegate.SunspotBox(), json, mismatchHandler)
+  private fun SunspotBox(
+    parentId: Id,
+    parentChildren: Widget.Children<W>,
+  ): DiffConsumingSunspotBox<W> {
+    return DiffConsumingSunspotBox(parentId, parentChildren, delegate.SunspotBox(), json, mismatchHandler)
   }
   etc.
 }
@@ -106,24 +113,26 @@ internal fun generateDiffConsumingWidgetFactory(schema: Schema, host: Schema = s
             // 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.DiffConsumingWidgetFactory.parameterizedBy(typeVariableW),
+              WidgetProtocol.DiffConsumingNodeFactory.parameterizedBy(typeVariableW),
             )
             addFunction(
               FunSpec.builder("create")
                 .addModifiers(OVERRIDE)
+                .addParameter("parentId", Protocol.Id)
+                .addParameter("parentChildren", RedwoodWidget.WidgetChildrenOfW)
                 .addParameter("kind", INT)
                 .returns(
-                  WidgetProtocol.DiffConsumingWidget.parameterizedBy(typeVariableW)
+                  WidgetProtocol.DiffConsumingNode.parameterizedBy(typeVariableW)
                     .copy(nullable = true),
                 )
                 .beginControlFlow("return when (kind)")
                 .apply {
                   for (widget in schema.widgets.sortedBy { it.tag }) {
-                    addStatement("%L -> %N()", widget.tag, widget.type.flatName)
+                    addStatement("%L -> %N(parentId, parentChildren)", widget.tag, widget.type.flatName)
                   }
                   for (dependency in schema.dependencies.sortedBy { it.widgets.firstOrNull()?.tag ?: 0U }) {
                     for (widget in dependency.widgets.sortedBy { it.tag }) {
-                      addStatement("%L -> %N.%N()", widget.tag, dependency.name, widget.type.flatName)
+                      addStatement("%L -> %N.%N(parentId, parentChildren)", widget.tag, dependency.name, widget.type.flatName)
                     }
                   }
                 }
@@ -155,9 +164,11 @@ internal fun generateDiffConsumingWidgetFactory(schema: Schema, host: Schema = s
             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(delegate.%N(), json, mismatchHandler)",
+                  "return %T(parentId, parentChildren, delegate.%N(), json, mismatchHandler)",
                   diffConsumingWidgetType,
                   widget.type.flatName,
                 )
@@ -205,23 +216,27 @@ internal class DiffConsumingSunspotButton<W : Any>(
 internal fun generateDiffConsumingWidget(schema: Schema, widget: Widget, host: Schema = schema): FileSpec {
   val type = schema.diffConsumingWidgetType(widget, host)
   val widgetType = schema.widgetType(widget).parameterizedBy(typeVariableW)
-  val protocolType = WidgetProtocol.DiffConsumingWidget.parameterizedBy(typeVariableW)
+  val protocolType = WidgetProtocol.DiffConsumingNode.parameterizedBy(typeVariableW)
   return FileSpec.builder(type.packageName, type.simpleName)
     .addType(
       TypeSpec.classBuilder(type)
         .addModifiers(INTERNAL)
         .addTypeVariable(typeVariableW)
-        .addSuperinterface(protocolType)
+        .superclass(protocolType)
         .primaryConstructor(
           FunSpec.constructorBuilder()
-            .addParameter("delegate", widgetType)
+            .addParameter("parentId", Protocol.Id)
+            .addParameter("parentChildren", RedwoodWidget.WidgetChildrenOfW)
+            .addParameter("widget", widgetType)
             .addParameter("json", KotlinxSerialization.Json)
             .addParameter("mismatchHandler", WidgetProtocol.ProtocolMismatchHandler)
             .build(),
         )
+        .addSuperclassConstructorParameter("parentId")
+        .addSuperclassConstructorParameter("parentChildren")
         .addProperty(
-          PropertySpec.builder("delegate", widgetType, PRIVATE)
-            .initializer("delegate")
+          PropertySpec.builder("widget", widgetType, OVERRIDE)
+            .initializer("widget")
             .build(),
         )
         .addProperty(
@@ -234,31 +249,6 @@ internal fun generateDiffConsumingWidget(schema: Schema, widget: Widget, host: S
             .initializer("mismatchHandler")
             .build(),
         )
-        .addProperty(
-          PropertySpec.builder("value", typeVariableW, OVERRIDE)
-            .getter(
-              FunSpec.getterBuilder()
-                .addStatement("return delegate.value")
-                .build(),
-            )
-            .build(),
-        )
-        .addProperty(
-          PropertySpec.builder("layoutModifiers", Redwood.LayoutModifier, OVERRIDE)
-            .mutable()
-            .getter(
-              FunSpec.getterBuilder()
-                .addStatement("return delegate.layoutModifiers")
-                .build(),
-            )
-            .setter(
-              FunSpec.setterBuilder()
-                .addParameter("value", Redwood.LayoutModifier)
-                .addStatement("delegate.layoutModifiers = value")
-                .build(),
-            )
-            .build(),
-        )
         .apply {
           val (childrens, properties) = widget.traits.partition { it is Children }
           var nextSerializerId = 0
@@ -280,7 +270,7 @@ internal fun generateDiffConsumingWidget(schema: Schema, widget: Widget, host: S
                       }
 
                       addStatement(
-                        "%LU -> delegate.%N(json.decodeFromJsonElement(serializer_%L, diff.value))",
+                        "%LU -> widget.%N(json.decodeFromJsonElement(serializer_%L, diff.value))",
                         trait.tag,
                         trait.name,
                         serializerId,
@@ -317,7 +307,7 @@ internal fun generateDiffConsumingWidget(schema: Schema, widget: Widget, host: S
                       nextControlFlow("else")
                       addStatement("null")
                       endControlFlow()
-                      addStatement("delegate.%1N(%1N)", trait.name)
+                      addStatement("widget.%1N(%1N)", trait.name)
                       endControlFlow()
                     }
 
@@ -346,12 +336,12 @@ internal fun generateDiffConsumingWidget(schema: Schema, widget: Widget, host: S
             FunSpec.builder("children")
               .addModifiers(OVERRIDE)
               .addParameter("tag", U_INT)
-              .returns(RedwoodWidget.WidgetChildrenOfT.copy(nullable = true))
+              .returns(RedwoodWidget.WidgetChildrenOfW.copy(nullable = true))
               .apply {
                 if (childrens.isNotEmpty()) {
                   beginControlFlow("return when (tag)")
                   for (children in childrens) {
-                    addStatement("%LU -> delegate.%N", children.tag, children.name)
+                    addStatement("%LU -> widget.%N", children.tag, children.name)
                   }
                   beginControlFlow("else ->")
                   addStatement("mismatchHandler.onUnknownChildren(%L, tag)", widget.tag)
@@ -370,7 +360,7 @@ internal fun generateDiffConsumingWidget(schema: Schema, widget: Widget, host: S
           FunSpec.builder("updateLayoutModifier")
             .addModifiers(OVERRIDE)
             .addParameter("value", KotlinxSerialization.JsonArray)
-            .addStatement("layoutModifiers = value.%M(json, mismatchHandler)", host.toLayoutModifier)
+            .addStatement("widget.layoutModifiers = value.%M(json, mismatchHandler)", host.toLayoutModifier)
             .build(),
         )
         .build(),
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 15b3ccf8f..6586bda86 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
@@ -42,8 +42,8 @@ internal object ComposeProtocol {
 }
 
 internal object WidgetProtocol {
-  val DiffConsumingWidget = ClassName("app.cash.redwood.protocol.widget", "DiffConsumingWidget")
-  val DiffConsumingWidgetFactory = DiffConsumingWidget.nestedClass("Factory")
+  val DiffConsumingNode = ClassName("app.cash.redwood.protocol.widget", "DiffConsumingNode")
+  val DiffConsumingNodeFactory = DiffConsumingNode.nestedClass("Factory")
   val ProtocolMismatchHandler =
     ClassName("app.cash.redwood.protocol.widget", "ProtocolMismatchHandler")
 }
@@ -58,7 +58,7 @@ internal object Redwood {
 internal object RedwoodWidget {
   val Widget = ClassName("app.cash.redwood.widget", "Widget")
   val WidgetChildren = Widget.nestedClass("Children")
-  val WidgetChildrenOfT = WidgetChildren.parameterizedBy(typeVariableW)
+  val WidgetChildrenOfW = WidgetChildren.parameterizedBy(typeVariableW)
   val WidgetFactory = Widget.nestedClass("Factory")
 }
 
diff --git a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/widgetGeneration.kt b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/widgetGeneration.kt
index ea72fe37b..d2c670a3f 100644
--- a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/widgetGeneration.kt
+++ b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/widgetGeneration.kt
@@ -111,7 +111,7 @@ internal fun generateWidget(schema: Schema, widget: Widget): FileSpec {
               }
               is Children -> {
                 addProperty(
-                  PropertySpec.builder(trait.name, RedwoodWidget.WidgetChildrenOfT)
+                  PropertySpec.builder(trait.name, RedwoodWidget.WidgetChildrenOfW)
                     .addModifiers(PUBLIC, ABSTRACT)
                     .addKdoc("{tag=${trait.tag}}")
                     .build(),
diff --git a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidget.kt b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/DiffConsumingNode.kt
similarity index 69%
rename from redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidget.kt
rename to redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/DiffConsumingNode.kt
index 0e5ab2d35..f66389e56 100644
--- a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidget.kt
+++ b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/DiffConsumingNode.kt
@@ -16,17 +16,29 @@
 package app.cash.redwood.protocol.widget
 
 import app.cash.redwood.protocol.EventSink
+import app.cash.redwood.protocol.Id
 import app.cash.redwood.protocol.PropertyDiff
 import app.cash.redwood.widget.Widget
 import kotlinx.serialization.json.JsonArray
 
 /**
  * A [Widget] which consumes protocol diffs and applies them to a platform-specific representation.
+ *
+ * @suppress
  */
-public interface DiffConsumingWidget<W : Any> : Widget<W> {
-  public fun apply(diff: PropertyDiff, eventSink: EventSink)
+public abstract class DiffConsumingNode<W : Any>(
+  public val parentId: Id,
+  public val parentChildren: Widget.Children<W>,
+) {
+  private var _childIds: MutableList<Id>? = null
+  public val childIds: MutableList<Id>
+    get() = _childIds ?: mutableListOf<Id>().also { _childIds = it }
+
+  public abstract val widget: Widget<W>
+
+  public abstract fun apply(diff: PropertyDiff, eventSink: EventSink)
 
-  public fun updateLayoutModifier(value: JsonArray)
+  public abstract fun updateLayoutModifier(value: JsonArray)
 
   /**
    * Return one of this widget's children groups by its [tag].
@@ -35,7 +47,7 @@ public interface DiffConsumingWidget<W : Any> : Widget<W> {
    * If `null` is returned, the caller should make every effort to ignore these children and
    * continue executing.
    */
-  public fun children(tag: UInt): Widget.Children<W>?
+  public abstract fun children(tag: UInt): Widget.Children<W>?
 
   public interface Factory<W : Any> {
     /**
@@ -45,6 +57,10 @@ public interface DiffConsumingWidget<W : Any> : Widget<W> {
      * If `null` is returned, the caller should make every effort to ignore this widget and
      * continue executing.
      */
-    public fun create(kind: Int): DiffConsumingWidget<W>?
+    public fun create(
+      parentId: Id,
+      parentChildren: Widget.Children<W>,
+      kind: Int,
+    ): DiffConsumingNode<W>?
   }
 }
diff --git a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolBridge.kt b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolBridge.kt
index f00195060..810d7eb6c 100644
--- a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolBridge.kt
+++ b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolBridge.kt
@@ -15,7 +15,6 @@
  */
 package app.cash.redwood.protocol.widget
 
-import app.cash.redwood.LayoutModifier
 import app.cash.redwood.protocol.ChildrenDiff
 import app.cash.redwood.protocol.ChildrenDiff.Companion.RootChildrenTag
 import app.cash.redwood.protocol.Diff
@@ -36,23 +35,24 @@ import kotlinx.serialization.json.JsonArray
  */
 public class ProtocolBridge<W : Any>(
   container: Widget.Children<W>,
-  private val factory: DiffConsumingWidget.Factory<W>,
+  private val factory: DiffConsumingNode.Factory<W>,
   private val eventSink: EventSink,
 ) : DiffSink {
-  private val nodes = mutableMapOf(
-    Id.Root to Node(DiffConsumingProtocolRoot(container), Id.Root, container),
+  private val nodes = mutableMapOf<Id, DiffConsumingNode<W>>(
+    Id.Root to DiffConsumingProtocolRoot(container),
   )
 
   override fun sendDiff(diff: Diff) {
     for (childrenDiff in diff.childrenDiffs) {
-      val node = node(childrenDiff.id)
-      val children = node.widget.children(childrenDiff.tag) ?: continue
+      val id = childrenDiff.id
+      val node = node(id)
+      val children = node.children(childrenDiff.tag) ?: continue
 
       when (childrenDiff) {
         is ChildrenDiff.Insert -> {
-          val childWidget = factory.create(childrenDiff.kind) ?: continue
-          children.insert(childrenDiff.index, childWidget)
-          val old = nodes.put(childrenDiff.childId, Node(childWidget, childrenDiff.id, children))
+          val childWidget = factory.create(id, children, childrenDiff.kind) ?: continue
+          children.insert(childrenDiff.index, childWidget.widget)
+          val old = nodes.put(childrenDiff.childId, childWidget)
           require(old == null) {
             "Insert attempted to replace existing widget with ID ${childrenDiff.childId.value}"
           }
@@ -75,37 +75,26 @@ public class ProtocolBridge<W : Any>(
     for (layoutModifier in diff.layoutModifiers) {
       val node = node(layoutModifier.id)
       val childIndex = node(node.parentId).childIds.indexOf(layoutModifier.id)
-      node.widget.updateLayoutModifier(layoutModifier.elements)
+      node.updateLayoutModifier(layoutModifier.elements)
       node.parentChildren.onLayoutModifierUpdated(childIndex)
     }
 
     for (propertyDiff in diff.propertyDiffs) {
-      node(propertyDiff.id).widget.apply(propertyDiff, eventSink)
+      node(propertyDiff.id).apply(propertyDiff, eventSink)
     }
   }
 
-  private fun node(id: Id): Node<W> {
+  private fun node(id: Id): DiffConsumingNode<W> {
     return checkNotNull(nodes[id]) { "Unknown widget ID $id" }
   }
 }
 
-private class Node<W : Any>(
-  val widget: DiffConsumingWidget<W>,
-  val parentId: Id,
-  val parentChildren: Widget.Children<W>,
-) {
-  private var _childIds: MutableList<Id>? = null
-  val childIds: MutableList<Id>
-    get() = _childIds ?: mutableListOf<Id>().also { _childIds = it }
-}
-
 private class DiffConsumingProtocolRoot<W : Any>(
   private val children: Widget.Children<W>,
-) : DiffConsumingWidget<W> {
-  override var layoutModifiers: LayoutModifier
-    get() = throw AssertionError()
-    set(value) = throw AssertionError("unexpected: $value")
-
+) : DiffConsumingNode<W>(
+  parentId = Id.Root, // This value is a lie, but it's never accessed on this node.
+  parentChildren = children, // This value is a lie, but it's never accessed on this node.
+) {
   override fun updateLayoutModifier(value: JsonArray) {
     throw AssertionError("unexpected: $value")
   }
@@ -119,5 +108,5 @@ private class DiffConsumingProtocolRoot<W : Any>(
     else -> throw AssertionError("unexpected: $tag")
   }
 
-  override val value: W get() = throw AssertionError()
+  override val widget: Widget<W> get() = throw AssertionError()
 }
diff --git a/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/TreehouseApp.kt b/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/TreehouseApp.kt
index 451964700..30b73425d 100644
--- a/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/TreehouseApp.kt
+++ b/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/TreehouseApp.kt
@@ -17,7 +17,7 @@ package app.cash.redwood.treehouse
 
 import app.cash.redwood.protocol.Diff
 import app.cash.redwood.protocol.EventSink
-import app.cash.redwood.protocol.widget.DiffConsumingWidget
+import app.cash.redwood.protocol.widget.DiffConsumingNode
 import app.cash.redwood.protocol.widget.ProtocolBridge
 import app.cash.redwood.widget.Widget
 import app.cash.zipline.Zipline
@@ -178,7 +178,7 @@ public class TreehouseApp<A : Any> internal constructor(
       @Suppress("UNCHECKED_CAST") // We don't have a type parameter for the widget type.
       val bridge = ProtocolBridge(
         container = view.children as Widget.Children<Any>,
-        factory = widgetFactory as DiffConsumingWidget.Factory<Any>,
+        factory = widgetFactory as DiffConsumingNode.Factory<Any>,
         eventSink = eventSink,
       )
 
diff --git a/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/TreehouseView.kt b/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/TreehouseView.kt
index 793455ae3..c7bdec585 100644
--- a/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/TreehouseView.kt
+++ b/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/TreehouseView.kt
@@ -15,7 +15,7 @@
  */
 package app.cash.redwood.treehouse
 
-import app.cash.redwood.protocol.widget.DiffConsumingWidget
+import app.cash.redwood.protocol.widget.DiffConsumingNode
 import app.cash.redwood.protocol.widget.ProtocolMismatchHandler
 import app.cash.redwood.widget.Widget
 import kotlinx.coroutines.flow.StateFlow
@@ -42,7 +42,7 @@ public interface TreehouseView<A : Any> {
       app: TreehouseApp<A>,
       json: Json,
       protocolMismatchHandler: ProtocolMismatchHandler,
-    ): DiffConsumingWidget.Factory<*>
+    ): DiffConsumingNode.Factory<*>
   }
 
   public open class CodeListener {
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 1a4718aa4..e28bbe307 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
@@ -18,7 +18,7 @@ package app.cash.zipline.samples.emojisearch.views
 import android.os.Bundle
 import androidx.activity.ComponentActivity
 import app.cash.redwood.compose.AndroidUiDispatcher.Companion.Main
-import app.cash.redwood.protocol.widget.DiffConsumingWidget
+import app.cash.redwood.protocol.widget.DiffConsumingNode
 import app.cash.redwood.protocol.widget.ProtocolMismatchHandler
 import app.cash.redwood.treehouse.TreehouseApp
 import app.cash.redwood.treehouse.TreehouseLauncher
@@ -47,7 +47,7 @@ class EmojiSearchActivity : ComponentActivity() {
         app: TreehouseApp<EmojiSearchPresenter>,
         json: Json,
         protocolMismatchHandler: ProtocolMismatchHandler,
-      ): DiffConsumingWidget.Factory<*> {
+      ): DiffConsumingNode.Factory<*> {
         return DiffConsumingEmojiSearchWidgetFactory(
           delegate = AndroidViewEmojiSearchWidgetFactory(
             context = this@EmojiSearchActivity,
diff --git a/samples/emoji-search/ios/app/EmojiSearchApp/EmojiSearchViewController.swift b/samples/emoji-search/ios/app/EmojiSearchApp/EmojiSearchViewController.swift
index 77fd3c8d0..117eccff5 100644
--- a/samples/emoji-search/ios/app/EmojiSearchApp/EmojiSearchViewController.swift
+++ b/samples/emoji-search/ios/app/EmojiSearchApp/EmojiSearchViewController.swift
@@ -51,7 +51,7 @@ class EmojiSearchContent : Redwood_treehouseTreehouseViewContent {
 }
 
 class EmojiSearchWidgetSystem : Redwood_treehouseTreehouseViewWidgetSystem {
-    func widgetFactory(app: Redwood_treehouseTreehouseApp<AnyObject>, json: Kotlinx_serialization_jsonJson, protocolMismatchHandler: Redwood_protocol_widgetProtocolMismatchHandler) -> Redwood_protocol_widgetDiffConsumingWidgetFactory {
+    func widgetFactory(app: Redwood_treehouseTreehouseApp<AnyObject>, json: Kotlinx_serialization_jsonJson, protocolMismatchHandler: Redwood_protocol_widgetProtocolMismatchHandler) -> Redwood_protocol_widgetDiffConsumingNodeFactory {
         return ExposedKt.diffConsumingEmojiSearchWidgetFactory(
             delegate: IosEmojiSearchWidgetFactory(treehouseApp: app, widgetSystem: self),
             json: json,


Test patch:
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/DiffConsumingWidgetFactoryTest.kt
index ee2df0d5a..4af336df6 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/DiffConsumingWidgetFactoryTest.kt
@@ -42,7 +42,7 @@ class DiffConsumingWidgetFactoryTest {
     val factory = DiffConsumingExampleSchemaWidgetFactory(EmptyExampleSchemaWidgetFactory())
 
     val t = assertFailsWith<IllegalArgumentException> {
-      factory.create(345432)
+      factory.create(Id.Root, ThrowingWidgetChildren(), 345432)
     }
     assertEquals("Unknown widget kind 345432", t.message)
   }
@@ -54,7 +54,7 @@ class DiffConsumingWidgetFactoryTest {
       mismatchHandler = handler,
     )
 
-    assertNull(factory.create(345432))
+    assertNull(factory.create(Id.Root, ThrowingWidgetChildren(), 345432))
 
     assertEquals("Unknown widget 345432", handler.events.single())
   }
@@ -72,7 +72,7 @@ class DiffConsumingWidgetFactoryTest {
       },
       json = json,
     )
-    val textInput = factory.create(5)!!
+    val textInput = factory.create(Id.Root, ThrowingWidgetChildren(), 5)!!
 
     textInput.updateLayoutModifier(
       buildJsonArray {
@@ -90,7 +90,7 @@ class DiffConsumingWidgetFactoryTest {
     )
 
     with(object : TestScope {}) {
-      assertEquals(LayoutModifier.customType(10.seconds), textInput.layoutModifiers)
+      assertEquals(LayoutModifier.customType(10.seconds), recordingTextInput.layoutModifiers)
     }
   }
 
@@ -107,7 +107,7 @@ class DiffConsumingWidgetFactoryTest {
       },
       json = json,
     )
-    val textInput = factory.create(5)!!
+    val textInput = factory.create(Id.Root, ThrowingWidgetChildren(), 5)!!
 
     textInput.updateLayoutModifier(
       buildJsonArray {
@@ -125,13 +125,13 @@ class DiffConsumingWidgetFactoryTest {
     )
 
     with(object : TestScope {}) {
-      assertEquals(LayoutModifier.customTypeWithDefault(10.seconds, "sup"), textInput.layoutModifiers)
+      assertEquals(LayoutModifier.customTypeWithDefault(10.seconds, "sup"), recordingTextInput.layoutModifiers)
     }
   }
 
   @Test fun unknownLayoutModifierThrowsDefault() {
     val factory = DiffConsumingExampleSchemaWidgetFactory(EmptyExampleSchemaWidgetFactory())
-    val button = factory.create(4)!!
+    val button = factory.create(Id.Root, ThrowingWidgetChildren(), 4)!!
 
     val t = assertFailsWith<IllegalArgumentException> {
       button.updateLayoutModifier(
@@ -164,7 +164,7 @@ class DiffConsumingWidgetFactoryTest {
       mismatchHandler = handler,
     )
 
-    val textInput = factory.create(5)!!
+    val textInput = factory.create(Id.Root, ThrowingWidgetChildren(), 5)!!
     textInput.updateLayoutModifier(
       buildJsonArray {
         add(
@@ -192,7 +192,7 @@ class DiffConsumingWidgetFactoryTest {
 
   @Test fun unknownChildrenThrowsDefault() {
     val factory = DiffConsumingExampleSchemaWidgetFactory(EmptyExampleSchemaWidgetFactory())
-    val button = factory.create(4)!!
+    val button = factory.create(Id.Root, ThrowingWidgetChildren(), 4)!!
 
     val t = assertFailsWith<IllegalArgumentException> {
       button.children(345432U)
@@ -207,7 +207,7 @@ class DiffConsumingWidgetFactoryTest {
       mismatchHandler = handler,
     )
 
-    val button = factory.create(4)!!
+    val button = factory.create(Id.Root, ThrowingWidgetChildren(), 4)!!
     assertNull(button.children(345432U))
 
     assertEquals("Unknown children 345432 for 4", handler.events.single())
@@ -226,7 +226,7 @@ class DiffConsumingWidgetFactoryTest {
       },
       json = json,
     )
-    val textInput = factory.create(5)!!
+    val textInput = factory.create(Id.Root, ThrowingWidgetChildren(), 5)!!
 
     val throwingEventSink = EventSink { error(it) }
     textInput.apply(PropertyDiff(Id(1U), 2U, JsonPrimitive("PT10S")), throwingEventSink)
@@ -236,7 +236,7 @@ class DiffConsumingWidgetFactoryTest {
 
   @Test fun unknownPropertyThrowsDefaults() {
     val factory = DiffConsumingExampleSchemaWidgetFactory(EmptyExampleSchemaWidgetFactory())
-    val button = factory.create(4) as DiffConsumingWidget<*>
+    val button = factory.create(Id.Root, ThrowingWidgetChildren(), 4)!!
 
     val diff = PropertyDiff(Id(1U), 345432U)
     val eventSink = EventSink { throw UnsupportedOperationException() }
@@ -252,7 +252,7 @@ class DiffConsumingWidgetFactoryTest {
       delegate = EmptyExampleSchemaWidgetFactory(),
       mismatchHandler = handler,
     )
-    val button = factory.create(4) as DiffConsumingWidget<*>
+    val button = factory.create(Id.Root, ThrowingWidgetChildren(), 4)!!
 
     button.apply(PropertyDiff(Id(1U), 345432U)) { throw UnsupportedOperationException() }
 
@@ -272,7 +272,7 @@ class DiffConsumingWidgetFactoryTest {
       },
       json = json,
     )
-    val textInput = factory.create(5)!!
+    val textInput = factory.create(Id.Root, ThrowingWidgetChildren(), 5)!!
 
     val eventSink = RecordingEventSink()
     textInput.apply(PropertyDiff(Id(1U), 4U, JsonPrimitive(true)), eventSink)
diff --git a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ThrowingWidgetChildren.kt b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ThrowingWidgetChildren.kt
new file mode 100644
index 000000000..fc3ed5d9f
--- /dev/null
+++ b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ThrowingWidgetChildren.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2022 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package app.cash.redwood.protocol.widget
+
+import app.cash.redwood.widget.Widget
+
+class ThrowingWidgetChildren<W : Any> : Widget.Children<W> {
+  override fun insert(index: Int, widget: Widget<W>) = throw AssertionError()
+  override fun move(fromIndex: Int, toIndex: Int, count: Int) = throw AssertionError()
+  override fun remove(index: Int, count: Int) = throw AssertionError()
+  override fun onLayoutModifierUpdated(index: Int) = throw AssertionError()
+}