all: use notifyItemChanged everywhere

Apply the notifyItemChanged fix everywhere by making it an explicit
part of the RecyclerView framework.

This way, implementing future selection and rewrite behavior will be
much easier, as the payload argument is available in every adapter
implementation.
This commit is contained in:
OxygenCobalt 2022-06-02 10:42:33 -06:00
parent 10afae0bfc
commit e73d10070b
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
11 changed files with 123 additions and 69 deletions

View file

@ -9,9 +9,11 @@
- Fixed crash when seeking to the end of a track as the track changed to a track with a lower duration
- Fixed regression where GadgetBridge media controls would no longer work
- Fixed issue where the album/artist/genre would not be correctly restored
- Fixed issue where items would not highlight properly in the detail UI
#### Dev/Meta
- Switched from `LiveData` to `StateFlow`
- Use `notifyItemChanged` instead of directly mutating `ViewHolder` instances.
## v2.3.0

View file

@ -45,7 +45,7 @@ import org.oxycblt.auxio.util.textSafe
class AlbumDetailAdapter(listener: Listener) :
DetailAdapter<AlbumDetailAdapter.Listener>(listener, DIFFER) {
private var currentSong: Song? = null
private var currentHighlightedSongPos: Int? = null
private var currentSongPos: Int? = null
override fun getCreatorFromItem(item: Item) =
super.getCreatorFromItem(item)
@ -65,13 +65,19 @@ class AlbumDetailAdapter(listener: Listener) :
else -> null
}
override fun onBind(viewHolder: RecyclerView.ViewHolder, item: Item, listener: Listener) {
super.onBind(viewHolder, item, listener)
when (item) {
is Album -> (viewHolder as AlbumDetailViewHolder).bind(item, listener)
is DiscHeader -> (viewHolder as DiscHeaderViewHolder).bind(item, Unit)
is Song -> (viewHolder as AlbumSongViewHolder).bind(item, listener)
override fun onBind(
viewHolder: RecyclerView.ViewHolder,
item: Item,
listener: Listener,
payload: List<Any>
) {
super.onBind(viewHolder, item, listener, payload)
if (payload.isEmpty()) {
when (item) {
is Album -> (viewHolder as AlbumDetailViewHolder).bind(item, listener)
is DiscHeader -> (viewHolder as DiscHeaderViewHolder).bind(item, Unit)
is Song -> (viewHolder as AlbumSongViewHolder).bind(item, listener)
}
}
}
@ -83,8 +89,8 @@ class AlbumDetailAdapter(listener: Listener) :
fun highlightSong(song: Song?) {
if (song == currentSong) return
currentSong = song
currentHighlightedSongPos?.let { notifyItemChanged(it, PAYLOAD_HIGHLIGHT_CHANGED) }
currentHighlightedSongPos = highlightItem(song)
currentSongPos?.let { pos -> notifyItemChanged(pos, PAYLOAD_HIGHLIGHT_CHANGED) }
currentSongPos = highlightItem(song)
}
companion object {

View file

@ -45,7 +45,7 @@ import org.oxycblt.auxio.util.textSafe
class ArtistDetailAdapter(listener: Listener) :
DetailAdapter<DetailAdapter.Listener>(listener, DIFFER) {
private var currentAlbum: Album? = null
private var currentHighlightedAlbumPos: Int? = null
private var currentAlbumPos: Int? = null
private var currentSong: Song? = null
private var currentHighlightedSongPos: Int? = null
@ -68,29 +68,35 @@ class ArtistDetailAdapter(listener: Listener) :
else -> null
}
override fun onBind(viewHolder: RecyclerView.ViewHolder, item: Item, listener: Listener) {
super.onBind(viewHolder, item, listener)
when (item) {
is Artist -> (viewHolder as ArtistDetailViewHolder).bind(item, listener)
is Album -> (viewHolder as ArtistAlbumViewHolder).bind(item, listener)
is Song -> (viewHolder as ArtistSongViewHolder).bind(item, listener)
else -> {}
override fun onBind(
viewHolder: RecyclerView.ViewHolder,
item: Item,
listener: Listener,
payload: List<Any>
) {
super.onBind(viewHolder, item, listener, payload)
if (payload.isEmpty()) {
when (item) {
is Artist -> (viewHolder as ArtistDetailViewHolder).bind(item, listener)
is Album -> (viewHolder as ArtistAlbumViewHolder).bind(item, listener)
is Song -> (viewHolder as ArtistSongViewHolder).bind(item, listener)
else -> {}
}
}
}
override fun onHighlightViewHolder(viewHolder: Highlightable, item: Item) {
viewHolder.setHighlighted(
(item is Album && item.id == currentAlbum?.id) ||
(item is Song && item.id == currentSong?.id)
)
(item is Song && item.id == currentSong?.id))
}
/** Update the current [album] that this adapter should highlight */
fun highlightAlbum(album: Album?) {
if (album == currentAlbum) return
currentAlbum = album
currentHighlightedAlbumPos?.let { notifyItemChanged(it, PAYLOAD_HIGHLIGHT_CHANGED) }
currentHighlightedAlbumPos = highlightItem(album)
currentAlbumPos?.let { pos -> notifyItemChanged(pos, PAYLOAD_HIGHLIGHT_CHANGED) }
currentAlbumPos = highlightItem(album)
}
/** Update the [song] that this adapter should highlight */

View file

@ -71,10 +71,17 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
else -> null
}
override fun onBind(viewHolder: RecyclerView.ViewHolder, item: Item, listener: L) {
when (item) {
is Header -> (viewHolder as NewHeaderViewHolder).bind(item, Unit)
is SortHeader -> (viewHolder as SortHeaderViewHolder).bind(item, listener)
override fun onBind(
viewHolder: RecyclerView.ViewHolder,
item: Item,
listener: L,
payload: List<Any>
) {
if (payload.isEmpty()) {
when (item) {
is Header -> (viewHolder as NewHeaderViewHolder).bind(item, Unit)
is SortHeader -> (viewHolder as SortHeaderViewHolder).bind(item, listener)
}
}
if (viewHolder is Highlightable) {

View file

@ -43,7 +43,7 @@ import org.oxycblt.auxio.util.textSafe
class GenreDetailAdapter(listener: Listener) :
DetailAdapter<DetailAdapter.Listener>(listener, DIFFER) {
private var currentSong: Song? = null
private var currentHighlightedSongPos: Int? = null
private var currentSongPos: Int? = null
override fun getCreatorFromItem(item: Item) =
super.getCreatorFromItem(item)
@ -61,12 +61,19 @@ class GenreDetailAdapter(listener: Listener) :
else -> null
}
override fun onBind(viewHolder: RecyclerView.ViewHolder, item: Item, listener: Listener) {
super.onBind(viewHolder, item, listener)
when (item) {
is Genre -> (viewHolder as GenreDetailViewHolder).bind(item, listener)
is Song -> (viewHolder as GenreSongViewHolder).bind(item, listener)
else -> {}
override fun onBind(
viewHolder: RecyclerView.ViewHolder,
item: Item,
listener: Listener,
payload: List<Any>
) {
super.onBind(viewHolder, item, listener, payload)
if (payload.isEmpty()) {
when (item) {
is Genre -> (viewHolder as GenreDetailViewHolder).bind(item, listener)
is Song -> (viewHolder as GenreSongViewHolder).bind(item, listener)
else -> {}
}
}
}
@ -78,8 +85,8 @@ class GenreDetailAdapter(listener: Listener) :
fun highlightSong(song: Song?) {
if (song == currentSong) return
currentSong = song
currentHighlightedSongPos?.let { notifyItemChanged(it, PAYLOAD_HIGHLIGHT_CHANGED) }
currentHighlightedSongPos = highlightItem(song)
currentSongPos?.let { pos -> notifyItemChanged(pos, PAYLOAD_HIGHLIGHT_CHANGED) }
currentSongPos = highlightItem(song)
}
companion object {

View file

@ -53,6 +53,7 @@ class TabAdapter(listener: Listener) :
fun setTab(at: Int, tab: Tab) {
tabs[at] = tab
adapter.notifyItemChanged(at, PAYLOAD_TAB_CHANGED)
}
fun moveItems(from: Int, to: Int) {
@ -63,18 +64,17 @@ class TabAdapter(listener: Listener) :
adapter.notifyItemMoved(from, to)
}
}
companion object {
val PAYLOAD_TAB_CHANGED = Any()
}
}
class TabViewHolder private constructor(private val binding: ItemTabBinding) :
BindingViewHolder<Tab, TabAdapter.Listener>(binding.root) {
@SuppressLint("ClickableViewAccessibility")
override fun bind(item: Tab, listener: TabAdapter.Listener) {
binding.root.apply {
setOnClickListener {
binding.tabIcon.isChecked = !binding.tabIcon.isChecked
listener.onVisibilityToggled(item.mode)
}
}
binding.root.apply { setOnClickListener { listener.onVisibilityToggled(item.mode) } }
binding.tabIcon.apply {
setText(item.mode.string)

View file

@ -60,7 +60,8 @@ class SearchAdapter(listener: MenuItemListener) : MultiAdapter<MenuItemListener>
override fun onBind(
viewHolder: RecyclerView.ViewHolder,
item: Item,
listener: MenuItemListener
listener: MenuItemListener,
payload: List<Any>
) {
when (item) {
is Song -> (viewHolder as SongViewHolder).bind(item, listener)

View file

@ -37,13 +37,23 @@ abstract class MonoAdapter<T, L, VH : BindingViewHolder<T, L>>(private val liste
/** The creator instance that all viewholders will be derived from. */
protected abstract val creator: BindingViewHolder.Creator<VH>
/**
* An optional override to further modify the given [viewHolder]. The normal operation is to
* bind the viewholder, with nothing more.
*/
open fun onBind(viewHolder: VH, item: T, listener: L, payload: List<Any>) {
viewHolder.bind(item, listener)
}
override fun getItemCount(): Int = data.getItemCount()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
creator.create(parent.context)
override fun onBindViewHolder(viewHolder: VH, position: Int) {
viewHolder.bind(data.getItem(position), listener)
override fun onBindViewHolder(holder: VH, position: Int) = throw UnsupportedOperationException()
override fun onBindViewHolder(viewHolder: VH, position: Int, payload: List<Any>) {
onBind(viewHolder, data.getItem(position), listener, payload)
}
}
@ -53,7 +63,7 @@ private typealias AnyCreator = BindingViewHolder.Creator<out RecyclerView.ViewHo
* An adapter for many viewholders tied to many types of data. Deriving this is more complicated
* than [MonoAdapter], as less overrides can be provided "for free".
* @author OxygenCobalt
*
*
* TODO: Force impls to handle payload situations.
*/
abstract class MultiAdapter<L>(private val listener: L) :
@ -78,7 +88,12 @@ abstract class MultiAdapter<L>(private val listener: L) :
* Bind the given viewholder to an item. Casting must be done on the consumer's end due to
* bounds on [BindingViewHolder].
*/
protected abstract fun onBind(viewHolder: RecyclerView.ViewHolder, item: Item, listener: L)
protected abstract fun onBind(
viewHolder: RecyclerView.ViewHolder,
item: Item,
listener: L,
payload: List<Any>
)
override fun getItemCount(): Int = data.getItemCount()
@ -94,8 +109,15 @@ abstract class MultiAdapter<L>(private val listener: L) :
}
.create(parent.context)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
onBind(holder, data.getItem(position), listener)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) =
throw UnsupportedOperationException()
override fun onBindViewHolder(
viewHolder: RecyclerView.ViewHolder,
position: Int,
payload: List<Any>
) {
onBind(viewHolder, data.getItem(position), listener, payload)
}
}

View file

@ -19,7 +19,6 @@ package org.oxycblt.auxio.ui.accent
import android.content.Context
import androidx.appcompat.widget.TooltipCompat
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemAccentBinding
import org.oxycblt.auxio.ui.BackingData
@ -38,28 +37,28 @@ class AccentAdapter(listener: Listener) :
MonoAdapter<Accent, AccentAdapter.Listener, AccentViewHolder>(listener) {
var selectedAccent: Accent? = null
private set
private var selectedViewHolder: AccentViewHolder? = null
override val data = AccentData()
override val creator = AccentViewHolder.CREATOR
override fun onBindViewHolder(viewHolder: AccentViewHolder, position: Int) {
super.onBindViewHolder(viewHolder, position)
if (data.getItem(position) == selectedAccent) {
selectedViewHolder?.setSelected(false)
selectedViewHolder = viewHolder
viewHolder.setSelected(true)
override fun onBind(
viewHolder: AccentViewHolder,
item: Accent,
listener: Listener,
payload: List<Any>
) {
if (payload.isEmpty()) {
super.onBind(viewHolder, item, listener, payload)
}
viewHolder.setSelected(item == selectedAccent)
}
fun setSelectedAccent(accent: Accent, recycler: RecyclerView) {
fun setSelectedAccent(accent: Accent) {
if (accent == selectedAccent) return
selectedAccent?.let { old -> notifyItemChanged(old.index, PAYLOAD_SELECTION_CHANGED) }
selectedAccent = accent
selectedViewHolder?.setSelected(false)
selectedViewHolder =
recycler.findViewHolderForAdapterPosition(accent.index) as AccentViewHolder?
selectedViewHolder?.setSelected(true)
notifyItemChanged(accent.index, PAYLOAD_SELECTION_CHANGED)
}
interface Listener {
@ -70,6 +69,10 @@ class AccentAdapter(listener: Listener) :
override fun getItem(position: Int) = Accent.from(position)
override fun getItemCount() = Accent.MAX
}
companion object {
val PAYLOAD_SELECTION_CHANGED = Any()
}
}
class AccentViewHolder private constructor(private val binding: ItemAccentBinding) :

View file

@ -55,17 +55,13 @@ class AccentCustomizeDialog :
}
override fun onBindingCreated(binding: DialogAccentBinding, savedInstanceState: Bundle?) {
// --- UI SETUP ---
binding.accentRecycler.adapter = accentAdapter
accentAdapter.setSelectedAccent(
if (savedInstanceState != null) {
Accent.from(savedInstanceState.getInt(KEY_PENDING_ACCENT))
} else {
settingsManager.accent
},
binding.accentRecycler)
})
}
override fun onSaveInstanceState(outState: Bundle) {
@ -78,7 +74,7 @@ class AccentCustomizeDialog :
}
override fun onAccentSelected(accent: Accent) {
accentAdapter.setSelectedAccent(accent, requireBinding().accentRecycler)
accentAdapter.setSelectedAccent(accent)
}
companion object {

View file

@ -169,7 +169,11 @@ fun Fragment.launch(
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(state, block) }
}
/** Combines the called flow with the given flow and then collects them both into [block]. */
/**
* Combines the called flow with the given flow and then collects them both into [block].
* This is a bit of a dumb hack with [combine], as when we have to combine flows, we often
* just want to call the same block with both functions, and not do any transformations.
*/
suspend fun <T1, T2> Flow<T1>.collectWith(other: Flow<T2>, block: suspend (T1, T2) -> Unit) {
combine(this, other) { a, b -> a to b }.collect { block(it.first, it.second) }
}