Skip to content

Commit

Permalink
Implement host-side protocol widget reuse
Browse files Browse the repository at this point in the history
This does not yet support sending the detach flag from the guest.
  • Loading branch information
JakeWharton committed Dec 10, 2024
1 parent 660d72f commit b22206a
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import app.cash.redwood.protocol.RedwoodVersion
import app.cash.redwood.protocol.WidgetTag
import app.cash.redwood.widget.ChangeListener
import app.cash.redwood.widget.Widget
import kotlin.collections.mutableMapOf
import kotlin.native.ObjCName

/**
Expand Down Expand Up @@ -107,10 +106,12 @@ public class HostProtocolAdapter<W : Any>(
}

is Remove -> {
for (childIndex in change.index until change.index + change.count) {
val child = children.nodes[childIndex]
child.visitIds(removeNodeById)
poolOrDetach(child)
if (!change.detach) {
for (childIndex in change.index until change.index + change.count) {
val child = children.nodes[childIndex]
child.visitIds(removeNodeById)
poolOrDetach(child)
}
}
children.remove(change.index, change.count)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import app.cash.redwood.protocol.PropertyChange
import app.cash.redwood.protocol.PropertyTag
import app.cash.redwood.protocol.WidgetTag
import app.cash.redwood.protocol.guest.guestRedwoodVersion
import app.cash.redwood.testing.WidgetValue
import app.cash.redwood.widget.MutableListChildren
import assertk.assertFailure
import assertk.assertThat
Expand All @@ -37,7 +38,9 @@ import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf
import assertk.assertions.message
import com.example.redwood.testapp.protocol.host.TestSchemaProtocolFactory
import com.example.redwood.testapp.testing.TestRowValue
import com.example.redwood.testapp.testing.TestSchemaTestingWidgetFactory
import com.example.redwood.testapp.testing.TextValue
import com.example.redwood.testapp.widget.TestSchemaWidgetSystem
import kotlin.test.Test
import kotlin.test.assertFailsWith
Expand Down Expand Up @@ -204,9 +207,10 @@ class HostProtocolAdapterTest {
}

@Test fun entireSubtreeRemoved() {
val container = MutableListChildren<WidgetValue>()
val host = HostProtocolAdapter(
guestVersion = guestRedwoodVersion,
container = MutableListChildren(),
container = container,
factory = TestSchemaProtocolFactory(
widgetSystem = TestSchemaWidgetSystem(
TestSchema = TestSchemaTestingWidgetFactory(),
Expand Down Expand Up @@ -263,4 +267,69 @@ class HostProtocolAdapterTest {
.message()
.isEqualTo("Unknown widget ID 3")
}

@Test fun detachAndAdd() {
val container = MutableListChildren<WidgetValue>()
val host = HostProtocolAdapter(
guestVersion = guestRedwoodVersion,
container = container,
factory = TestSchemaProtocolFactory(
widgetSystem = TestSchemaWidgetSystem(
TestSchema = TestSchemaTestingWidgetFactory(),
RedwoodLayout = RedwoodLayoutTestingWidgetFactory(),
RedwoodLazyLayout = RedwoodLazyLayoutTestingWidgetFactory(),
),
),
eventSink = ::error,
leakDetector = LeakDetector.none(),
)

// TestRow {
// TestRow {
// Text("hello")
host.sendChanges(
listOf(
// TestRow
Create(Id(1), WidgetTag(1)),
ModifierChange(Id(1)),
// TestRow
Create(Id(2), WidgetTag(1)),
ModifierChange(Id(2)),
// Text
Create(Id(3), WidgetTag(3)),
PropertyChange(Id(3), WidgetTag(3), PropertyTag(1), JsonPrimitive("hello")),
ModifierChange(Id(3)),
Add(Id(2), ChildrenTag(1), Id(3), 0),
Add(Id(1), ChildrenTag(1), Id(2), 0),
Add(Id.Root, ChildrenTag.Root, Id(1), 0),
),
)

host.sendChanges(
listOf(
// Detach inner TestRow.
Remove(Id(1), ChildrenTag(1), 0, 1, detach = true),
// Remove outer TestRow.
Remove(Id.Root, ChildrenTag.Root, 0, 1),
// New outer TestRow.
Create(Id(4), WidgetTag(1)),
ModifierChange(Id(4)),
Add(Id.Root, ChildrenTag.Root, Id(4), 0),
// Re-attach inner TestRow.
Add(Id(4), ChildrenTag(1), Id(2), 0),
),
)

assertThat(container.single().value).isEqualTo(
TestRowValue(
children = listOf(
TestRowValue(
children = listOf(
TextValue(text = "hello"),
),
),
),
),
)
}
}
6 changes: 4 additions & 2 deletions redwood-protocol/api/redwood-protocol.api
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,10 @@ public final class app/cash/redwood/protocol/ChildrenChange$Move$Companion {

public final class app/cash/redwood/protocol/ChildrenChange$Remove : app/cash/redwood/protocol/ChildrenChange {
public static final field Companion Lapp/cash/redwood/protocol/ChildrenChange$Remove$Companion;
public synthetic fun <init> (IIIILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (IIIIZLkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun equals (Ljava/lang/Object;)Z
public final fun getCount ()I
public final fun getDetach ()Z
public fun getId-0HhLjSo ()I
public final fun getIndex ()I
public fun getTag-b0W0yNk ()I
Expand All @@ -101,7 +102,8 @@ public synthetic class app/cash/redwood/protocol/ChildrenChange$Remove$$serializ
}

public final class app/cash/redwood/protocol/ChildrenChange$Remove$Companion {
public final fun invoke-ARs5Qwk (IIII)Lapp/cash/redwood/protocol/ChildrenChange$Remove;
public final fun invoke-HpxY78w (IIIIZ)Lapp/cash/redwood/protocol/ChildrenChange$Remove;
public static synthetic fun invoke-HpxY78w$default (Lapp/cash/redwood/protocol/ChildrenChange$Remove$Companion;IIIIZILjava/lang/Object;)Lapp/cash/redwood/protocol/ChildrenChange$Remove;
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

Expand Down
4 changes: 3 additions & 1 deletion redwood-protocol/api/redwood-protocol.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ sealed interface app.cash.redwood.protocol/ChildrenChange : app.cash.redwood.pro
final class Remove : app.cash.redwood.protocol/ChildrenChange { // app.cash.redwood.protocol/ChildrenChange.Remove|null[0]
final val count // app.cash.redwood.protocol/ChildrenChange.Remove.count|{}count[0]
final fun <get-count>(): kotlin/Int // app.cash.redwood.protocol/ChildrenChange.Remove.count.<get-count>|<get-count>(){}[0]
final val detach // app.cash.redwood.protocol/ChildrenChange.Remove.detach|{}detach[0]
final fun <get-detach>(): kotlin/Boolean // app.cash.redwood.protocol/ChildrenChange.Remove.detach.<get-detach>|<get-detach>(){}[0]
final val id // app.cash.redwood.protocol/ChildrenChange.Remove.id|{}id[0]
final fun <get-id>(): app.cash.redwood.protocol/Id // app.cash.redwood.protocol/ChildrenChange.Remove.id.<get-id>|<get-id>(){}[0]
final val index // app.cash.redwood.protocol/ChildrenChange.Remove.index|{}index[0]
Expand All @@ -112,7 +114,7 @@ sealed interface app.cash.redwood.protocol/ChildrenChange : app.cash.redwood.pro
}

final object Companion { // app.cash.redwood.protocol/ChildrenChange.Remove.Companion|null[0]
final fun invoke(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/ChildrenTag, kotlin/Int, kotlin/Int): app.cash.redwood.protocol/ChildrenChange.Remove // app.cash.redwood.protocol/ChildrenChange.Remove.Companion.invoke|invoke(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.ChildrenTag;kotlin.Int;kotlin.Int){}[0]
final fun invoke(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/ChildrenTag, kotlin/Int, kotlin/Int, kotlin/Boolean = ...): app.cash.redwood.protocol/ChildrenChange.Remove // app.cash.redwood.protocol/ChildrenChange.Remove.Companion.invoke|invoke(app.cash.redwood.protocol.Id;app.cash.redwood.protocol.ChildrenTag;kotlin.Int;kotlin.Int;kotlin.Boolean){}[0]
final fun serializer(): kotlinx.serialization/KSerializer<app.cash.redwood.protocol/ChildrenChange.Remove> // app.cash.redwood.protocol/ChildrenChange.Remove.Companion.serializer|serializer(){}[0]
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,12 @@ public sealed interface ChildrenChange : Change {
private val _tag: Int,
public val index: Int,
public val count: Int,
/**
* When true, the associated nodes should only be detached from the tree with the expectation
* that a future [Add] will re-attach them. Otherwise, nodes should be detached and fully
* removed for garbage collection.
*/
public val detach: Boolean = false,
) : ChildrenChange {
override val id: Id get() = Id(_id)
override val tag: ChildrenTag get() = ChildrenTag(_tag)
Expand All @@ -258,7 +264,8 @@ public sealed interface ChildrenChange : Change {
tag: ChildrenTag,
index: Int,
count: Int,
): Remove = Remove(id.value, tag.value, index, count)
detach: Boolean = false,
): Remove = Remove(id.value, tag.value, index, count, detach)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class SnapshotChangeListTest {
|
|Found:
| - Move(_id=0, _tag=1, fromIndex=1, toIndex=2, count=3)
| - Remove(_id=0, _tag=1, index=1, count=2)
| - Remove(_id=0, _tag=1, index=1, count=2, detach=false)
""".trimMargin(),
)
}
Expand Down

0 comments on commit b22206a

Please sign in to comment.