Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove widgets individually in the protocol #2503

Merged
merged 1 commit into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion redwood-protocol-guest/api/redwood-protocol-guest.api
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public final class app/cash/redwood/protocol/guest/DefaultGuestProtocolAdapter :

public abstract class app/cash/redwood/protocol/guest/GuestProtocolAdapter : app/cash/redwood/protocol/EventSink {
public static final field $stable I
public fun <init> ()V
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public abstract fun emitChanges ()V
public abstract fun getRoot ()Lapp/cash/redwood/widget/Widget$Children;
public abstract fun getWidgetSystem ()Lapp/cash/redwood/widget/WidgetSystem;
Expand Down
2 changes: 1 addition & 1 deletion redwood-protocol-guest/api/redwood-protocol-guest.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ abstract interface app.cash.redwood.protocol.guest/ProtocolWidgetSystemFactory {
}

abstract class app.cash.redwood.protocol.guest/GuestProtocolAdapter : app.cash.redwood.protocol/EventSink { // app.cash.redwood.protocol.guest/GuestProtocolAdapter|null[0]
constructor <init>() // app.cash.redwood.protocol.guest/GuestProtocolAdapter.<init>|<init>(){}[0]
constructor <init>(app.cash.redwood.protocol/RedwoodVersion) // app.cash.redwood.protocol.guest/GuestProtocolAdapter.<init>|<init>(app.cash.redwood.protocol.RedwoodVersion){}[0]

abstract val root // app.cash.redwood.protocol.guest/GuestProtocolAdapter.root|{}root[0]
abstract fun <get-root>(): app.cash.redwood.widget/Widget.Children<kotlin/Unit> // app.cash.redwood.protocol.guest/GuestProtocolAdapter.root.<get-root>|<get-root>(){}[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,10 @@ import kotlinx.serialization.json.JsonPrimitive
@OptIn(RedwoodCodegenApi::class)
public class DefaultGuestProtocolAdapter(
public override val json: Json = Json.Default,
// Just keep this parameter because we may need it in the future to vary the protocol.
@Suppress("unused")
hostVersion: RedwoodVersion,
private val widgetSystemFactory: ProtocolWidgetSystemFactory,
private val mismatchHandler: ProtocolMismatchHandler = ProtocolMismatchHandler.Throwing,
) : GuestProtocolAdapter() {
) : GuestProtocolAdapter(hostVersion) {
private var nextValue = Id.Root.value + 1
private val widgets = mutableMapOf<Int, ProtocolWidget>()
private val changes = mutableListOf<Change>()
Expand Down Expand Up @@ -160,7 +158,14 @@ public class DefaultGuestProtocolAdapter(
index: Int,
count: Int,
) {
changes.add(ChildrenChange.Remove(id, tag, index, count))
if (hostSupportsRemoveDetachAndNoCount) {
for (i in index + count - 1 downTo index) {
changes.add(ChildrenChange.Remove(id, tag, i, detach = false))
}
} else {
@Suppress("DEPRECATION") // Supporting older hosts.
changes.add(ChildrenChange.Remove(id, tag, index, count))
}
}

/** Returns the changes accumulated since the last call to this function. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import app.cash.redwood.protocol.ChildrenTag
import app.cash.redwood.protocol.EventSink
import app.cash.redwood.protocol.Id
import app.cash.redwood.protocol.PropertyTag
import app.cash.redwood.protocol.RedwoodVersion
import app.cash.redwood.protocol.WidgetTag
import app.cash.redwood.widget.Widget
import app.cash.redwood.widget.WidgetSystem
Expand All @@ -36,10 +37,22 @@ import kotlinx.serialization.json.Json
*
* This interface is for generated code use only.
*/
public abstract class GuestProtocolAdapter : EventSink {
public abstract class GuestProtocolAdapter(
hostVersion: RedwoodVersion,
) : EventSink {
@RedwoodCodegenApi
public abstract val json: Json

/**
* Whether the host supports the detach parameter on
* [app.cash.redwood.protocol.ChildrenChange.Remove]. This also implies that the `count`
* parameter does not need to be sent.
*
* TODO Remove when minimum-supported host version is 1.17 or higher.
*/
@RedwoodCodegenApi
protected val hostSupportsRemoveDetachAndNoCount: Boolean = hostVersion >= RedwoodVersion("0.17.0-SNAPSHOT")

/**
* The provider of factories of widgets which record property changes and whose children changes
* are also recorded. You **must** attach returned widgets to [root] or the children of a widget
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
package app.cash.redwood.protocol.guest

import androidx.compose.runtime.BroadcastFrameClock
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import app.cash.redwood.compose.WidgetVersion
import app.cash.redwood.protocol.Change
Expand All @@ -38,6 +40,7 @@ import app.cash.redwood.ui.OnBackPressedCallback
import app.cash.redwood.ui.OnBackPressedDispatcher
import app.cash.redwood.ui.UiConfiguration
import assertk.assertThat
import assertk.assertions.containsExactly
import assertk.assertions.isEqualTo
import com.example.redwood.testapp.compose.Button
import com.example.redwood.testapp.compose.Button2
Expand Down Expand Up @@ -288,6 +291,46 @@ class ProtocolTest {
)
}

@Test fun removeIndividuallyForLatestVersion() = runTest {
assertThat(removeNodeRange(latestVersion))
.containsExactly(
ChildrenChange.Remove(Id.Root, ChildrenTag.Root, 2, false),
ChildrenChange.Remove(Id.Root, ChildrenTag.Root, 1, false),
ChildrenChange.Remove(Id.Root, ChildrenTag.Root, 0, false),
)
}

@Suppress("DEPRECATION") // Testing for old behavior.
@Test
fun removeRangeForOldVersions() = runTest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm actually thinking this behavior switching is pointless now. I broke this change out of the large one, but it's always valid to send individual removes to any host. So we always can do it and only worry about whether or not it's valid to send detach=true or not.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just going to leave it for now, land the rest which are kinda stacked on it, and then maybe delete.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I got rid of this in #2509.

assertThat(removeNodeRange(RedwoodVersion("0.16.0")))
.containsExactly(
ChildrenChange.Remove(Id.Root, ChildrenTag.Root, 0, 3),
)
}

private suspend fun TestScope.removeNodeRange(
hostVersion: RedwoodVersion,
): List<Change> {
val (composition) = testProtocolComposition(hostVersion)

var remove by mutableStateOf(false)
val items = @Composable {
Text("one")
Text("two")
Text("three")
}
composition.setContent {
if (!remove) {
items()
}
}
composition.awaitSnapshot()

remove = true
return composition.awaitSnapshot()
}

private fun TestScope.testProtocolComposition(
hostVersion: RedwoodVersion = latestVersion,
): Pair<TestRedwoodComposition<List<Change>>, GuestProtocolAdapter> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,18 @@ public class HostProtocolAdapter<W : Any>(
children.move(change.fromIndex, change.toIndex, change.count)
}

is Remove -> {
if (!change.detach) {
for (childIndex in change.index until change.index + change.count) {
val child = children.nodes[childIndex]
child.visitIds(removeNodeById)
poolOrDetach(child)
is Remove ->
@Suppress("DEPRECATION") // For compatibility with old guests.
{
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)
}
children.remove(change.index, change.count)
}
}

val widget = node.widget
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ class HostProtocolAdapterTest {
id = Id.Root,
tag = ChildrenTag.Root,
index = 0,
count = 1,
detach = false,
),
),
)
Expand Down Expand Up @@ -253,7 +253,7 @@ class HostProtocolAdapterTest {
// Remove root TestRow.
host.sendChanges(
listOf(
Remove(Id.Root, ChildrenTag.Root, 0, 1),
Remove(Id.Root, ChildrenTag.Root, 0, false),
),
)

Expand Down Expand Up @@ -308,9 +308,9 @@ class HostProtocolAdapterTest {
host.sendChanges(
listOf(
// Detach inner TestRow.
Remove(Id(1), ChildrenTag(1), 0, 1, detach = true),
Remove(Id(1), ChildrenTag(1), 0, detach = true),
// Remove outer TestRow.
Remove(Id.Root, ChildrenTag.Root, 0, 1),
Remove(Id.Root, ChildrenTag.Root, 0, detach = false),
// New outer TestRow.
Create(Id(4), WidgetTag(1)),
ModifierChange(Id(4)),
Expand Down
4 changes: 2 additions & 2 deletions redwood-protocol/api/redwood-protocol.api
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +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-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 invoke-ARs5Qwk (IIII)Lapp/cash/redwood/protocol/ChildrenChange$Remove;
public final fun invoke-ARs5Qwk (IIIZ)Lapp/cash/redwood/protocol/ChildrenChange$Remove;
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

Expand Down
3 changes: 2 additions & 1 deletion redwood-protocol/api/redwood-protocol.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ 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, 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 invoke(app.cash.redwood.protocol/Id, app.cash.redwood.protocol/ChildrenTag, 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.Boolean){}[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 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 @@ -247,25 +247,35 @@ public sealed interface ChildrenChange : Change {
@SerialName("tag")
private val _tag: Int,
public val index: Int,
public val count: Int,
@Deprecated("Always 1 after 0.17.0. Will be >1 with earlier guests.")
public val count: Int = 1,
/**
* 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.
*
* Note: This property is only populated for hosts running 0.17.0 or newer.
*/
public val detach: Boolean = false,
) : ChildrenChange {
override val id: Id get() = Id(_id)
override val tag: ChildrenTag get() = ChildrenTag(_tag)

public companion object {
public operator fun invoke(
id: Id,
tag: ChildrenTag,
index: Int,
detach: Boolean,
): Remove = Remove(id.value, tag.value, index, 1, detach)

@Deprecated("Count is only supported on hosts older than 0.17.0.")
public operator fun invoke(
id: Id,
tag: ChildrenTag,
index: Int,
count: Int,
detach: Boolean = false,
): Remove = Remove(id.value, tag.value, index, count, detach)
): Remove = Remove(id.value, tag.value, index, count, false)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ class ProtocolTest {
Create(Id(1), WidgetTag(2)),
ChildrenChange.Add(Id(1), ChildrenTag(2), Id(3), 4),
ChildrenChange.Move(Id(1), ChildrenTag(2), 3, 4, 5),
ChildrenChange.Remove(Id(4), ChildrenTag(3), 2, 1),
@Suppress("DEPRECATION") // Testing for compatibility.
ChildrenChange.Remove(Id(4), ChildrenTag(3), 2, 10),
ChildrenChange.Remove(Id(5), ChildrenTag(3), 2, detach = true),
ModifierChange(
Id(1),
listOf(
Expand Down Expand Up @@ -90,7 +92,8 @@ class ProtocolTest {
"""["create",{"id":1,"tag":2}],""" +
"""["add",{"id":1,"tag":2,"childId":3,"index":4}],""" +
"""["move",{"id":1,"tag":2,"fromIndex":3,"toIndex":4,"count":5}],""" +
"""["remove",{"id":4,"tag":3,"index":2,"count":1}],""" +
"""["remove",{"id":4,"tag":3,"index":2,"count":10}],""" +
"""["remove",{"id":5,"tag":3,"index":2,"detach":true}],""" +
"""["modifier",{"id":1,"elements":[[1,{}],[2,3],[3,[]],[4],[5]]}],""" +
"""["property",{"id":1,"widget":2,"tag":2,"value":"hello"}],""" +
"""["property",{"id":1,"widget":2,"tag":2}]""" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class SnapshotChangeListTest {
Move(Id.Root, ChildrenTag.Root, 1, 2, 3),
PropertyChange(Id(1), WidgetTag(1), PropertyTag(1), JsonPrimitive("Hello")),
Add(Id.Root, ChildrenTag.Root, Id(1), 0),
Remove(Id.Root, ChildrenTag.Root, 1, 2),
Remove(Id.Root, ChildrenTag.Root, 1, false),
),
)
}.hasMessage(
Expand All @@ -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, detach=false)
| - Remove(_id=0, _tag=1, index=1, count=1, detach=false)
""".trimMargin(),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,20 @@ internal actual fun GuestProtocolAdapter(
hostVersion: RedwoodVersion,
widgetSystemFactory: ProtocolWidgetSystemFactory,
mismatchHandler: ProtocolMismatchHandler,
): GuestProtocolAdapter = FastGuestProtocolAdapter(json, widgetSystemFactory, mismatchHandler)
): GuestProtocolAdapter = FastGuestProtocolAdapter(
json = json,
hostVersion = hostVersion,
widgetSystemFactory = widgetSystemFactory,
mismatchHandler = mismatchHandler,
)

@OptIn(ExperimentalSerializationApi::class, RedwoodCodegenApi::class)
internal class FastGuestProtocolAdapter(
override val json: Json = Json.Default,
hostVersion: RedwoodVersion,
private val widgetSystemFactory: ProtocolWidgetSystemFactory,
private val mismatchHandler: ProtocolMismatchHandler = ProtocolMismatchHandler.Throwing,
) : GuestProtocolAdapter() {
) : GuestProtocolAdapter(hostVersion) {
private var nextValue = Id.Root.value + 1
private val widgets = JsMap<Int, ProtocolWidget>()
private val changes = JsArray<Change>()
Expand Down Expand Up @@ -208,7 +214,13 @@ internal class FastGuestProtocolAdapter(
val tag = tag
val index = index
val count = count
changes.push(js("""["remove",{"id":id,"tag":tag,"index":index,"count":count}]"""))
if (hostSupportsRemoveDetachAndNoCount) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume in follow-up we'll change the caller to use a different function?

for (i in index + count - 1 downTo index) {
changes.push(js("""["remove",{"id":id,"tag":tag,"index":i}]"""))
}
} else {
changes.push(js("""["remove",{"id":id,"tag":tag,"index":index,"count":count}]"""))
}
}

override fun emitChanges() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ class FastGuestProtocolAdapterTest {
block: (Widget.Children<Unit>, TestSchemaWidgetSystem<Unit>) -> Unit,
): List<Change> {
val guest = FastGuestProtocolAdapter(
// Use latest guest version as the host version to avoid any compatibility behavior.
hostVersion = guestRedwoodVersion,
widgetSystemFactory = TestSchemaProtocolWidgetSystemFactory,
json = json,
mismatchHandler = ProtocolMismatchHandler.Throwing,
Expand Down
Loading