diff --git a/redwood-lazylayout-uiview/src/commonMain/kotlin/app/cash/redwood/lazylayout/uiview/UIViewLazyList.kt b/redwood-lazylayout-uiview/src/commonMain/kotlin/app/cash/redwood/lazylayout/uiview/UIViewLazyList.kt index 10c9f4d975..2a3cd0671f 100644 --- a/redwood-lazylayout-uiview/src/commonMain/kotlin/app/cash/redwood/lazylayout/uiview/UIViewLazyList.kt +++ b/redwood-lazylayout-uiview/src/commonMain/kotlin/app/cash/redwood/lazylayout/uiview/UIViewLazyList.kt @@ -74,19 +74,6 @@ internal open class UIViewLazyList( protected var onViewportChanged: ((firstVisibleItemIndex: Int, lastVisibleItemIndex: Int) -> Unit)? = null private val processor = object : LazyListUpdateProcessor() { - override fun createView( - binding: Binding, - index: Int, - ): LazyListContainerCell { - val result = tableView.dequeueReusableCellWithIdentifier( - identifier = reuseIdentifier, - forIndexPath = NSIndexPath.indexPathForItem(index.convert(), 0.convert()), - ) as LazyListContainerCell - require(result.binding == null) - result.binding = binding - return result - } - override fun insertRows(index: Int, count: Int) { // TODO(jwilson): pass a range somehow when 'count' is large? tableView.insertRowsAtIndexPaths( @@ -120,7 +107,26 @@ internal open class UIViewLazyList( override fun tableView( tableView: UITableView, cellForRowAtIndexPath: NSIndexPath, - ) = processor.getView(cellForRowAtIndexPath.item.toInt()) + ): LazyListContainerCell { + val index = cellForRowAtIndexPath.item.toInt() + return processor.getOrCreateBoundView(index) { binding -> + createView(tableView, binding, index) + } + } + + private fun createView( + tableView: UITableView, + binding: Binding, + index: Int, + ): LazyListContainerCell { + val result = tableView.dequeueReusableCellWithIdentifier( + identifier = reuseIdentifier, + forIndexPath = NSIndexPath.indexPathForItem(index.convert(), 0.convert()), + ) as LazyListContainerCell + require(result.binding == null) + result.binding = binding + return result + } } private val tableViewDelegate: UITableViewDelegateProtocol = diff --git a/redwood-lazylayout-widget/src/commonMain/kotlin/app/cash/redwood/lazylayout/widget/LazyListUpdateProcessor.kt b/redwood-lazylayout-widget/src/commonMain/kotlin/app/cash/redwood/lazylayout/widget/LazyListUpdateProcessor.kt index 9ec9b1f86f..9305b9ff02 100644 --- a/redwood-lazylayout-widget/src/commonMain/kotlin/app/cash/redwood/lazylayout/widget/LazyListUpdateProcessor.kt +++ b/redwood-lazylayout-widget/src/commonMain/kotlin/app/cash/redwood/lazylayout/widget/LazyListUpdateProcessor.kt @@ -270,46 +270,44 @@ public abstract class LazyListUpdateProcessor, W : Any> { return loaded } - public fun getView(index: Int): V { - return when { - index < itemsBefore.size -> getOrCreatePlaceholder(itemsBefore, index, index) - index < itemsBefore.size + loadedItems.size -> getLoadedView(index) - else -> getOrCreatePlaceholder(itemsAfter, index - itemsBefore.size - loadedItems.size, index) - } - } - - private fun getLoadedView(index: Int): V { - val binding = loadedItems[index - itemsBefore.size] + public fun getOrCreateBoundView( + index: Int, + createView: (binding: Binding) -> V, + ): V { + val binding = requireBindingAtIndex(index) var view = binding.view if (view == null) { - view = createView(binding, index) + view = createView(binding) binding.bind(view) } return view } - private fun getOrCreatePlaceholder( - placeholders: SparseList?>, - placeholderIndex: Int, - cellIndex: Int, - ): V { - var binding = placeholders[placeholderIndex] - - // Return an existing placeholder. - if (binding != null) return binding.view!! + public fun bind(index: Int, view: V): Binding { + return requireBindingAtIndex(index).apply { + bind(view) + } + } - // Create a new placeholder cell and bind it to the view. - binding = Binding( - processor = this, + /** + * Callers must immediately call [Binding.bind] on the result if it isn't already bound. Otherwise + * we could leak placeholder bindings. + */ + private fun requireBindingAtIndex(index: Int): Binding { + fun createBinding() = Binding( + processor = this@LazyListUpdateProcessor, isPlaceholder = true, - ) - binding.content = takePlaceholder() - val view = createView(binding, cellIndex) - binding.bind(view) - placeholders.set(placeholderIndex, binding) - return view + ).apply { + content = takePlaceholder() + } + + return when { + index < itemsBefore.size -> itemsBefore.getOrCreate(index, ::createBinding) + index < itemsBefore.size + loadedItems.size -> loadedItems[index - itemsBefore.size] + else -> itemsAfter.getOrCreate(index - itemsBefore.size - loadedItems.size, ::createBinding) + } } private fun takePlaceholder(): Widget { @@ -317,8 +315,6 @@ public abstract class LazyListUpdateProcessor, W : Any> { ?: throw IllegalStateException("no more placeholders!") } - protected abstract fun createView(binding: Binding, index: Int): V - protected abstract fun insertRows(index: Int, count: Int) protected abstract fun deleteRows(index: Int, count: Int) diff --git a/redwood-lazylayout-widget/src/commonMain/kotlin/app/cash/redwood/lazylayout/widget/SparseList.kt b/redwood-lazylayout-widget/src/commonMain/kotlin/app/cash/redwood/lazylayout/widget/SparseList.kt index 570acea35a..5eaf130bb4 100644 --- a/redwood-lazylayout-widget/src/commonMain/kotlin/app/cash/redwood/lazylayout/widget/SparseList.kt +++ b/redwood-lazylayout-widget/src/commonMain/kotlin/app/cash/redwood/lazylayout/widget/SparseList.kt @@ -46,6 +46,16 @@ internal class SparseList : AbstractList() { } } + inline fun getOrCreate(index: Int, create: () -> T & Any): T & Any { + var result = get(index) + + if (result != null) return result + + result = create() + set(index, result) + return result + } + fun removeLast(): T? { return removeAt(size - 1) } diff --git a/redwood-lazylayout-widget/src/commonTest/kotlin/app/cash/redwood/lazylayout/widget/FakeProcessor.kt b/redwood-lazylayout-widget/src/commonTest/kotlin/app/cash/redwood/lazylayout/widget/FakeProcessor.kt index ce6c6f18ec..eb8655f340 100644 --- a/redwood-lazylayout-widget/src/commonTest/kotlin/app/cash/redwood/lazylayout/widget/FakeProcessor.kt +++ b/redwood-lazylayout-widget/src/commonTest/kotlin/app/cash/redwood/lazylayout/widget/FakeProcessor.kt @@ -26,10 +26,11 @@ class FakeProcessor : LazyListUpdateProcessor( private var scrollWindowOffset = 0 private val scrollWindowCells = mutableListOf() - override fun createView( - binding: Binding, - index: Int, - ) = StringCell(binding) + private fun getView(index: Int): StringCell { + return getOrCreateBoundView(index) { binding -> + StringCell(binding) + } + } override fun insertRows(index: Int, count: Int) { require(index >= 0 && count >= 0 && index <= dataSize + 1)