Repository: cashapp/redwood
Pull request #364 (2022-09-09 20:01:41)

Make the existing MutableListChildren more useful
- Implements MutableList as its name suggests (previously it just wrapped one)
- Provide a callback whenever it changes via the Widget.Children mechanism


Patch:
diff --git a/redwood-treehouse/src/iosMain/kotlin/app/cash/redwood/treehouse/treehouseLauncherIos.kt b/redwood-treehouse/src/iosMain/kotlin/app/cash/redwood/treehouse/treehouseLauncherIos.kt
index 5152cc676..386a89656 100644
--- a/redwood-treehouse/src/iosMain/kotlin/app/cash/redwood/treehouse/treehouseLauncherIos.kt
+++ b/redwood-treehouse/src/iosMain/kotlin/app/cash/redwood/treehouse/treehouseLauncherIos.kt
@@ -20,7 +20,7 @@ import app.cash.redwood.protocol.ChildrenDiff.Companion.RootChildrenTag
 import app.cash.redwood.protocol.EventSink
 import app.cash.redwood.protocol.PropertyDiff
 import app.cash.redwood.protocol.widget.DiffConsumingWidget
-import app.cash.redwood.widget.UIViewChildren
+import app.cash.redwood.widget.MutableListChildren
 import app.cash.zipline.loader.ManifestVerifier
 import app.cash.zipline.loader.ZiplineCache
 import app.cash.zipline.loader.ZiplineHttpClient
@@ -34,6 +34,9 @@ import platform.Foundation.NSLog
 import platform.Foundation.NSTemporaryDirectory
 import platform.Foundation.NSThread
 import platform.UIKit.UIView
+import platform.UIKit.addSubview
+import platform.UIKit.removeFromSuperview
+import platform.UIKit.subviews
 
 public fun TreehouseLauncher(
   httpClient: ZiplineHttpClient,
@@ -94,7 +97,11 @@ internal class IosTreehouseDispatchers : TreehouseDispatchers {
 internal class ProtocolDisplayRoot(
   override val value: UIView,
 ) : DiffConsumingWidget<UIView> {
-  private val children = UIViewChildren(value)
+  private val children = MutableListChildren { newViews ->
+    @Suppress("UNCHECKED_CAST") // cinterop loses the generic.
+    (value.subviews as List<UIView>).forEach(UIView::removeFromSuperview)
+    newViews.forEach(value::addSubview)
+  }
 
   override var layoutModifiers: LayoutModifier = LayoutModifier
 
diff --git a/redwood-widget/src/commonMain/kotlin/app/cash/redwood/widget/MutableListChildren.kt b/redwood-widget/src/commonMain/kotlin/app/cash/redwood/widget/MutableListChildren.kt
index a5e4eeed3..3cbf4f7ab 100644
--- a/redwood-widget/src/commonMain/kotlin/app/cash/redwood/widget/MutableListChildren.kt
+++ b/redwood-widget/src/commonMain/kotlin/app/cash/redwood/widget/MutableListChildren.kt
@@ -17,12 +17,23 @@ package app.cash.redwood.widget
 
 import kotlin.jvm.JvmOverloads
 
+/**
+ * A [MutableList] that is also a [Widget.Children].
+ *
+ * @param list Optional existing [MutableList] instance to wrap.
+ * @param onUpdate Optional callback invoked when contents change via the [Widget.Children] API.
+ */
 public class MutableListChildren<T : Any>
 @JvmOverloads constructor(
-  public val list: MutableList<T> = mutableListOf(),
-) : Widget.Children<T>, Iterable<T> {
+  private val list: MutableList<T> = mutableListOf(),
+  private val onUpdate: (List<T>) -> Unit = {},
+) : Widget.Children<T>, MutableList<T> by list {
+  /** @param onUpdate Callback invoked when contents change via the [Widget.Children] API. */
+  public constructor(onUpdate: (List<T>) -> Unit) : this(mutableListOf(), onUpdate = onUpdate)
+
   override fun insert(index: Int, widget: T) {
     list.add(index, widget)
+    onUpdate(list)
   }
 
   override fun move(fromIndex: Int, toIndex: Int, count: Int) {
@@ -58,6 +69,8 @@ public class MutableListChildren<T : Any>
       subView.clear()
       list.addAll(dest, subCopy)
     }
+
+    onUpdate(list)
   }
 
   override fun remove(index: Int, count: Int) {
@@ -81,11 +94,12 @@ public class MutableListChildren<T : Any>
     } else {
       list.subList(index, index + count).clear()
     }
+
+    onUpdate(list)
   }
 
   override fun clear() {
     list.clear()
+    onUpdate(list)
   }
-
-  override fun iterator(): Iterator<T> = list.iterator()
 }
diff --git a/redwood-widget/src/iosMain/kotlin/app/cash/redwood/widget/UIViewChildren.kt b/redwood-widget/src/iosMain/kotlin/app/cash/redwood/widget/UIViewChildren.kt
deleted file mode 100644
index c9066709c..000000000
--- a/redwood-widget/src/iosMain/kotlin/app/cash/redwood/widget/UIViewChildren.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2021 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.widget
-
-import kotlinx.cinterop.convert
-import platform.UIKit.UIView
-import platform.UIKit.insertSubview
-import platform.UIKit.removeFromSuperview
-import platform.UIKit.subviews
-import platform.darwin.NSInteger
-
-public class UIViewChildren(
-  private val root: UIView,
-) : Widget.Children<UIView> {
-  override fun insert(index: Int, widget: UIView) {
-    root.insertSubview(widget, index.convert<NSInteger>())
-  }
-
-  override fun move(fromIndex: Int, toIndex: Int, count: Int) {
-    val views = Array(count) {
-      val subview = root.subviews[fromIndex] as UIView
-      subview.removeFromSuperview()
-      subview
-    }
-
-    val newIndex = if (toIndex > fromIndex) {
-      toIndex - count
-    } else {
-      toIndex
-    }
-    views.forEachIndexed { offset, view ->
-      root.insertSubview(view, (newIndex + offset).convert<NSInteger>())
-    }
-  }
-
-  override fun remove(index: Int, count: Int) {
-    repeat(count) {
-      (root.subviews[index] as UIView).removeFromSuperview()
-    }
-  }
-
-  override fun clear() {
-    for (subview in root.subviews) {
-      (subview as UIView).removeFromSuperview()
-    }
-  }
-}
diff --git a/samples/counter/ios/shared/src/commonMain/kotlin/example/ios/sunspot/IosSunspotBox.kt b/samples/counter/ios/shared/src/commonMain/kotlin/example/ios/sunspot/IosSunspotBox.kt
index 81bca8aff..f09d46d39 100644
--- a/samples/counter/ios/shared/src/commonMain/kotlin/example/ios/sunspot/IosSunspotBox.kt
+++ b/samples/counter/ios/shared/src/commonMain/kotlin/example/ios/sunspot/IosSunspotBox.kt
@@ -16,11 +16,14 @@
 package example.ios.sunspot
 
 import app.cash.redwood.LayoutModifier
+import app.cash.redwood.widget.MutableListChildren
 import example.sunspot.widget.SunspotBox
 import platform.UIKit.UILayoutConstraintAxisHorizontal
 import platform.UIKit.UIStackView
 import platform.UIKit.UIStackViewDistributionFillEqually
 import platform.UIKit.UIView
+import platform.UIKit.removeFromSuperview
+import platform.UIKit.subviews
 
 class IosSunspotBox(
   override val value: UIStackView = UIStackView().apply {
@@ -30,5 +33,9 @@ class IosSunspotBox(
 ) : SunspotBox<UIView> {
   override var layoutModifiers: LayoutModifier = LayoutModifier
 
-  override val children = UIStackViewChildren(value)
+  override val children = MutableListChildren { newViews ->
+    @Suppress("UNCHECKED_CAST") // cinterop loses the generic.
+    (value.subviews as List<UIView>).forEach(UIView::removeFromSuperview)
+    newViews.forEach(value::addArrangedSubview)
+  }
 }
diff --git a/samples/counter/ios/shared/src/commonMain/kotlin/example/ios/sunspot/UIStackViewChildren.kt b/samples/counter/ios/shared/src/commonMain/kotlin/example/ios/sunspot/UIStackViewChildren.kt
deleted file mode 100644
index bb5438890..000000000
--- a/samples/counter/ios/shared/src/commonMain/kotlin/example/ios/sunspot/UIStackViewChildren.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2021 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 example.ios.sunspot
-
-import app.cash.redwood.widget.Widget
-import kotlinx.cinterop.convert
-import platform.UIKit.UIStackView
-import platform.UIKit.UIView
-import platform.UIKit.removeFromSuperview
-import platform.UIKit.subviews
-
-class UIStackViewChildren(
-  private val root: UIStackView,
-) : Widget.Children<UIView> {
-  override fun insert(index: Int, widget: UIView) {
-    root.insertArrangedSubview(widget, index.convert())
-  }
-
-  override fun move(fromIndex: Int, toIndex: Int, count: Int) {
-    val views = Array(count) {
-      val subview = root.subviews[fromIndex] as UIView
-      subview.removeFromSuperview()
-      subview
-    }
-
-    val newIndex = if (toIndex > fromIndex) {
-      toIndex - count
-    } else {
-      toIndex
-    }
-    views.forEachIndexed { offset, view ->
-      root.insertArrangedSubview(view, (newIndex + offset).convert())
-    }
-  }
-
-  override fun remove(index: Int, count: Int) {
-    repeat(count) {
-      (root.subviews[index] as UIView).removeFromSuperview()
-    }
-  }
-
-  override fun clear() {
-    for (subview in root.subviews) {
-      (subview as UIView).removeFromSuperview()
-    }
-  }
-}


Test patch:
diff --git a/redwood-widget/src/commonTest/kotlin/app/cash/redwood/widget/MutableListChildrenTest.kt b/redwood-widget/src/commonTest/kotlin/app/cash/redwood/widget/MutableListChildrenTest.kt
index b7106fce6..1a8a8f0a2 100644
--- a/redwood-widget/src/commonTest/kotlin/app/cash/redwood/widget/MutableListChildrenTest.kt
+++ b/redwood-widget/src/commonTest/kotlin/app/cash/redwood/widget/MutableListChildrenTest.kt
@@ -19,14 +19,41 @@ import kotlin.test.Test
 import kotlin.test.assertEquals
 
 class MutableListChildrenTest : AbstractWidgetChildrenTest<String>() {
-  override val children = MutableListChildren<String>()
+  private var updateCount = Int.MIN_VALUE
+  override val children = MutableListChildren<String> { updateCount++ }
   override fun widget(name: String) = name
-  override fun names() = children.list
+  override fun names() = children.toList()
 
-  @Test fun iterableIteratesChildren() {
-    assertEquals(emptyList(), children.toList())
+  @Test fun insertCallsUpdate() {
+    updateCount = 0
+    children.insert(0, "one")
+    assertEquals(1, updateCount)
+  }
+
+  @Test fun moveCallsUpdate() {
+    children.insert(0, "one")
+    children.insert(1, "two")
+
+    updateCount = 0
+    children.move(0, 1, 1)
+    assertEquals(1, updateCount)
+  }
+
+  @Test fun removeCallsUpdate() {
+    children.insert(0, "one")
+    children.insert(1, "two")
+
+    updateCount = 0
+    children.remove(0, 1)
+    assertEquals(1, updateCount)
+  }
+
+  @Test fun clearCallsUpdate() {
+    children.insert(0, "one")
+    children.insert(1, "two")
 
-    children.insert(0, widget("one"))
-    assertEquals(listOf("one"), children.toList())
+    updateCount = 0
+    children.clear()
+    assertEquals(1, updateCount)
   }
 }
diff --git a/redwood-widget/src/iosTest/kotlin/app/cash/redwood/widget/UIViewChildrenTest.kt b/redwood-widget/src/iosTest/kotlin/app/cash/redwood/widget/UIViewChildrenTest.kt
deleted file mode 100644
index 8945a0d4f..000000000
--- a/redwood-widget/src/iosTest/kotlin/app/cash/redwood/widget/UIViewChildrenTest.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2021 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.widget
-
-import platform.UIKit.UILabel
-import platform.UIKit.UIView
-import platform.UIKit.subviews
-
-class UIViewChildrenTest : AbstractWidgetChildrenTest<UIView>() {
-  private val parent = UIView()
-  override val children = UIViewChildren(parent)
-
-  override fun widget(name: String): UIView {
-    return UILabel().apply {
-      text = name
-    }
-  }
-
-  override fun names(): List<String> {
-    return parent.subviews.map { (it as UILabel).text!! }
-  }
-}