fix: multiple recyclerview adapter regressions in SubscriptionsFragment (#7085)

* Fix IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder

* Fix new videos not being added to feed

* Remove obsolete sortedFeed variable

* Display items from start
This commit is contained in:
Thomas W. 2025-02-10 17:54:37 +01:00 committed by GitHub
parent 18dd76093c
commit 481828c0f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 20 additions and 39 deletions

View File

@ -18,9 +18,6 @@ import com.github.libretube.ui.viewholders.SubscriptionChannelViewHolder
class SubscriptionChannelAdapter : class SubscriptionChannelAdapter :
ListAdapter<Subscription, SubscriptionChannelViewHolder>(DiffUtilItemCallback()) { ListAdapter<Subscription, SubscriptionChannelViewHolder>(DiffUtilItemCallback()) {
private var visibleCount = 20
override fun getItemCount() = minOf(visibleCount, currentList.size)
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
@ -31,13 +28,6 @@ class SubscriptionChannelAdapter :
return SubscriptionChannelViewHolder(binding) return SubscriptionChannelViewHolder(binding)
} }
fun updateItems() {
val oldSize = visibleCount
visibleCount += minOf(10, currentList.size - oldSize)
if (visibleCount == oldSize) return
notifyItemRangeInserted(oldSize, visibleCount)
}
override fun onBindViewHolder(holder: SubscriptionChannelViewHolder, position: Int) { override fun onBindViewHolder(holder: SubscriptionChannelViewHolder, position: Int) {
val subscription = getItem(holder.bindingAdapterPosition) val subscription = getItem(holder.bindingAdapterPosition)

View File

@ -65,8 +65,6 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
private var isAppBarFullyExpanded = true private var isAppBarFullyExpanded = true
private var feedAdapter = VideosAdapter() private var feedAdapter = VideosAdapter()
private val sortedFeed: MutableList<StreamItem> = mutableListOf()
private var selectedSortOrder = PreferenceHelper.getInt(PreferenceKeys.FEED_SORT_ORDER, 0) private var selectedSortOrder = PreferenceHelper.getInt(PreferenceKeys.FEED_SORT_ORDER, 0)
set(value) { set(value) {
PreferenceHelper.putInt(PreferenceKeys.FEED_SORT_ORDER, value) PreferenceHelper.putInt(PreferenceKeys.FEED_SORT_ORDER, value)
@ -175,15 +173,10 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
if (viewModel.subscriptions.value != null && isCurrentTabSubChannels) { if (viewModel.subscriptions.value != null && isCurrentTabSubChannels) {
binding.subRefresh.isRefreshing = true binding.subRefresh.isRefreshing = true
channelsAdapter.updateItems()
binding.subRefresh.isRefreshing = false binding.subRefresh.isRefreshing = false
} }
} }
binding.subFeed.addOnBottomReachedListener {
loadNextFeedItems()
}
// add some extra margin to the subscribed channels while the mini player is visible // add some extra margin to the subscribed channels while the mini player is visible
// otherwise the last channel would be invisible // otherwise the last channel would be invisible
playerModel.isMiniPlayerVisible.observe(viewLifecycleOwner) { playerModel.isMiniPlayerVisible.observe(viewLifecycleOwner) {
@ -192,8 +185,8 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
} }
} }
binding.channelGroups.setOnCheckedStateChangeListener { group, checkedIds -> binding.channelGroups.setOnCheckedStateChangeListener { group, _ ->
selectedFilterGroup = group.children.indexOfFirst { it.id == checkedIds.first() } selectedFilterGroup = group.children.indexOfFirst { it.id == group.checkedChipId }
if (isCurrentTabSubChannels) showSubscriptions() else showFeed() if (isCurrentTabSubChannels) showSubscriptions() else showFeed()
} }
@ -231,25 +224,22 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
} }
} }
private fun loadNextFeedItems() { private fun loadFeedItems(sortedFeed: List<StreamItem>) {
val binding = _binding ?: return val binding = _binding ?: return
val hasMore = sortedFeed.size > feedAdapter.itemCount if (viewModel.videoFeed.value != null && !isCurrentTabSubChannels && !binding.subRefresh.isRefreshing) {
if (viewModel.videoFeed.value != null && !isCurrentTabSubChannels && !binding.subRefresh.isRefreshing && hasMore) {
binding.subRefresh.isRefreshing = true binding.subRefresh.isRefreshing = true
lifecycleScope.launch { lifecycleScope.launch {
val toIndex = minOf(feedAdapter.itemCount + 10, sortedFeed.size) val streamItemsToInsert = sortedFeed.let {
withContext(Dispatchers.IO) {
var streamItemsToInsert = sortedFeed runCatching { it.deArrow() }.getOrDefault(it)
.subList(feedAdapter.itemCount, toIndex) }
.toList()
withContext(Dispatchers.IO) {
runCatching { streamItemsToInsert = streamItemsToInsert.deArrow() }
} }
feedAdapter.insertItems(streamItemsToInsert) feedAdapter.submitList(streamItemsToInsert) {
binding.subFeed.scrollToPosition(0)
}
binding.subRefresh.isRefreshing = false binding.subRefresh.isRefreshing = false
} }
} }
@ -383,15 +373,13 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
val sorted = feed val sorted = feed
.sortedBySelectedOrder() .sortedBySelectedOrder()
.toMutableList() .toMutableList()
sortedFeed.clear()
sortedFeed.addAll(sorted)
// add an "all caught up item" // add an "all caught up item"
if (selectedSortOrder == 0) { if (selectedSortOrder == 0) {
val lastCheckedFeedTime = PreferenceHelper.getLastCheckedFeedTime() val lastCheckedFeedTime = PreferenceHelper.getLastCheckedFeedTime()
val caughtUpIndex = feed.indexOfFirst { it.uploaded <= lastCheckedFeedTime && !it.isUpcoming } val caughtUpIndex = feed.indexOfFirst { it.uploaded <= lastCheckedFeedTime && !it.isUpcoming }
if (caughtUpIndex > 0) { if (caughtUpIndex > 0) {
sortedFeed.add( sorted.add(
caughtUpIndex, caughtUpIndex,
StreamItem(type = VideosAdapter.CAUGHT_UP_STREAM_TYPE) StreamItem(type = VideosAdapter.CAUGHT_UP_STREAM_TYPE)
) )
@ -404,13 +392,13 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
val notLoaded = viewModel.videoFeed.value.isNullOrEmpty() val notLoaded = viewModel.videoFeed.value.isNullOrEmpty()
binding.subFeed.isGone = notLoaded binding.subFeed.isGone = notLoaded
binding.emptyFeed.isVisible = notLoaded binding.emptyFeed.isVisible = notLoaded
loadNextFeedItems() loadFeedItems(sorted)
binding.toggleSubs.text = getString(R.string.subscriptions) binding.toggleSubs.text = getString(R.string.subscriptions)
feed.firstOrNull { !it.isUpcoming }?.uploaded?.let { feed.firstOrNull { !it.isUpcoming }?.uploaded?.let {
PreferenceHelper.setLastFeedWatchedTime(it) PreferenceHelper.setLastFeedWatchedTime(it)
}; }
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
@ -423,9 +411,13 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
) )
if (legacySubscriptions) { if (legacySubscriptions) {
legacySubscriptionsAdapter.submitList(subscriptions) legacySubscriptionsAdapter.submitList(subscriptions) {
binding.subFeed.scrollToPosition(0)
}
} else { } else {
channelsAdapter.submitList(subscriptions) channelsAdapter.submitList(subscriptions) {
binding.subFeed.scrollToPosition(0)
}
} }
binding.subRefresh.isRefreshing = false binding.subRefresh.isRefreshing = false
@ -442,7 +434,6 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
fun removeItem(videoId: String) { fun removeItem(videoId: String) {
feedAdapter.removeItemById(videoId) feedAdapter.removeItemById(videoId)
sortedFeed.removeAll { it.url?.toID() != videoId }
} }
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {