Close #18668: Add stable ID cache for TabAdapter.getItemId
parent
7c46f5e74f
commit
e41344aa1f
@ -0,0 +1,45 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.tabstray.browser
|
||||||
|
|
||||||
|
import android.util.LruCache
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import mozilla.components.concept.tabstray.Tab
|
||||||
|
|
||||||
|
internal const val INITIAL_NUMBER_OF_TABS = 20
|
||||||
|
internal const val CACHE_SIZE_MULTIPLIER = 1.5
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storage for Browser tabs that need a stable ID for each item in a [RecyclerView.Adapter].
|
||||||
|
* This ID is commonly needed by [RecyclerView.Adapter.getItemId] when
|
||||||
|
* enabling [RecyclerView.Adapter.setHasStableIds].
|
||||||
|
*/
|
||||||
|
internal class TabAdapterIdStorage(initialSize: Int = INITIAL_NUMBER_OF_TABS) {
|
||||||
|
private val uniqueTabIds = LruCache<String, Long>(initialSize)
|
||||||
|
private var lastUsedSuggestionId = 0L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a unique tab ID for the given [Tab].
|
||||||
|
*/
|
||||||
|
fun getStableId(tab: Tab): Long {
|
||||||
|
val key = tab.id
|
||||||
|
return uniqueTabIds[key] ?: run {
|
||||||
|
lastUsedSuggestionId += 1
|
||||||
|
uniqueTabIds.put(key, lastUsedSuggestionId)
|
||||||
|
lastUsedSuggestionId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resizes the internal cache size if the [count] is larger than what is currently available.
|
||||||
|
*/
|
||||||
|
fun resizeCacheIfNeeded(count: Int) {
|
||||||
|
val currentMaxSize = uniqueTabIds.maxSize()
|
||||||
|
if (count > currentMaxSize) {
|
||||||
|
val newMaxSize = (count * CACHE_SIZE_MULTIPLIER).toInt()
|
||||||
|
uniqueTabIds.resize(newMaxSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.tabstray.browser
|
||||||
|
|
||||||
|
import mozilla.components.concept.tabstray.Tab
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@RunWith(FenixRobolectricTestRunner::class)
|
||||||
|
class TabAdapterIdStorageTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `the same ID is returned when queried multiple times`() {
|
||||||
|
val storage = TabAdapterIdStorage()
|
||||||
|
val tab = createTab()
|
||||||
|
|
||||||
|
val id1 = storage.getStableId(tab)
|
||||||
|
val id2 = storage.getStableId(tab)
|
||||||
|
|
||||||
|
assertEquals(id1, id2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `the same ID is returned when the cache is at max`() {
|
||||||
|
val storage = TabAdapterIdStorage(2)
|
||||||
|
val tab1 = createTab()
|
||||||
|
val tab2 = createTab()
|
||||||
|
|
||||||
|
val id1 = storage.getStableId(tab1)
|
||||||
|
val id2 = storage.getStableId(tab2)
|
||||||
|
val id1Again = storage.getStableId(tab1)
|
||||||
|
|
||||||
|
assertEquals(id1, id1Again)
|
||||||
|
assertNotEquals(id1, id2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `the same ID is NOT returned if the cache is over max`() {
|
||||||
|
val storage = TabAdapterIdStorage(2)
|
||||||
|
val tab1 = createTab()
|
||||||
|
val tab2 = createTab()
|
||||||
|
val tab3 = createTab()
|
||||||
|
|
||||||
|
val id1 = storage.getStableId(tab1)
|
||||||
|
val id2 = storage.getStableId(tab2)
|
||||||
|
val id3 = storage.getStableId(tab3)
|
||||||
|
val id1Again = storage.getStableId(tab1)
|
||||||
|
|
||||||
|
assertNotEquals(id1, id1Again)
|
||||||
|
assertNotEquals(id1, id2)
|
||||||
|
assertNotEquals(id1, id3)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `the same ID is returned if the cache is resized when full`() {
|
||||||
|
val storage = TabAdapterIdStorage(2)
|
||||||
|
val tab1 = createTab()
|
||||||
|
val tab2 = createTab()
|
||||||
|
val tab3 = createTab()
|
||||||
|
|
||||||
|
val id1 = storage.getStableId(tab1)
|
||||||
|
val id2 = storage.getStableId(tab2)
|
||||||
|
|
||||||
|
storage.resizeCacheIfNeeded(3)
|
||||||
|
|
||||||
|
val id3 = storage.getStableId(tab3)
|
||||||
|
val id1Again = storage.getStableId(tab1)
|
||||||
|
|
||||||
|
assertEquals(id1, id1Again)
|
||||||
|
assertNotEquals(id1, id2)
|
||||||
|
assertNotEquals(id1, id3)
|
||||||
|
assertNotEquals(id2, id3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createTab() = Tab(
|
||||||
|
UUID.randomUUID().toString(),
|
||||||
|
"https://mozilla.org"
|
||||||
|
)
|
@ -0,0 +1,76 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.tabstray.browser
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import mozilla.components.browser.tabstray.TabViewHolder
|
||||||
|
import mozilla.components.browser.tabstray.TabsTrayStyling
|
||||||
|
import mozilla.components.concept.tabstray.Tab
|
||||||
|
import mozilla.components.concept.tabstray.Tabs
|
||||||
|
import mozilla.components.concept.tabstray.TabsTray
|
||||||
|
import mozilla.components.support.base.observer.Observable
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||||
|
|
||||||
|
@RunWith(FenixRobolectricTestRunner::class)
|
||||||
|
class TabsAdapterTest {
|
||||||
|
|
||||||
|
lateinit var adapter: TabsAdapter<TestTabsAdapter.ViewHolder>
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
adapter = TestTabsAdapter()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getItemId gives a new ID for each position`() {
|
||||||
|
val (tab1, tab2, tab3) = Triple(createTab(), createTab(), createTab())
|
||||||
|
val tabs = Tabs(
|
||||||
|
list = listOf(tab1, tab2, tab3),
|
||||||
|
selectedIndex = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
adapter.updateTabs(tabs)
|
||||||
|
|
||||||
|
val id1 = adapter.getItemId(0)
|
||||||
|
val id2 = adapter.getItemId(1)
|
||||||
|
val id3 = adapter.getItemId(2)
|
||||||
|
val id1Again = adapter.getItemId(0)
|
||||||
|
|
||||||
|
assertEquals(id1, id1Again)
|
||||||
|
assertNotEquals(id1, id2)
|
||||||
|
assertNotEquals(id1, id3)
|
||||||
|
assertNotEquals(id2, id3)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalStateException::class)
|
||||||
|
fun `getItemId throws if a tab does not exist for the position`() {
|
||||||
|
adapter.getItemId(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestTabsAdapter : TabsAdapter<TestTabsAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
inner class ViewHolder(view: View) : TabViewHolder(view) {
|
||||||
|
override var tab: Tab? = null
|
||||||
|
|
||||||
|
override fun bind(
|
||||||
|
tab: Tab,
|
||||||
|
isSelected: Boolean,
|
||||||
|
styling: TabsTrayStyling,
|
||||||
|
observable: Observable<TabsTray.Observer>
|
||||||
|
) = throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): ViewHolder = throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue