Close #18774: Migrate mutli-selection to store

Removes the recyclerview-selection library and replaces it with the
SelectionHolder/SelectionInteractor with a Store.

This is an implementation that's similar to what we have in other UI
lists (library).
upstream-sync
Jonathan Almeida 3 years ago committed by Jonathan Almeida
parent 499aa858b2
commit 9078139e40

@ -533,7 +533,6 @@ dependencies {
implementation Deps.androidx_navigation_fragment
implementation Deps.androidx_navigation_ui
implementation Deps.androidx_recyclerview
implementation Deps.androidx_recyclerview_selection
implementation Deps.androidx_lifecycle_livedata
implementation Deps.androidx_lifecycle_runtime
implementation Deps.androidx_lifecycle_viewmodel

@ -0,0 +1,26 @@
/* 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
import android.app.Dialog
import android.content.Context
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
/**
* Default tabs tray dialog implementation for overriding the default on back pressed.
*/
class TabsTrayDialog(
context: Context,
theme: Int,
private val interactor: () -> BrowserTrayInteractor
) : Dialog(context, theme) {
override fun onBackPressed() {
if (interactor.invoke().onBackPressed()) {
return
}
dismiss()
}
}

@ -19,7 +19,6 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.tabs.TabLayout
import kotlinx.android.synthetic.main.component_tabstray2.*
import kotlinx.android.synthetic.main.component_tabstray2.view.*
import org.mozilla.fenix.HomeActivity
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.lib.state.ext.consumeFrom
@ -29,6 +28,8 @@ import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.home.HomeScreenViewModel
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
@ -39,7 +40,6 @@ import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsInteractor
class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
private lateinit var behavior: BottomSheetBehavior<ConstraintLayout>
private lateinit var navigationInteractor: NavigationInteractor
private val tabLayout: TabLayout? get() =
@ -47,6 +47,9 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
private val isPrivateModeSelected: Boolean get() =
tabLayout?.selectedTabPosition == TrayPagerAdapter.POSITION_PRIVATE_TABS
private lateinit var tabsTrayStore: TabsTrayStore
private lateinit var browserTrayInteractor: BrowserTrayInteractor
private lateinit var behavior: BottomSheetBehavior<ConstraintLayout>
private val tabLayoutMediator = ViewBoundFeatureWrapper<TabLayoutMediator>()
@ -60,7 +63,8 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
}
private val removeUseCases by lazy {
RemoveTabUseCaseWrapper(requireComponents.analytics.metrics
RemoveTabUseCaseWrapper(
requireComponents.analytics.metrics
) {
tabRemoved(it)
}
@ -71,6 +75,9 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
setStyle(STYLE_NO_TITLE, R.style.TabTrayDialogStyle)
}
override fun onCreateDialog(savedInstanceState: Bundle?) =
TabsTrayDialog(requireContext(), theme) { browserTrayInteractor }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -92,6 +99,8 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome
)
tabsTrayStore = StoreProvider.get(this) { TabsTrayStore() }
return containerView
}
@ -100,11 +109,12 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
super.onViewCreated(view, savedInstanceState)
setupMenu(view)
val browserTrayInteractor = DefaultBrowserTrayInteractor(
this,
browserTrayInteractor = DefaultBrowserTrayInteractor(
tabsTrayStore,
selectTabUseCase,
removeUseCases,
requireComponents.settings
requireComponents.settings,
this
)
val syncedTabsTrayInteractor = SyncedTabsInteractor(
@ -113,7 +123,13 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
this
)
setupPager(view.context, this, browserTrayInteractor, syncedTabsTrayInteractor)
setupPager(
view.context,
tabsTrayStore,
this,
browserTrayInteractor,
syncedTabsTrayInteractor
)
tabLayoutMediator.set(
feature = TabLayoutMediator(
@ -160,6 +176,7 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
private fun setupPager(
context: Context,
store: TabsTrayStore,
trayInteractor: TabsTrayInteractor,
browserInteractor: BrowserTrayInteractor,
syncedTabsTrayInteractor: SyncedTabsInteractor
@ -167,9 +184,10 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
tabsTray.apply {
adapter = TrayPagerAdapter(
context,
trayInteractor,
store,
browserInteractor,
syncedTabsTrayInteractor
syncedTabsTrayInteractor,
trayInteractor
)
isUserInputEnabled = false
}

@ -4,7 +4,6 @@
package org.mozilla.fenix.tabstray
import android.view.MotionEvent
import android.view.View
import android.widget.ImageButton
import android.widget.ImageView
@ -12,8 +11,8 @@ import android.widget.TextView
import androidx.annotation.VisibleForTesting
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.AppCompatImageButton
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.recyclerview.selection.ItemDetailsLookup
import kotlinx.android.synthetic.main.checkbox_item.view.*
import mozilla.components.browser.state.selector.findTabOrCustomTab
import mozilla.components.browser.state.store.BrowserStore
@ -36,6 +35,8 @@ import org.mozilla.fenix.ext.removeAndDisable
import org.mozilla.fenix.ext.removeTouchDelegate
import org.mozilla.fenix.ext.showAndEnable
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.selection.SelectionHolder
import org.mozilla.fenix.selection.SelectionInteractor
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
/**
@ -45,7 +46,9 @@ abstract class TabsTrayViewHolder(
itemView: View,
private val imageLoader: ImageLoader,
private val thumbnailSize: Int,
private val browserTrayInteractor: BrowserTrayInteractor?,
private val browserTrayInteractor: BrowserTrayInteractor,
private val trayStore: TabsTrayStore,
private val selectionHolder: SelectionHolder<Tab>?,
private val store: BrowserStore = itemView.context.components.core.store,
private val metrics: MetricController = itemView.context.components.analytics.metrics
) : TabViewHolder(itemView) {
@ -81,13 +84,63 @@ abstract class TabsTrayViewHolder(
updateFavicon(tab)
updateCloseButtonDescription(tab.title)
updateSelectedTabIndicator(isSelected)
updateMediaState(tab)
selectionHolder?.let {
setSelectionInteractor(tab, it, browserTrayInteractor)
}
if (tab.thumbnail != null) {
thumbnailView.setImageBitmap(tab.thumbnail)
} else {
loadIntoThumbnailView(thumbnailView, tab.id)
}
}
fun showTabIsMultiSelectEnabled(isSelected: Boolean) {
itemView.selected_mask.isVisible = isSelected
closeView.isInvisible = trayStore.state.mode is TabsTrayState.Mode.Select
}
private fun updateFavicon(tab: Tab) {
if (tab.icon != null) {
faviconView?.visibility = View.VISIBLE
faviconView?.setImageBitmap(tab.icon)
} else {
faviconView?.visibility = View.GONE
}
}
private fun updateTitle(tab: Tab) {
val title = if (tab.title.isNotEmpty()) {
tab.title
} else {
tab.url
}
titleView.text = title
}
private fun updateUrl(tab: Tab) {
// Truncate to MAX_URI_LENGTH to prevent the UI from locking up for
// extremely large URLs such as data URIs or bookmarklets. The same
// is done in the toolbar and awesomebar:
// https://github.com/mozilla-mobile/fenix/issues/1824
// https://github.com/mozilla-mobile/android-components/issues/6985
urlView?.text = tab.url
.toShortUrl(itemView.context.components.publicSuffixList)
.take(MAX_URI_LENGTH)
}
private fun updateCloseButtonDescription(title: String) {
closeView.contentDescription =
closeView.context.getString(R.string.close_tab_title, title)
}
/**
* NB: Why do we query for the media state from the store, when we have [Tab.playbackState] and
* [Tab.controller] already mapped?
*/
private fun updateMediaState(tab: Tab) {
// Media state
playPauseButtonView.increaseTapArea(PLAY_PAUSE_BUTTON_EXTRA_DPS)
@ -136,63 +189,34 @@ abstract class TabsTrayViewHolder(
}
}
}
closeView.setOnClickListener {
observable.notifyObservers { onTabClosed(tab) }
}
}
fun getItemDetails() = object : ItemDetailsLookup.ItemDetails<Long>() {
override fun getPosition(): Int = bindingAdapterPosition
override fun getSelectionKey(): Long = itemId
override fun inSelectionHotspot(e: MotionEvent): Boolean {
return browserTrayInteractor?.isMultiSelectMode() == true
}
}
fun showTabIsMultiSelectEnabled(isSelected: Boolean) {
itemView.selected_mask.isVisible = isSelected
// TODO Enable this with https://github.com/mozilla-mobile/fenix/issues/18656
// itemView.mozac_browser_tabstray_close.isVisible =
// browserTrayInteractor?.isMultiSelectMode() == false
private fun loadIntoThumbnailView(thumbnailView: ImageView, id: String) {
imageLoader.loadIntoView(thumbnailView, ImageLoadRequest(id, thumbnailSize))
}
private fun updateFavicon(tab: Tab) {
if (tab.icon != null) {
faviconView?.visibility = View.VISIBLE
faviconView?.setImageBitmap(tab.icon)
} else {
faviconView?.visibility = View.GONE
private fun setSelectionInteractor(
item: Tab,
holder: SelectionHolder<Tab>,
interactor: SelectionInteractor<Tab>
) {
itemView.setOnClickListener {
val selected = holder.selectedItems
when {
selected.isEmpty() -> interactor.open(item)
item in selected -> interactor.deselect(item)
else -> interactor.select(item)
}
}
}
private fun updateTitle(tab: Tab) {
val title = if (tab.title.isNotEmpty()) {
tab.title
} else {
tab.url
itemView.setOnLongClickListener {
if (holder.selectedItems.isEmpty()) {
interactor.select(item)
true
} else {
false
}
}
titleView.text = title
}
private fun updateUrl(tab: Tab) {
// Truncate to MAX_URI_LENGTH to prevent the UI from locking up for
// extremely large URLs such as data URIs or bookmarklets. The same
// is done in the toolbar and awesomebar:
// https://github.com/mozilla-mobile/fenix/issues/1824
// https://github.com/mozilla-mobile/android-components/issues/6985
urlView?.text = tab.url
.toShortUrl(itemView.context.components.publicSuffixList)
.take(MAX_URI_LENGTH)
}
private fun updateCloseButtonDescription(title: String) {
closeView.contentDescription =
closeView.context.getString(R.string.close_tab_title, title)
}
private fun loadIntoThumbnailView(thumbnailView: ImageView, id: String) {
imageLoader.loadIntoView(thumbnailView, ImageLoadRequest(id, thumbnailSize))
}
companion object {

@ -18,14 +18,15 @@ import org.mozilla.fenix.sync.SyncedTabsAdapter
import org.mozilla.fenix.tabstray.viewholders.SyncedTabViewHolder
class TrayPagerAdapter(
val context: Context,
val interactor: TabsTrayInteractor,
val browserInteractor: BrowserTrayInteractor,
val syncedTabsInteractor: SyncedTabsView.Listener
private val context: Context,
private val store: TabsTrayStore,
private val browserInteractor: BrowserTrayInteractor,
private val syncedTabsInteractor: SyncedTabsView.Listener,
private val interactor: TabsTrayInteractor
) : RecyclerView.Adapter<AbstractTrayViewHolder>() {
private val normalAdapter by lazy { BrowserTabsAdapter(context, browserInteractor) }
private val privateAdapter by lazy { BrowserTabsAdapter(context, browserInteractor) }
private val normalAdapter by lazy { BrowserTabsAdapter(context, browserInteractor, store) }
private val privateAdapter by lazy { BrowserTabsAdapter(context, browserInteractor, store) }
private val syncedTabsAdapter by lazy { SyncedTabsAdapter(syncedTabsInteractor) }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractTrayViewHolder {
@ -33,6 +34,7 @@ class TrayPagerAdapter(
return when (viewType) {
NormalBrowserTabViewHolder.LAYOUT_ID -> NormalBrowserTabViewHolder(
store,
itemView,
interactor
)

@ -6,16 +6,18 @@ package org.mozilla.fenix.tabstray.browser
import android.content.Context
import android.view.ViewGroup
import androidx.recyclerview.selection.SelectionTracker
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.GridLayoutManager
import kotlinx.android.synthetic.main.tab_tray_item.view.*
import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM
import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_HIGHLIGHT_SELECTED_ITEM
import mozilla.components.browser.thumbnails.loader.ThumbnailLoader
import mozilla.components.concept.tabstray.Tab
import mozilla.components.concept.tabstray.TabsTray
import mozilla.components.support.base.observer.Observable
import mozilla.components.support.base.observer.ObserverRegistry
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.selection.SelectionHolder
import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.tabstray.TabsTrayViewHolder
/**
@ -24,7 +26,7 @@ import org.mozilla.fenix.tabstray.TabsTrayViewHolder
class BrowserTabsAdapter(
private val context: Context,
private val interactor: BrowserTrayInteractor,
private val layoutManager: (() -> GridLayoutManager)? = null,
private val store: TabsTrayStore,
delegate: Observable<TabsTray.Observer> = ObserverRegistry()
) : TabsAdapter<TabsTrayViewHolder>(delegate) {
@ -39,12 +41,13 @@ class BrowserTabsAdapter(
/**
* Tracks the selected tabs in multi-select mode.
*/
var tracker: SelectionTracker<Long>? = null
var selectionHolder: SelectionHolder<Tab>? = null
private val selectedItemAdapterBinding = SelectedItemAdapterBinding(store, this)
private val imageLoader = ThumbnailLoader(context.components.core.thumbnailStorage)
override fun getItemViewType(position: Int): Int {
return if (context.settings().gridTabView) {
return if (context.components.settings.gridTabView) {
ViewType.GRID.ordinal
} else {
ViewType.LIST.ordinal
@ -53,8 +56,20 @@ class BrowserTabsAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabsTrayViewHolder {
return when (viewType) {
ViewType.GRID.ordinal -> TabsTrayGridViewHolder(parent, imageLoader, interactor)
else -> TabsTrayListViewHolder(parent, imageLoader, interactor)
ViewType.GRID.ordinal -> TabsTrayGridViewHolder(
parent,
imageLoader,
interactor,
store,
selectionHolder
)
else -> TabsTrayListViewHolder(
parent,
imageLoader,
interactor,
store,
selectionHolder
)
}
}
@ -62,17 +77,48 @@ class BrowserTabsAdapter(
super.onBindViewHolder(holder, position)
holder.tab?.let { tab ->
holder.itemView.setOnClickListener {
interactor.onOpenTab(tab)
holder.itemView.mozac_browser_tabstray_close.setOnClickListener {
interactor.close(tab)
}
holder.itemView.mozac_browser_tabstray_close.setOnClickListener {
interactor.onCloseTab(tab)
selectionHolder?.let {
holder.showTabIsMultiSelectEnabled(it.selectedItems.contains(tab))
}
}
}
/**
* Over-ridden [onBindViewHolder] that uses the payloads to notify the selected tab how to
* display itself.
*/
override fun onBindViewHolder(holder: TabsTrayViewHolder, position: Int, payloads: List<Any>) {
val tabs = tabs ?: return
if (tabs.list.isEmpty()) return
tracker?.let {
holder.showTabIsMultiSelectEnabled(it.isSelected(getItemId(position)))
if (payloads.isEmpty()) {
onBindViewHolder(holder, position)
return
}
if (position == tabs.selectedIndex) {
if (payloads.contains(PAYLOAD_HIGHLIGHT_SELECTED_ITEM)) {
holder.updateSelectedTabIndicator(true)
} else if (payloads.contains(PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM)) {
holder.updateSelectedTabIndicator(false)
}
}
selectionHolder?.let {
holder.showTabIsMultiSelectEnabled(it.selectedItems.contains(holder.tab))
}
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
selectedItemAdapterBinding.start()
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
selectedItemAdapterBinding.start()
}
}

@ -9,31 +9,26 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.concept.tabstray.Tab
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.support.base.feature.UserInteractionHandler
import org.mozilla.fenix.selection.SelectionInteractor
import org.mozilla.fenix.tabstray.TabsTrayAction
import org.mozilla.fenix.tabstray.TabsTrayInteractor
import org.mozilla.fenix.tabstray.TrayPagerAdapter
import org.mozilla.fenix.tabstray.ext.numberOfGridColumns
import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.tabstray.TabsTrayState.Mode
import org.mozilla.fenix.tabstray.TabsTrayStore
/**
* For interacting with UI that is specifically for [BaseBrowserTrayList] and other browser
* tab tray views.
*/
interface BrowserTrayInteractor {
/**
* Select the tab.
*/
fun onOpenTab(tab: Tab)
interface BrowserTrayInteractor : SelectionInteractor<Tab>, UserInteractionHandler {
/**
* Close the tab.
*/
fun onCloseTab(tab: Tab)
/**
* If multi-select mode is enabled or disabled.
*/
fun isMultiSelectMode(): Boolean
fun close(tab: Tab)
/**
* Returns the appropriate [RecyclerView.LayoutManager] to be used at [position].
@ -45,32 +40,52 @@ interface BrowserTrayInteractor {
* A default implementation of [BrowserTrayInteractor].
*/
class DefaultBrowserTrayInteractor(
private val trayInteractor: TabsTrayInteractor,
private val store: TabsTrayStore,
private val selectTabUseCase: TabsUseCases.SelectTabUseCase,
private val removeUseCases: TabsUseCases.RemoveTabUseCase,
private val settings: Settings
private val settings: Settings,
private val trayInteractor: TabsTrayInteractor
) : BrowserTrayInteractor {
/**
* See [BrowserTrayInteractor.onOpenTab].
* See [SelectionInteractor.open]
*/
override fun onOpenTab(tab: Tab) {
selectTabUseCase.invoke(tab.id)
override fun open(item: Tab) {
selectTabUseCase.invoke(item.id)
trayInteractor.navigateToBrowser()
}
/**
* See [BrowserTrayInteractor.onCloseTab].
* See [BrowserTrayInteractor.close].
*/
override fun onCloseTab(tab: Tab) {
override fun close(tab: Tab) {
removeUseCases.invoke(tab.id)
}
/**
* See [BrowserTrayInteractor.isMultiSelectMode].
* See [SelectionInteractor.select]
*/
override fun isMultiSelectMode(): Boolean {
// Needs https://github.com/mozilla-mobile/fenix/issues/18513 to change this value
override fun select(item: Tab) {
store.dispatch(TabsTrayAction.AddSelectTab(item))
}
/**
* See [SelectionInteractor.deselect]
*/
override fun deselect(item: Tab) {
store.dispatch(TabsTrayAction.RemoveSelectTab(item))
}
/**
* See [UserInteractionHandler.onBackPressed]
*
* TODO move this to the navigation interactor when it lands.
*/
override fun onBackPressed(): Boolean {
if (store.state.mode is Mode.Select) {
store.dispatch(TabsTrayAction.ExitSelectMode)
return true
}
return false
}

@ -0,0 +1,54 @@
/* 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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.map
import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM
import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_HIGHLIGHT_SELECTED_ITEM
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.tabstray.TabsTrayState.Mode
import org.mozilla.fenix.tabstray.TabsTrayStore
/**
* Notifies the adapter when the selection mode changes.
*/
class SelectedItemAdapterBinding(
val store: TabsTrayStore,
val adapter: BrowserTabsAdapter
) : LifecycleAwareFeature {
private var scope: CoroutineScope? = null
@OptIn(ExperimentalCoroutinesApi::class)
override fun start() {
scope = store.flowScoped { flow ->
flow.map { it.mode }
// ignore initial mode update; the adapter is already in an updated state.
.drop(1)
.ifChanged()
.collect { mode ->
notifyAdapter(mode)
}
}
}
override fun stop() {
scope?.cancel()
}
private fun notifyAdapter(mode: Mode) = with(adapter) {
if (mode == Mode.Normal) {
notifyItemRangeChanged(0, itemCount, PAYLOAD_HIGHLIGHT_SELECTED_ITEM)
} else {
notifyItemRangeChanged(0, itemCount, PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM)
}
}
}

@ -1,45 +0,0 @@
/* 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)
}
}
}

@ -31,18 +31,10 @@ abstract class TabsAdapter<T : TabViewHolder>(
protected var tabs: Tabs? = null
protected var styling: TabsTrayStyling = TabsTrayStyling()
private val idStorage = TabAdapterIdStorage()
init {
setHasStableIds(true)
}
@CallSuper
override fun updateTabs(tabs: Tabs) {
this.tabs = tabs
idStorage.resizeCacheIfNeeded(tabs.list.size)
notifyObservers { onTabsUpdated() }
}
@ -53,13 +45,6 @@ abstract class TabsAdapter<T : TabViewHolder>(
holder.bind(tabs.list[position], isTabSelected(tabs, position), styling, this)
}
override fun getItemId(position: Int): Long {
val key = tabs?.list?.get(position)
?: throw IllegalStateException("Unknown tab for position $position")
return idStorage.getStableId(key)
}
override fun getItemCount(): Int = tabs?.list?.size ?: 0
final override fun isTabSelected(tabs: Tabs, position: Int): Boolean =

@ -1,28 +0,0 @@
/* 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.MotionEvent
import androidx.recyclerview.selection.ItemDetailsLookup
import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails
import androidx.recyclerview.widget.RecyclerView
import org.mozilla.fenix.tabstray.TabsTrayViewHolder
/**
* An [ItemDetailsLookup] for retrieving the [ItemDetails] of a [TabsTrayViewHolder].
*/
class TabsDetailsLookup(
private val recyclerView: RecyclerView
) : ItemDetailsLookup<Long>() {
override fun getItemDetails(event: MotionEvent): ItemDetails<Long>? {
val view = recyclerView.findChildViewUnder(event.x, event.y)
if (view != null) {
val viewHolder = recyclerView.getChildViewHolder(view) as TabsTrayViewHolder
return viewHolder.getItemDetails()
}
return null
}
}

@ -1,24 +0,0 @@
/* 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 androidx.recyclerview.selection.ItemKeyProvider
import androidx.recyclerview.widget.RecyclerView
/**
* A key provider for the browser tabs.
*/
class TabsItemKeyProvider(private val recyclerView: RecyclerView) :
ItemKeyProvider<Long>(SCOPE_MAPPED) {
override fun getKey(position: Int): Long? {
return recyclerView.adapter?.getItemId(position)
}
override fun getPosition(key: Long): Int {
val viewHolder = recyclerView.findViewHolderForItemId(key)
return viewHolder?.layoutPosition ?: RecyclerView.NO_POSITION
}
}

@ -19,6 +19,9 @@ import org.mozilla.fenix.ext.increaseTapArea
import kotlin.math.max
import kotlinx.android.synthetic.main.tab_tray_grid_item.view.tab_tray_grid_item
import org.mozilla.fenix.tabstray.TabsTrayViewHolder
import org.mozilla.fenix.selection.SelectionHolder
import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
/**
* A RecyclerView ViewHolder implementation for "tab" items with grid layout.
@ -26,7 +29,9 @@ import org.mozilla.fenix.tabstray.TabsTrayViewHolder
class TabsTrayGridViewHolder(
parent: ViewGroup,
imageLoader: ImageLoader,
browserTrayInteractor: BrowserTrayInteractor? = null,
browserTrayInteractor: BrowserTrayInteractor,
store: TabsTrayStore,
selectionHolder: SelectionHolder<Tab>? = null,
itemView: View =
LayoutInflater.from(parent.context).inflate(R.layout.tab_tray_grid_item, parent, false),
thumbnailSize: Int =
@ -34,7 +39,14 @@ class TabsTrayGridViewHolder(
itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_grid_item_thumbnail_height),
itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_grid_item_thumbnail_width)
)
) : TabsTrayViewHolder(itemView, imageLoader, thumbnailSize, browserTrayInteractor) {
) : TabsTrayViewHolder(
itemView,
imageLoader,
thumbnailSize,
browserTrayInteractor,
store,
selectionHolder
) {
private val closeButton: AppCompatImageButton = itemView.findViewById(R.id.mozac_browser_tabstray_close)

@ -9,8 +9,12 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import mozilla.components.concept.base.images.ImageLoader
import mozilla.components.concept.tabstray.Tab
import org.mozilla.fenix.R
import org.mozilla.fenix.tabstray.TabsTrayViewHolder
import org.mozilla.fenix.selection.SelectionHolder
import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
import kotlin.math.max
/**
@ -19,7 +23,9 @@ import kotlin.math.max
class TabsTrayListViewHolder(
parent: ViewGroup,
imageLoader: ImageLoader,
browserTrayInteractor: BrowserTrayInteractor? = null,
browserTrayInteractor: BrowserTrayInteractor,
store: TabsTrayStore,
selectionHolder: SelectionHolder<Tab>? = null,
itemView: View =
LayoutInflater.from(parent.context).inflate(R.layout.tab_tray_item, parent, false),
thumbnailSize: Int =
@ -27,7 +33,14 @@ class TabsTrayListViewHolder(
itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_list_item_thumbnail_height),
itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_list_item_thumbnail_width)
)
) : TabsTrayViewHolder(itemView, imageLoader, thumbnailSize, browserTrayInteractor) {
) : TabsTrayViewHolder(
itemView,
imageLoader,
thumbnailSize,
browserTrayInteractor,
store,
selectionHolder
) {
override fun updateSelectedTabIndicator(showAsSelected: Boolean) {
val color = if (showAsSelected) {

@ -5,49 +5,39 @@
package org.mozilla.fenix.tabstray.viewholders
import android.view.View
import androidx.recyclerview.selection.SelectionPredicates
import androidx.recyclerview.selection.SelectionTracker
import androidx.recyclerview.selection.StorageStrategy
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.concept.tabstray.Tab
import org.mozilla.fenix.R
import org.mozilla.fenix.selection.SelectionHolder
import org.mozilla.fenix.tabstray.TabsTrayInteractor
import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.tabstray.browser.BrowserTabsAdapter
import org.mozilla.fenix.tabstray.browser.TabsDetailsLookup
import org.mozilla.fenix.tabstray.browser.TabsItemKeyProvider
/**
* View holder for the normal tabs tray list.
*/
class NormalBrowserTabViewHolder(
private val store: TabsTrayStore,
containerView: View,
interactor: TabsTrayInteractor
) : BaseBrowserTabViewHolder(containerView, interactor) {
) : BaseBrowserTabViewHolder(containerView, interactor), SelectionHolder<Tab> {
private lateinit var selectionTracker: SelectionTracker<Long>
/**
* Holds the list of selected tabs.
*
* Implementation notes: we do this here because we only want the normal tabs list to be able
* to select tabs.
*/
override val selectedItems: Set<Tab>
get() = store.state.mode.selectedTabs
override fun bind(
adapter: RecyclerView.Adapter<out RecyclerView.ViewHolder>,
layoutManager: RecyclerView.LayoutManager
) {
super.bind(adapter, layoutManager)
selectionTracker = SelectionTracker.Builder(
"mySelection",
trayList,
TabsItemKeyProvider(trayList),
TabsDetailsLookup(trayList),
StorageStrategy.createLongStorage()
).withSelectionPredicate(
SelectionPredicates.createSelectAnything()
).build()
(adapter as BrowserTabsAdapter).selectionHolder = this
(adapter as BrowserTabsAdapter).tracker = selectionTracker
selectionTracker.addObserver(object : SelectionTracker.SelectionObserver<Long>() {
override fun onItemStateChanged(key: Long, selected: Boolean) {
// TODO Do nothing for now; remove in a future patch if needed.
}
})
super.bind(adapter, layoutManager)
}
companion object {

@ -4,7 +4,6 @@
package org.mozilla.fenix.tabtray
import android.view.MotionEvent
import android.view.View
import android.widget.ImageButton
import android.widget.ImageView
@ -13,12 +12,7 @@ import androidx.annotation.VisibleForTesting
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.AppCompatImageButton
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.selection.ItemDetailsLookup
import kotlinx.android.synthetic.main.checkbox_item.view.*
import kotlinx.android.synthetic.main.tab_tray_grid_item.view.*
import kotlinx.android.synthetic.main.tab_tray_grid_item.view.mozac_browser_tabstray_close
import kotlinx.android.synthetic.main.tab_tray_item.view.*
import mozilla.components.browser.state.selector.findTabOrCustomTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.tabstray.TabViewHolder
@ -41,7 +35,6 @@ import org.mozilla.fenix.ext.removeTouchDelegate
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showAndEnable
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
import kotlin.math.max
/**
@ -51,8 +44,7 @@ class TabTrayViewHolder(
itemView: View,
private val imageLoader: ImageLoader,
private val store: BrowserStore = itemView.context.components.core.store,
private val metrics: MetricController = itemView.context.components.analytics.metrics,
private val browserTrayInteractor: BrowserTrayInteractor? = null
private val metrics: MetricController = itemView.context.components.analytics.metrics
) : TabViewHolder(itemView) {
private val faviconView: ImageView? =
@ -204,20 +196,6 @@ class TabTrayViewHolder(
)
}
fun getItemDetails() = object : ItemDetailsLookup.ItemDetails<Long>() {
override fun getPosition(): Int = bindingAdapterPosition
override fun getSelectionKey(): Long = itemId
override fun inSelectionHotspot(e: MotionEvent): Boolean {
return browserTrayInteractor?.isMultiSelectMode() == true
}
}
fun showTabIsMultiSelectEnabled(isSelected: Boolean) {
itemView.selected_mask.isVisible = isSelected
itemView.mozac_browser_tabstray_close.isVisible =
browserTrayInteractor?.isMultiSelectMode() == false
}
private fun updateCloseButtonDescription(title: String) {
closeView.contentDescription =
closeView.context.getString(R.string.close_tab_title, title)

@ -0,0 +1,24 @@
/* 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
import android.content.Context
import io.mockk.mockk
import io.mockk.verify
import org.junit.Test
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
class TabsTrayDialogTest {
@Test
fun `WHEN onBackPressed THEN invoke interactor`() {
val context = mockk<Context>(relaxed = true)
val interactor = mockk<BrowserTrayInteractor>(relaxed = true)
val dialog = TabsTrayDialog(context, 0) { interactor }
dialog.onBackPressed()
verify { interactor.onBackPressed() }
}
}

@ -0,0 +1,78 @@
/* 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 io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM
import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_HIGHLIGHT_SELECTED_ITEM
import mozilla.components.concept.tabstray.Tab
import mozilla.components.concept.tabstray.Tabs
import mozilla.components.support.test.robolectric.testContext
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.selection.SelectionHolder
import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.tabstray.TabsTrayViewHolder
@RunWith(FenixRobolectricTestRunner::class)
class BrowserTabsAdapterTest {
private val context = testContext
private val interactor = mockk<BrowserTrayInteractor>(relaxed = true)
private val store = TabsTrayStore()
@Test
fun `WHEN bind with payloads is called THEN update the holder`() {
val adapter = BrowserTabsAdapter(context, interactor, store)
val holder = mockk<TabsTrayViewHolder>(relaxed = true)
adapter.updateTabs(Tabs(
list = listOf(
createTab("tab1")
),
selectedIndex = 0
))
adapter.onBindViewHolder(holder, 0, listOf(PAYLOAD_HIGHLIGHT_SELECTED_ITEM))
verify { holder.updateSelectedTabIndicator(true) }
adapter.onBindViewHolder(holder, 0, listOf(PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM))
verify { holder.updateSelectedTabIndicator(false) }
}
@Test
fun `WHEN the selection holder is set THEN update the selected tab`() {
val adapter = BrowserTabsAdapter(context, interactor, store)
val holder = mockk<TabsTrayViewHolder>(relaxed = true)
val tab = createTab("tab1")
every { holder.tab }.answers { tab }
testSelectionHolder.internalState.add(tab)
adapter.selectionHolder = testSelectionHolder
adapter.updateTabs(Tabs(
list = listOf(
tab
),
selectedIndex = 0
))
adapter.onBindViewHolder(holder, 0, listOf(PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM))
verify { holder.showTabIsMultiSelectEnabled(true) }
}
private val testSelectionHolder = object : SelectionHolder<Tab> {
override val selectedItems: Set<Tab>
get() = internalState
val internalState = mutableSetOf<Tab>()
}
}

@ -32,7 +32,7 @@ class DefaultBrowserTrayInteractorTest {
@Test
fun `WHEN pager position is synced tabs THEN return a list layout manager`() {
val interactor = DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), mockk())
val interactor = DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), mockk(), mockk())
val result = interactor.getLayoutManagerForPosition(
mockk(),
@ -46,7 +46,7 @@ class DefaultBrowserTrayInteractorTest {
fun `WHEN setting is grid view THEN return grid layout manager`() {
val context = mockk<Context>()
val settings = mockk<Settings>()
val interactor = DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), settings)
val interactor = DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), settings, mockk())
every { context.numberOfGridColumns }.answers { 4 }
every { settings.gridTabView }.answers { true }
@ -63,7 +63,7 @@ class DefaultBrowserTrayInteractorTest {
fun `WHEN setting is list view THEN return list layout manager`() {
val context = mockk<Context>()
val settings = mockk<Settings>()
val interactor = DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), settings)
val interactor = DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), settings, mockk())
every { context.numberOfGridColumns }.answers { 4 }
every { settings.gridTabView }.answers { false }

@ -0,0 +1,70 @@
/* 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 io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM
import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_HIGHLIGHT_SELECTED_ITEM
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.tabstray.TabsTrayAction
import org.mozilla.fenix.tabstray.TabsTrayStore
class SelectedItemAdapterBindingTest {
@OptIn(ExperimentalCoroutinesApi::class)
@get:Rule
val coroutinesTestRule = MainCoroutineRule(TestCoroutineDispatcher())
private val adapter = mockk<BrowserTabsAdapter>(relaxed = true)
@Before
fun setup() {
every { adapter.itemCount }.answers { 1 }
}
@Test
fun `WHEN observing on start THEN ignore the initial state update`() {
val store = TabsTrayStore()
val binding = SelectedItemAdapterBinding(store, adapter)
binding.start()
verify(exactly = 0) {
adapter.notifyItemRangeChanged(any(), any(), any())
}
}
@Test
fun `WHEN mode changes THEN notify the adapter`() {
val store = TabsTrayStore()
val binding = SelectedItemAdapterBinding(store, adapter)
binding.start()
store.dispatch(TabsTrayAction.EnterSelectMode)
store.waitUntilIdle()
verify {
adapter.notifyItemRangeChanged(eq(0), eq(1), eq(PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM))
}
store.dispatch(TabsTrayAction.ExitSelectMode)
store.waitUntilIdle()
verify {
adapter.notifyItemRangeChanged(eq(0), eq(1), eq(PAYLOAD_HIGHLIGHT_SELECTED_ITEM))
}
}
}

@ -1,78 +0,0 @@
/* 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 org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@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)
}
}

@ -1,76 +0,0 @@
/* 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()
}
}

@ -29,7 +29,6 @@ object Versions {
const val androidx_fragment = "1.2.5"
const val androidx_navigation = "2.3.3"
const val androidx_recyclerview = "1.2.0-beta01"
const val androidx_recyclerview_selection = "1.0.0"
const val androidx_core = "1.3.2"
const val androidx_paging = "2.1.2"
const val androidx_transition = "1.4.0"
@ -186,7 +185,6 @@ object Deps {
const val androidx_navigation_fragment = "androidx.navigation:navigation-fragment-ktx:${Versions.androidx_navigation}"
const val androidx_navigation_ui = "androidx.navigation:navigation-ui:${Versions.androidx_navigation}"
const val androidx_recyclerview = "androidx.recyclerview:recyclerview:${Versions.androidx_recyclerview}"
const val androidx_recyclerview_selection = "androidx.recyclerview:recyclerview-selection:${Versions.androidx_recyclerview_selection}"
const val androidx_core = "androidx.core:core:${Versions.androidx_core}"
const val androidx_core_ktx = "androidx.core:core-ktx:${Versions.androidx_core}"
const val androidx_transition = "androidx.transition:transition:${Versions.androidx_transition}"

Loading…
Cancel
Save