diff options
author | Treehugger Robot <treehugger-gerrit@google.com> | 2019-06-14 23:29:47 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2019-06-14 23:29:47 +0000 |
commit | 502d3944dd72628e71019619333fc94e38609dec (patch) | |
tree | 485ff7d5da086c19dad9753c7c2efb63c7ed397b | |
parent | d4cc2951f98fc32b35bb527a1c95a5161b4011e0 (diff) | |
parent | ce047371fd8936cb6723fc06a077a569cb407f0b (diff) | |
download | support-502d3944dd72628e71019619333fc94e38609dec.tar.gz |
Merge "Removed tiled implementation from PagedStorage" into androidx-master-dev
6 files changed, 65 insertions, 836 deletions
diff --git a/paging/common/src/main/kotlin/androidx/paging/PagedList.kt b/paging/common/src/main/kotlin/androidx/paging/PagedList.kt index 42fc73a53ca..830e27e1d22 100644 --- a/paging/common/src/main/kotlin/androidx/paging/PagedList.kt +++ b/paging/common/src/main/kotlin/androidx/paging/PagedList.kt @@ -1029,7 +1029,7 @@ abstract class PagedList<T : Any> : AbstractList<T> { * @see size */ open val loadedCount - get() = storage.loadedCount + get() = storage.storageCount /** * Returns whether the list is immutable. @@ -1207,10 +1207,10 @@ abstract class PagedList<T : Any> : AbstractList<T> { private fun dispatchBoundaryCallbacks(begin: Boolean, end: Boolean) { // safe to deref boundaryCallback here, since we only defer if boundaryCallback present if (begin) { - boundaryCallback!!.onItemAtFrontLoaded(storage.firstLoadedItem!!) + boundaryCallback!!.onItemAtFrontLoaded(storage.firstLoadedItem) } if (end) { - boundaryCallback!!.onItemAtEndLoaded(storage.lastLoadedItem!!) + boundaryCallback!!.onItemAtEndLoaded(storage.lastLoadedItem) } } diff --git a/paging/common/src/main/kotlin/androidx/paging/PagedStorage.kt b/paging/common/src/main/kotlin/androidx/paging/PagedStorage.kt index e9df6d849c6..92f300fa5f1 100644 --- a/paging/common/src/main/kotlin/androidx/paging/PagedStorage.kt +++ b/paging/common/src/main/kotlin/androidx/paging/PagedStorage.kt @@ -22,25 +22,14 @@ import java.util.AbstractList /** * Class holding the pages of data backing a [PagedList], presenting sparse loaded data as a List. * - * It has two modes of operation: contiguous and non-contiguous (tiled). This class only holds - * data, and does not have any notion of the ideas of async loads, or prefetching. + * This class only holds data, and does not have any notion of the ideas of async loads, or + * prefetching. * * @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY) class PagedStorage<T : Any> : AbstractList<T>, Pager.AdjacentProvider<T> { - /** - * List of pages in storage. - * - * Two storage modes: - * - * Contiguous - all content in pages is valid and loaded, but may return `false` from [isTiled]. - * Safe to access any item in any page. - * - * Non-contiguous - pages may have nulls or a placeholder page, [isTiled] always returns `true`. - * pages may have nulls, or placeholder (empty) pages while content is loading. - */ - private val pages: ArrayList<List<T>?> + private val pages: ArrayList<List<T>> var leadingNullCount: Int = 0 private set @@ -51,54 +40,28 @@ class PagedStorage<T : Any> : AbstractList<T>, Pager.AdjacentProvider<T> { var positionOffset: Int = 0 private set /** - * Number of loaded items held by [pages]. When tiling, doesn't count unloaded pages in [pages]. - * If tiling is disabled, same as [storageCount]. - * - * This count is the one used for trimming. - */ - var loadedCount: Int = 0 - private set - - /** - * Number of items represented by [pages]. If tiling is enabled, unloaded items in [pages] may - * be `null`, but this value still counts them. + * Number of loaded items held by [pages]. */ var storageCount: Int = 0 private set - /** - *If pageSize > 0, tiling is enabled, 'pages' may have gaps, and leadingPages is set - */ - private var pageSize: Int = 0 - var numberPrepended: Int = 0 private set var numberAppended: Int = 0 private set - /** - * `true` if all pages are the same size, except for the last, which may be smaller - */ - val isTiled - get() = pageSize > 0 - - val pageCount - get() = pages.size - val middleOfLoadedRange get() = leadingNullCount + positionOffset + storageCount / 2 - // ------------- Adjacent Provider interface (contiguous-only) ------------------ + // ------------- Adjacent Provider interface ------------------ override val firstLoadedItem - // Safe to access first page's first item here - // If contiguous, pages can't be empty, can't hold null Pages, and items can't be empty - get() = pages[0]?.first() + // guaranteed to have pages, and every page is non-empty, so this is safe + get() = pages.first().first() override val lastLoadedItem - // Safe to access last page's last item here: - // If contiguous, pages can't be empty, can't hold null Pages, and items can't be empty - get() = pages.last()?.last() + // guaranteed to have pages, and every page is non-empty, so this is safe + get() = pages.last().last() override val firstLoadedItemIndex get() = leadingNullCount + positionOffset @@ -111,9 +74,7 @@ class PagedStorage<T : Any> : AbstractList<T>, Pager.AdjacentProvider<T> { pages = ArrayList() trailingNullCount = 0 positionOffset = 0 - loadedCount = 0 storageCount = 0 - pageSize = 1 numberPrepended = 0 numberAppended = 0 } @@ -127,9 +88,7 @@ class PagedStorage<T : Any> : AbstractList<T>, Pager.AdjacentProvider<T> { pages = ArrayList(other.pages) trailingNullCount = other.trailingNullCount positionOffset = other.positionOffset - loadedCount = other.loadedCount storageCount = other.storageCount - pageSize = other.pageSize numberPrepended = other.numberPrepended numberAppended = other.numberAppended } @@ -143,12 +102,7 @@ class PagedStorage<T : Any> : AbstractList<T>, Pager.AdjacentProvider<T> { trailingNullCount = trailingNulls this.positionOffset = positionOffset - loadedCount = page.size - storageCount = loadedCount - - // initialized as tiled. There may be 3 nulls, 2 items, but we still call this tiled - // even if it will break if nulls convert. - pageSize = page.size + storageCount = page.size numberPrepended = 0 numberAppended = 0 @@ -179,36 +133,22 @@ class PagedStorage<T : Any> : AbstractList<T>, Pager.AdjacentProvider<T> { localIndex < 0 || localIndex >= storageCount -> return null } - var localPageIndex: Int + var localPageIndex = 0 var pageInternalIndex: Int - if (isTiled) { - // it's inside pages, and we're tiled. Jump to correct tile. - localPageIndex = localIndex / pageSize - pageInternalIndex = localIndex % pageSize - } else { - // it's inside pages, but page sizes aren't regular. Walk to correct tile. - // Pages can only be null while tiled, so accessing page count is safe. - pageInternalIndex = localIndex - val localPageCount = pages.size - localPageIndex = 0 - while (localPageIndex < localPageCount) { - val pageSize = pages[localPageIndex]!!.size - if (pageSize > pageInternalIndex) { - // stop, found the page - break - } - pageInternalIndex -= pageSize - localPageIndex++ + // Since we don't know if page sizes are regular, we walk to correct page. + pageInternalIndex = localIndex + val localPageCount = pages.size + while (localPageIndex < localPageCount) { + val pageSize = pages[localPageIndex].size + if (pageSize > pageInternalIndex) { + // stop, found the page + break } + pageInternalIndex -= pageSize + localPageIndex++ } - - val page = pages[localPageIndex] - return when { - // can only occur in tiled case, with untouched inner/placeholder pages - page == null || page.isEmpty() -> null - else -> page[pageInternalIndex] - } + return pages[localPageIndex][pageInternalIndex] } /** @@ -228,31 +168,6 @@ class PagedStorage<T : Any> : AbstractList<T>, Pager.AdjacentProvider<T> { override val size get() = leadingNullCount + storageCount + trailingNullCount - fun computeLeadingNulls(): Int { - var total = leadingNullCount - val pageCount = pages.size - for (i in 0 until pageCount) { - val page = pages[i] - if (page != null && page !is PlaceholderList) { - break - } - total += pageSize - } - return total - } - - fun computeTrailingNulls(): Int { - var total = trailingNullCount - for (i in pages.indices.reversed()) { - val page = pages[i] - if (page != null && page !is PlaceholderList) { - break - } - total += pageSize - } - return total - } - // ---------------- Trimming API ------------------- // Trimming is always done at the beginning or end of the list, as content is loaded. // In addition to trimming pages in the storage, we also support pre-trimming pages (dropping @@ -265,10 +180,9 @@ class PagedStorage<T : Any> : AbstractList<T>, Pager.AdjacentProvider<T> { private fun needsTrim(maxSize: Int, requiredRemaining: Int, localPageIndex: Int): Boolean { val page = pages[localPageIndex] - return page == null || (loadedCount > maxSize && + return storageCount > maxSize && pages.size > 2 && - page !is PlaceholderList && - loadedCount - page.size >= requiredRemaining) + storageCount - page.size >= requiredRemaining } fun needsTrimFromFront(maxSize: Int, requiredRemaining: Int) = @@ -278,9 +192,9 @@ class PagedStorage<T : Any> : AbstractList<T>, Pager.AdjacentProvider<T> { needsTrim(maxSize, requiredRemaining, pages.size - 1) fun shouldPreTrimNewPage(maxSize: Int, requiredRemaining: Int, countToBeAdded: Int) = - loadedCount + countToBeAdded > maxSize && + storageCount + countToBeAdded > maxSize && pages.size > 1 && - loadedCount >= requiredRemaining + storageCount >= requiredRemaining internal fun trimFromFront( insertNulls: Boolean, @@ -291,10 +205,9 @@ class PagedStorage<T : Any> : AbstractList<T>, Pager.AdjacentProvider<T> { var totalRemoved = 0 while (needsTrimFromFront(maxSize, requiredRemaining)) { val page = pages.removeAt(0) - val removed = page?.size ?: pageSize + val removed = page.size totalRemoved += removed storageCount -= removed - loadedCount -= page?.size ?: 0 } if (totalRemoved > 0) { @@ -321,10 +234,9 @@ class PagedStorage<T : Any> : AbstractList<T>, Pager.AdjacentProvider<T> { var totalRemoved = 0 while (needsTrimFromEnd(maxSize, requiredRemaining)) { val page = pages.removeAt(pages.size - 1) - val removed = page?.size ?: pageSize + val removed = page.size totalRemoved += removed storageCount -= removed - loadedCount -= page?.size ?: 0 } if (totalRemoved > 0) { @@ -349,18 +261,8 @@ class PagedStorage<T : Any> : AbstractList<T>, Pager.AdjacentProvider<T> { // Nothing returned from source, nothing to do return } - if (pageSize > 0 && count != pageSize) { - if (pages.size == 1 && count > pageSize) { - // prepending to a single item - update current page size to that of 'inner' page - pageSize = count - } else { - // no longer tiled - pageSize = -1 - } - } pages.add(0, page) - loadedCount += count storageCount += count val changedCount = minOf(leadingNullCount, count) @@ -382,16 +284,7 @@ class PagedStorage<T : Any> : AbstractList<T>, Pager.AdjacentProvider<T> { return } - if (pageSize > 0) { - // if the previous page was smaller than pageSize, - // or if this page is larger than the previous, disable tiling - if (pages[pages.size - 1]!!.size != pageSize || count > pageSize) { - pageSize = -1 - } - } - pages.add(page) - loadedCount += count storageCount += count val changedCount = minOf(trailingNullCount, count) @@ -414,238 +307,7 @@ class PagedStorage<T : Any> : AbstractList<T>, Pager.AdjacentProvider<T> { // ignored } - // ------------------ Non-Contiguous API (tiling required) ---------------------- - - /** - * Return true if the page at the passed position would be the first (if trimFromFront) or last - * page that's currently loading. - */ - fun pageWouldBeBoundary(positionOfPage: Int, trimFromFront: Boolean): Boolean { - when { - pageSize < 1 || pages.size < 2 -> - throw IllegalStateException("Trimming attempt before sufficient load") - // position represent page in leading nulls - positionOfPage < leadingNullCount -> return trimFromFront - // position represent page in trailing nulls - positionOfPage >= leadingNullCount + storageCount -> return !trimFromFront - } - - val localPageIndex = (positionOfPage - leadingNullCount) / pageSize - - // walk outside in, return false if we find non-placeholder page before localPageIndex - if (trimFromFront) { - for (i in 0 until localPageIndex) { - if (pages[i] != null) { - return false - } - } - } else { - for (i in pages.size - 1 downTo localPageIndex + 1) { - if (pages[i] != null) { - return false - } - } - } - - // didn't find another page, so this one would be a boundary - return true - } - - internal fun initAndSplit( - leadingNulls: Int, - multiPageList: List<T>, - trailingNulls: Int, - positionOffset: Int, - pageSize: Int, - callback: Callback - ) { - val pageCount = (multiPageList.size + (pageSize - 1)) / pageSize - for (i in 0 until pageCount) { - val beginInclusive = i * pageSize - val endExclusive = minOf(multiPageList.size, (i + 1) * pageSize) - - val sublist = multiPageList.subList(beginInclusive, endExclusive) - - if (i == 0) { - // Trailing nulls for first page includes other pages in multiPageList - val initialTrailingNulls = trailingNulls + multiPageList.size - sublist.size - init(leadingNulls, sublist, initialTrailingNulls, positionOffset) - } else { - val insertPosition = leadingNulls + beginInclusive - insertPage(insertPosition, sublist, null) - } - } - callback.onInitialized(size) - } - - internal fun tryInsertPageAndTrim( - position: Int, - page: List<T>, - lastLoad: Int, - maxSize: Int, - requiredRemaining: Int, - callback: Callback - ) { - val trim = maxSize != PagedList.Config.MAX_SIZE_UNBOUNDED - val trimFromFront = lastLoad > middleOfLoadedRange - - val pageInserted = (!trim || - !shouldPreTrimNewPage(maxSize, requiredRemaining, page.size) || - !pageWouldBeBoundary(position, trimFromFront)) - - if (pageInserted) { - insertPage(position, page, callback) - } else { - // trim would have us drop the page we just loaded - swap it to null - val localPageIndex = (position - leadingNullCount) / pageSize - pages.set(localPageIndex, null) - - // note: we also remove it, so we don't have to guess how large a 'null' page is later - storageCount -= page.size - if (trimFromFront) { - pages.removeAt(0) - leadingNullCount += page.size - } else { - pages.removeAt(pages.size - 1) - trailingNullCount += page.size - } - } - - if (trim) { - if (trimFromFront) { - trimFromFront(true, maxSize, requiredRemaining, callback) - } else { - trimFromEnd(true, maxSize, requiredRemaining, callback) - } - } - } - - internal fun insertPage(position: Int, page: List<T>, callback: Callback?) { - val newPageSize = page.size - if (newPageSize != pageSize) { - // differing page size is OK in 2 cases, when the page is being added: - // 1) to the end (in which case, ignore new smaller size) - // 2) only the last page has been added so far (in which case, adopt new bigger size) - - val size = size - val addingLastPage = position == size - size % pageSize && newPageSize < pageSize - val onlyEndPagePresent = (trailingNullCount == 0 && pages.size == 1 && - newPageSize > pageSize) - - // OK only if existing single page, and it's the last one - if (!onlyEndPagePresent && !addingLastPage) { - throw IllegalArgumentException("page introduces incorrect tiling") - } - if (onlyEndPagePresent) { - pageSize = newPageSize - } - } - - val pageIndex = position / pageSize - - allocatePageRange(pageIndex, pageIndex) - - val localPageIndex = pageIndex - leadingNullCount / pageSize - - val oldPage = pages[localPageIndex] - if (oldPage != null && oldPage !is PlaceholderList) { - throw IllegalArgumentException( - "Invalid position $position: data already loaded" - ) - } - pages[localPageIndex] = page - loadedCount += newPageSize - callback?.onPageInserted(position, newPageSize) - } - - @Suppress("MemberVisibilityCanBePrivate") - fun allocatePageRange(minimumPage: Int, maximumPage: Int) { - var leadingNullPages = leadingNullCount / pageSize - - if (minimumPage < leadingNullPages) { - for (i in 0 until leadingNullPages - minimumPage) { - pages.add(0, null) - } - val newStorageAllocated = (leadingNullPages - minimumPage) * pageSize - storageCount += newStorageAllocated - leadingNullCount -= newStorageAllocated - - leadingNullPages = minimumPage - } - if (maximumPage >= leadingNullPages + pages.size) { - val newStorageAllocated = minOf( - trailingNullCount, - (maximumPage + 1 - (leadingNullPages + pages.size)) * pageSize - ) - for (i in pages.size..maximumPage - leadingNullPages) { - pages.add(pages.size, null) - } - storageCount += newStorageAllocated - trailingNullCount -= newStorageAllocated - } - } - - /** - * @hide - */ - @RestrictTo(RestrictTo.Scope.LIBRARY) - fun allocatePlaceholders(index: Int, prefetchDistance: Int, pageSize: Int, callback: Callback) { - if (pageSize != this.pageSize) { - if (pageSize < this.pageSize) { - throw IllegalArgumentException("Page size cannot be reduced") - } - if (pages.size != 1 || trailingNullCount != 0) { - // not in single, last page allocated case - can't change page size - throw IllegalArgumentException( - "Page size can change only if last page is only one present" - ) - } - this.pageSize = pageSize - } - - val maxPageCount = (size + this.pageSize - 1) / this.pageSize - val minimumPage = maxOf((index - prefetchDistance) / this.pageSize, 0) - val maximumPage = minOf((index + prefetchDistance) / this.pageSize, maxPageCount - 1) - - allocatePageRange(minimumPage, maximumPage) - val leadingNullPages = leadingNullCount / this.pageSize - for (pageIndex in minimumPage..maximumPage) { - val localPageIndex = pageIndex - leadingNullPages - if (pages[localPageIndex] == null) { - pages[localPageIndex] = placeholderList - callback.onPagePlaceholderInserted(pageIndex) - } - } - } - - fun hasPage(pageSize: Int, index: Int): Boolean { - // NOTE: we pass pageSize here to avoid in case pageSize not fully initialized (when last - // page only one loaded). - val leadingNullPages = leadingNullCount / pageSize - - if (index < leadingNullPages || index >= leadingNullPages + pages.size) { - return false - } - - val page = pages[index - leadingNullPages] - - return page != null && page !is PlaceholderList - } - - override fun toString(): String { - var ret = "leading $leadingNullCount, storage $storageCount, trailing $trailingNullCount" - if (pages.isNotEmpty()) { - ret += " ${pages.joinToString(" ")}" - } - return ret - } - - /** - * Lists instances are compared (with instance equality) to [placeholderList] to check if an - * item in that position is already loading. We use a singleton placeholder list that is - * distinct from `Collections.emptyList()` for safety. - */ - private class PlaceholderList<T> : ArrayList<T>() - - private val placeholderList = PlaceholderList<T>() + override fun toString(): String = + "leading $leadingNullCount, storage $storageCount, trailing $trailingNullCount " + + pages.joinToString(" ") } diff --git a/paging/common/src/test/kotlin/androidx/paging/ContiguousPagedListTest.kt b/paging/common/src/test/kotlin/androidx/paging/ContiguousPagedListTest.kt index 849f5070912..b85fd0af256 100644 --- a/paging/common/src/test/kotlin/androidx/paging/ContiguousPagedListTest.kt +++ b/paging/common/src/test/kotlin/androidx/paging/ContiguousPagedListTest.kt @@ -144,21 +144,16 @@ class ContiguousPagedListTest(private val placeholdersEnabled: Boolean) { val expectedTrailing = ITEMS.size - start - count assertEquals(ITEMS.size, actual.size) - assertEquals( - (ITEMS.size - start - expectedTrailing), - actual.storageCount - ) assertEquals(start, actual.leadingNullCount) assertEquals(expectedTrailing, actual.trailingNullCount) } else { assertEquals(ITEMS.subList(start, start + count), actual) assertEquals(count, actual.size) - assertEquals(actual.size, actual.storageCount) assertEquals(0, actual.leadingNullCount) assertEquals(0, actual.trailingNullCount) } - assertEquals(count, actual.loadedCount) + assertEquals(count, actual.storageCount) } private fun verifyRange(start: Int, count: Int, actual: PagedList<Item>) { diff --git a/paging/common/src/test/kotlin/androidx/paging/PagedStorageTest.kt b/paging/common/src/test/kotlin/androidx/paging/PagedStorageTest.kt index 44c5f48976d..75a53b35f96 100644 --- a/paging/common/src/test/kotlin/androidx/paging/PagedStorageTest.kt +++ b/paging/common/src/test/kotlin/androidx/paging/PagedStorageTest.kt @@ -24,7 +24,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 import org.mockito.Mockito.mock -import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions @@ -133,344 +132,15 @@ class PagedStorageTest { } @Test - fun isTiled_addend_smallerPageIsNotLast() { - val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage(0, createPage("a", "a"), 0) - assertTrue(storage.isTiled) - - storage.appendPage(createPage("a", "a"), callback) - assertTrue(storage.isTiled) - - storage.appendPage(createPage("a"), callback) - assertTrue(storage.isTiled) - - // no matter what we append here, we're no longer tiled - storage.appendPage(createPage("a", "a"), callback) - assertFalse(storage.isTiled) - } - - @Test - fun isTiled_append_growingSizeDisable() { - val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage(0, createPage("a", "a"), 0) - assertTrue(storage.isTiled) - - // page size can't grow from append - storage.appendPage(createPage("a", "a", "a"), callback) - assertFalse(storage.isTiled) - } - - @Test - fun isTiled_prepend_smallerPage() { - val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage(0, createPage("a"), 0) - assertTrue(storage.isTiled) - - storage.prependPage(createPage("a", "a"), callback) - assertTrue(storage.isTiled) - - storage.prependPage(createPage("a", "a"), callback) - assertTrue(storage.isTiled) - - storage.prependPage(createPage("a"), callback) - assertFalse(storage.isTiled) - } - - @Test - fun isTiled_prepend_smallerThanInitialPage() { - val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage(0, createPage("a", "a"), 0) - assertTrue(storage.isTiled) - - storage.prependPage(createPage("a"), callback) - assertFalse(storage.isTiled) - } - - @Test - fun get_tiled() { - val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage(1, createPage("a", "b"), 5) - assertTrue(storage.isTiled) - - storage.appendPage(createPage("c", "d"), callback) - storage.appendPage(createPage("e", "f"), callback) - - assertTrue(storage.isTiled) - assertArrayEquals(arrayOf(null, "a", "b", "c", "d", "e", "f", null), storage.toArray()) - } - - @Test - fun get_nonTiled() { + fun get() { val callback = mock(PagedStorage.Callback::class.java) val storage = PagedStorage(1, createPage("a"), 6) - assertTrue(storage.isTiled) - storage.appendPage(createPage("b", "c"), callback) storage.appendPage(createPage("d", "e", "f"), callback) - - assertFalse(storage.isTiled) assertArrayEquals(arrayOf(null, "a", "b", "c", "d", "e", "f", null), storage.toArray()) } @Test - fun insertOne() { - val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage<String>() - - storage.init(2, createPage("c", "d"), 3, 0, callback) - - assertEquals(7, storage.size) - assertArrayEquals(arrayOf(null, null, "c", "d", null, null, null), storage.toArray()) - verify(callback).onInitialized(7) - verifyNoMoreInteractions(callback) - - storage.insertPage(4, createPage("e", "f"), callback) - - assertEquals(7, storage.size) - assertArrayEquals(arrayOf(null, null, "c", "d", "e", "f", null), storage.toArray()) - verify(callback).onPageInserted(4, 2) - verifyNoMoreInteractions(callback) - } - - @Test - fun insertThree() { - val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage<String>() - - storage.init(2, createPage("c", "d"), 3, 0, callback) - - assertEquals(7, storage.size) - assertArrayEquals(arrayOf(null, null, "c", "d", null, null, null), storage.toArray()) - verify(callback).onInitialized(7) - verifyNoMoreInteractions(callback) - - // first, insert 1st page - storage.insertPage(0, createPage("a", "b"), callback) - - assertEquals(7, storage.size) - assertArrayEquals(arrayOf("a", "b", "c", "d", null, null, null), storage.toArray()) - verify(callback).onPageInserted(0, 2) - verifyNoMoreInteractions(callback) - - // then 3rd page - storage.insertPage(4, createPage("e", "f"), callback) - - assertEquals(7, storage.size) - assertArrayEquals(arrayOf("a", "b", "c", "d", "e", "f", null), storage.toArray()) - verify(callback).onPageInserted(4, 2) - verifyNoMoreInteractions(callback) - - // then last, small page - storage.insertPage(6, createPage("g"), callback) - - assertEquals(7, storage.size) - assertArrayEquals(arrayOf("a", "b", "c", "d", "e", "f", "g"), storage.toArray()) - verify(callback).onPageInserted(6, 1) - verifyNoMoreInteractions(callback) - } - - @Test - fun insertLastFirst() { - val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage<String>() - - storage.init(6, createPage("g"), 0, 0, callback) - - assertEquals(7, storage.size) - assertArrayEquals(arrayOf(null, null, null, null, null, null, "g"), storage.toArray()) - verify(callback).onInitialized(7) - verifyNoMoreInteractions(callback) - - // insert 1st page - storage.insertPage(0, createPage("a", "b"), callback) - - assertEquals(7, storage.size) - assertArrayEquals(arrayOf("a", "b", null, null, null, null, "g"), storage.toArray()) - verify(callback).onPageInserted(0, 2) - verifyNoMoreInteractions(callback) - } - - @Test(expected = IllegalArgumentException::class) - fun insertFailure_decreaseLast() { - val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage<String>() - - storage.init(2, createPage("c", "d"), 0, 0, callback) - - // should throw, page too small - storage.insertPage(0, createPage("a"), callback) - } - - @Test(expected = IllegalArgumentException::class) - fun insertFailure_increase() { - val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage<String>() - - storage.init(0, createPage("a", "b"), 3, 0, callback) - - // should throw, page too big - storage.insertPage(2, createPage("c", "d", "e"), callback) - } - - @Test - fun allocatePlaceholders_simple() { - val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage<String>() - - storage.init(2, createPage("c"), 2, 0, callback) - - verify(callback).onInitialized(5) - - storage.allocatePlaceholders(2, 1, 1, callback) - - verify(callback).onPagePlaceholderInserted(1) - verify(callback).onPagePlaceholderInserted(3) - verifyNoMoreInteractions(callback) - - assertArrayEquals(arrayOf(null, null, "c", null, null), storage.toArray()) - } - - @Test - fun allocatePlaceholders_adoptPageSize() { - val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage<String>() - - storage.init(4, createPage("e"), 0, 0, callback) - - verify(callback).onInitialized(5) - - storage.allocatePlaceholders(0, 2, 2, callback) - - verify(callback).onPagePlaceholderInserted(0) - verify(callback).onPagePlaceholderInserted(1) - verifyNoMoreInteractions(callback) - - assertArrayEquals(arrayOf(null, null, null, null, "e"), storage.toArray()) - } - - @Test(expected = IllegalArgumentException::class) - fun allocatePlaceholders_cannotShrinkPageSize() { - val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage<String>() - - storage.init(4, createPage("e", "f"), 0, 0, callback) - - verify(callback).onInitialized(6) - - storage.allocatePlaceholders(0, 2, 1, callback) - } - - @Test(expected = IllegalArgumentException::class) - fun allocatePlaceholders_cannotAdoptPageSize() { - val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage<String>() - - storage.init(2, createPage("c", "d"), 2, 0, callback) - - verify(callback).onInitialized(6) - - storage.allocatePlaceholders(0, 2, 3, callback) - } - - @Test - fun get_placeholdersMulti() { - val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage<String>() - - storage.init(2, createPage("c", "d"), 3, 0, callback) - - assertArrayEquals(arrayOf(null, null, "c", "d", null, null, null), storage.toArray()) - - storage.allocatePlaceholders(0, 10, 2, callback) - - // allocating placeholders shouldn't affect result of get - assertArrayEquals(arrayOf(null, null, "c", "d", null, null, null), storage.toArray()) - } - - @Test - fun hasPage() { - val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage<String>() - - storage.init(4, createPage("e"), 0, 0, callback) - - assertFalse(storage.hasPage(1, 0)) - assertFalse(storage.hasPage(1, 1)) - assertFalse(storage.hasPage(1, 2)) - assertFalse(storage.hasPage(1, 3)) - assertTrue(storage.hasPage(1, 4)) - - assertFalse(storage.hasPage(2, 0)) - assertFalse(storage.hasPage(2, 1)) - assertTrue(storage.hasPage(2, 2)) - } - - @Test - fun pageWouldBeBoundary_unallocated() { - val storage = PagedStorage<String>() - storage.initAndSplit(2, listOf("c", "d", "e", "f"), 1, 0, 2, IGNORED_CALLBACK) - - assertTrue(storage.pageWouldBeBoundary(0, true)) - assertFalse(storage.pageWouldBeBoundary(0, false)) - assertTrue(storage.pageWouldBeBoundary(6, false)) - assertFalse(storage.pageWouldBeBoundary(6, true)) - } - - @Test - fun pageWouldBeBoundary_front() { - val storage = PagedStorage<String>() - storage.initAndSplit(8, listOf("i", "j", "k", "l", "m", "n"), 0, 0, 2, IGNORED_CALLBACK) - - for (i in 0..6 step 2) { - // any position in leading nulls would be front boundary - assertTrue(storage.pageWouldBeBoundary(i, true)) - assertFalse(storage.pageWouldBeBoundary(i, false)) - } - - storage.allocatePlaceholders(8, 6, 2, IGNORED_CALLBACK) - - for (i in 0..6 step 2) { - // 4 / 6 have a placeholder ahead, so they return false - assertEquals(i < 4, storage.pageWouldBeBoundary(i, true)) - assertFalse(storage.pageWouldBeBoundary(i, false)) - } - } - - @Test - fun pageWouldBeBoundary_end() { - val storage = PagedStorage<String>() - storage.initAndSplit(0, listOf("a", "b", "c", "d", "e", "f"), 8, 0, 2, IGNORED_CALLBACK) - - for (i in 6..12 step 2) { - // any position in leading nulls would be front boundary - assertFalse(storage.pageWouldBeBoundary(i, true)) - assertTrue(storage.pageWouldBeBoundary(i, false)) - } - - storage.allocatePlaceholders(6, 6, 2, IGNORED_CALLBACK) - - for (i in 6..12 step 2) { - // any position in leading nulls would be front boundary - assertFalse(storage.pageWouldBeBoundary(i, true)) - assertEquals(i > 10, storage.pageWouldBeBoundary(i, false)) - } - } - - @Test - fun trim_noOp() { - val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage<String>() - - storage.initAndSplit(0, listOf("a", "b", "c", "d", "e"), 0, 0, 1, callback) - - verify(callback).onInitialized(5) - storage.trimFromFront(true, 5, 5, callback) - verifyNoMoreInteractions(callback) - storage.trimFromEnd(true, 5, 5, callback) - verifyNoMoreInteractions(callback) - } - - @Test fun trim_twoPagesNoOp() { val callback = mock(PagedStorage.Callback::class.java) val storage = PagedStorage<String>() @@ -487,9 +157,7 @@ class PagedStorageTest { @Test fun trim_remainderPreventsNoOp() { - val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage<String>() - storage.initAndSplit(0, listOf("a", "b", "c", "d", "e", "f"), 0, 0, 2, callback) + val storage = PagedStorage(listOf(listOf("a", "b"), listOf("c", "d"), listOf("d", "e"))) // can't trim, since removing a page would mean fewer items than required assertFalse(storage.needsTrimFromFront(5, 5)) @@ -503,11 +171,7 @@ class PagedStorageTest { @Test fun trimFromFront_simple() { val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage<String>() - - storage.initAndSplit(0, listOf("a", "b", "c", "d", "e"), 0, 0, 1, callback) - verify(callback).onInitialized(5) - verifyNoMoreInteractions(callback) + val storage = PagedStorage(('a'..'e').map { listOf("$it") }) storage.trimFromFront(false, 4, 4, callback) verify(callback).onPagesRemoved(0, 1) @@ -520,11 +184,7 @@ class PagedStorageTest { @Test fun trimFromFront_simplePlaceholders() { val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage<String>() - - storage.initAndSplit(0, listOf("a", "b", "c", "d", "e"), 0, 0, 1, callback) - verify(callback).onInitialized(5) - verifyNoMoreInteractions(callback) + val storage = PagedStorage(('a'..'e').map { listOf("$it") }) storage.trimFromFront(true, 4, 4, callback) verify(callback).onPagesSwappedToPlaceholder(0, 1) @@ -535,37 +195,9 @@ class PagedStorageTest { } @Test - fun trimFromFront_complexWithGaps() { - val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage<String>() - - storage.initAndSplit(4, listOf("e"), 3, 0, 1, callback) - storage.insertPage(1, listOf("b"), callback) - storage.insertPage(3, listOf("d"), callback) - storage.insertPage(6, listOf("g"), callback) - storage.insertPage(7, listOf("h"), callback) - reset(callback) - assertEquals(7, storage.pageCount) // page for everything but leading null - assertEquals(listOf(null, "b", null, "d", "e", null, "g", "h"), storage) - - // going from: -b-de-gh - // to: ----e-gh - // we signal this as onPagesSwappedToPlaceholder(1, 3). We could theoretically separate - // this into two single page drop signals, but it's too rare to be worth it. - storage.trimFromFront(true, 3, 3, callback) - verify(callback).onPagesSwappedToPlaceholder(1, 3) - assertEquals(4, storage.pageCount) - assertEquals(listOf(null, null, null, null, "e", null, "g", "h"), storage) - } - - @Test fun trimFromEnd_simple() { val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage<String>() - - storage.initAndSplit(0, listOf("a", "b", "c", "d", "e"), 0, 0, 1, callback) - verify(callback).onInitialized(5) - verifyNoMoreInteractions(callback) + val storage = PagedStorage(('a'..'e').map { listOf("$it") }) storage.trimFromEnd(false, 4, 4, callback) verify(callback).onPagesRemoved(4, 1) @@ -575,42 +207,27 @@ class PagedStorageTest { @Test fun trimFromEnd_simplePlaceholders() { val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage<String>() - - storage.initAndSplit(0, listOf("a", "b", "c", "d", "e"), 0, 0, 1, callback) - verify(callback).onInitialized(5) - verifyNoMoreInteractions(callback) + val storage = PagedStorage(('a'..'e').map { listOf("$it") }) storage.trimFromEnd(true, 4, 4, callback) verify(callback).onPagesSwappedToPlaceholder(4, 1) verifyNoMoreInteractions(callback) } - @Test - fun trimFromEnd_complexWithGaps() { - val callback = mock(PagedStorage.Callback::class.java) - val storage = PagedStorage<String>() - - storage.initAndSplit(3, listOf("d"), 4, 0, 1, callback) - storage.insertPage(0, listOf("a"), callback) - storage.insertPage(1, listOf("b"), callback) - storage.insertPage(5, listOf("f"), callback) - storage.insertPage(6, listOf("g"), callback) - reset(callback) - assertEquals(7, storage.pageCount) // page for everything but trailing null - assertEquals(listOf("a", "b", null, "d", null, "f", "g", null), storage) - - // going from: ab-d-fg- - // to: ab-d---- - // we signal this as onPagesSwappedToPlaceholder(4, 3). We could theoretically separate - // this into two single page drop signals, but it's too rare to be worth it. - storage.trimFromEnd(true, 3, 3, callback) - verify(callback).onPagesSwappedToPlaceholder(4, 3) - assertEquals(4, storage.pageCount) - assertEquals(listOf("a", "b", null, "d", null, null, null, null), storage) - } - companion object { + @Suppress("TestFunctionName") + private fun PagedStorage(pages: List<List<String>>): PagedStorage<String> { + val storage = PagedStorage<String>() + val totalPageCount = pages.map { it.size }.reduce { a, b -> a + b } + storage.init(0, pages[0], totalPageCount - pages[0].size, 0, IGNORED_CALLBACK) + pages.forEachIndexed { index, page -> + if (index != 0) { + storage.appendPage(page, IGNORED_CALLBACK) + } + } + return storage + } + private val IGNORED_CALLBACK = object : PagedStorage.Callback { override fun onInitialized(count: Int) {} override fun onPagePrepended(leadingNulls: Int, changed: Int, added: Int) {} @@ -621,4 +238,4 @@ class PagedStorageTest { override fun onPagesSwappedToPlaceholder(startOfDrops: Int, count: Int) {} } } -} +}
\ No newline at end of file diff --git a/paging/runtime/src/androidTest/java/androidx/paging/PagedStorageDiffHelperTest.kt b/paging/runtime/src/androidTest/java/androidx/paging/PagedStorageDiffHelperTest.kt index 4c26b63da6e..57e931ec80c 100644 --- a/paging/runtime/src/androidTest/java/androidx/paging/PagedStorageDiffHelperTest.kt +++ b/paging/runtime/src/androidTest/java/androidx/paging/PagedStorageDiffHelperTest.kt @@ -42,26 +42,6 @@ class PagedStorageDiffHelperTest { } @Test - fun sameListNoUpdatesPlaceholder() { - val storageNoPlaceholder = PagedStorage(0, listOf("a", "b", "c"), 10) - - val storageWithPlaceholder = PagedStorage(0, listOf("a", "b", "c"), 10) - storageWithPlaceholder.allocatePlaceholders(3, 0, 3, - /* ignored */ mock(PagedStorage.Callback::class.java)) - - // even though one has placeholders, and null counts are different... - assertEquals(10, storageNoPlaceholder.trailingNullCount) - assertEquals(7, storageWithPlaceholder.trailingNullCount) - - // ... should be no interactions, since content still same - validateTwoListDiff( - storageNoPlaceholder, - storageWithPlaceholder) { - verifyZeroInteractions(it) - } - } - - @Test fun appendFill() { validateTwoListDiff( PagedStorage(5, listOf("a", "b"), 5), @@ -205,28 +185,6 @@ class PagedStorageDiffHelperTest { } } - @Test - fun transformAnchorIndex_loadingSnapshot() { - val oldList = PagedStorage(10, listOf("a"), 10) - val newList = PagedStorage(10, listOf("a"), 10) - - oldList.allocatePlaceholders(10, 5, 1, - /* ignored */ mock(PagedStorage.Callback::class.java)) - - assertEquals(5, oldList.leadingNullCount) - assertEquals(10, oldList.computeLeadingNulls()) - - validateTwoListDiffTransform( - oldList, - newList) { transformAnchorIndex -> - // previously, this would cause a crash where we tried to use storage space - // (getLeadingNullCount..size-getTrailingNulls) incorrectly, instead of diff space - // (computeLeadingNulls..size-computeTrailingNulls). Diff space greedily excludes the - // nulls that represent partially loaded pages to minimize diff computation cost. - assertEquals(15, transformAnchorIndex(15)) - } - } - companion object { private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<String>() { override fun areItemsTheSame(oldItem: String, newItem: String): Boolean { diff --git a/paging/runtime/src/main/java/androidx/paging/PagedStorageDiffHelper.kt b/paging/runtime/src/main/java/androidx/paging/PagedStorageDiffHelper.kt index ef182ca5da4..8e6fe2b3a8e 100644 --- a/paging/runtime/src/main/java/androidx/paging/PagedStorageDiffHelper.kt +++ b/paging/runtime/src/main/java/androidx/paging/PagedStorageDiffHelper.kt @@ -30,19 +30,16 @@ import androidx.recyclerview.widget.ListUpdateCallback * * To only inform DiffUtil about single loaded page in this case, by pruning all other nulls from * consideration. - * - * @see PagedStorage.computeLeadingNulls - * @see PagedStorage.computeTrailingNulls */ internal fun <T : Any> PagedStorage<T>.computeDiff( newList: PagedStorage<T>, diffCallback: DiffUtil.ItemCallback<T> ): DiffUtil.DiffResult { - val oldOffset = computeLeadingNulls() - val newOffset = newList.computeLeadingNulls() + val oldOffset = leadingNullCount + val newOffset = newList.leadingNullCount - val oldSize = size - oldOffset - computeTrailingNulls() - val newSize = newList.size - newOffset - newList.computeTrailingNulls() + val oldSize = size - oldOffset - trailingNullCount + val newSize = newList.size - newOffset - newList.trailingNullCount return DiffUtil.calculateDiff(object : DiffUtil.Callback() { override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { @@ -121,10 +118,10 @@ internal fun <T : Any> PagedStorage<T>.dispatchDiff( newList: PagedStorage<T>, diffResult: DiffUtil.DiffResult ) { - val trailingOld = computeTrailingNulls() - val trailingNew = newList.computeTrailingNulls() - val leadingOld = computeLeadingNulls() - val leadingNew = newList.computeLeadingNulls() + val trailingOld = trailingNullCount + val trailingNew = newList.trailingNullCount + val leadingOld = leadingNullCount + val leadingNew = newList.leadingNullCount if (trailingOld == 0 && trailingNew == 0 && @@ -168,13 +165,13 @@ internal fun PagedStorage<*>.transformAnchorIndex( newList: PagedStorage<*>, oldPosition: Int ): Int { - val oldOffset = computeLeadingNulls() + val oldOffset = leadingNullCount // diffResult's indices starting after nulls, need to transform to diffutil indices // (see also dispatchDiff(), which adds this offset when dispatching) val diffIndex = oldPosition - oldOffset - val oldSize = size - oldOffset - computeTrailingNulls() + val oldSize = size - oldOffset - trailingNullCount // if our anchor is non-null, use it or close item's position in new list if (diffIndex in 0 until oldSize) { |