recycler: spin off data into separate class

Spin off the data instances into their own class called BackingData.

This is to isolate the sane adapters that rely on one type of diffing
from the insane adapters that use synchronous and asynchronous diffing
simultaniously. It also allows some of the more esoteric adapters to
implement their own backing data without much trouble or leaky
abstractions.
This commit is contained in:
OxygenCobalt 2022-03-26 11:32:54 -06:00
parent ee1a234e76
commit 8100f294d7
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
22 changed files with 346 additions and 204 deletions

View file

@ -22,6 +22,8 @@ import coil.size.Size
import coil.size.pxOrElse
import coil.transform.Transformation
import kotlin.math.min
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
/**
* A transformation that performs a center crop-style transformation on an image, however unlike the
@ -45,8 +47,13 @@ class SquareFrameTransform : Transformation {
val dst = Bitmap.createBitmap(input, x, y, dstSize, dstSize)
if (dstSize != desiredWidth || dstSize != desiredHeight) {
// Desired size differs from the cropped size, resize the bitmap.
return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true)
logD("RETARD YOU STUPID FUCKING IDIOT $desiredWidth $desiredHeight")
try {
// Desired size differs from the cropped size, resize the bitmap.
return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true)
} catch (e: Exception) {
logE(e.stackTraceToString())
}
}
return dst

View file

@ -73,14 +73,16 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailItemListener {
requireBinding().detailRecycler.apply {
adapter = detailAdapter
applySpans { pos ->
val item = detailAdapter.currentList[pos]
val item = detailAdapter.data.currentList[pos]
item is Header || item is SortHeader || item is Album
}
}
// -- VIEWMODEL SETUP ---
detailModel.albumData.observe(viewLifecycleOwner) { list -> detailAdapter.submitList(list) }
detailModel.albumData.observe(viewLifecycleOwner) { list ->
detailAdapter.data.submitList(list)
}
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
handleNavigation(item, detailAdapter)
@ -168,7 +170,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailItemListener {
/** Scroll to an song using its [id]. */
private fun scrollToItem(id: Long, adapter: AlbumDetailAdapter) {
// Calculate where the item for the currently played song is
val pos = adapter.currentList.indexOfFirst { it.id == id && it is Song }
val pos = adapter.data.currentList.indexOfFirst { it.id == id && it is Song }
if (pos != -1) {
val binding = requireBinding()

View file

@ -55,7 +55,7 @@ class ArtistDetailFragment : DetailFragment(), DetailItemListener {
adapter = detailAdapter
applySpans { pos ->
// If the item is an ActionHeader we need to also make the item full-width
val item = detailAdapter.currentList[pos]
val item = detailAdapter.data.currentList[pos]
item is Header || item is SortHeader || item is Artist
}
}
@ -63,7 +63,7 @@ class ArtistDetailFragment : DetailFragment(), DetailItemListener {
// --- VIEWMODEL SETUP ---
detailModel.artistData.observe(viewLifecycleOwner) { list ->
detailAdapter.submitList(list)
detailAdapter.data.submitList(list)
}
detailModel.navToItem.observe(viewLifecycleOwner, ::handleNavigation)

View file

@ -53,14 +53,16 @@ class GenreDetailFragment : DetailFragment(), DetailItemListener {
binding.detailRecycler.apply {
adapter = detailAdapter
applySpans { pos ->
val item = detailAdapter.currentList[pos]
val item = detailAdapter.data.currentList[pos]
item is Header || item is SortHeader || item is Genre
}
}
// --- VIEWMODEL SETUP ---
detailModel.genreData.observe(viewLifecycleOwner) { list -> detailAdapter.submitList(list) }
detailModel.genreData.observe(viewLifecycleOwner) { list ->
detailAdapter.data.submitList(list)
}
detailModel.navToItem.observe(viewLifecycleOwner, ::handleNavigation)

View file

@ -30,8 +30,8 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.toDuration
import org.oxycblt.auxio.ui.BindingViewHolder
import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.ItemDiffCallback
import org.oxycblt.auxio.ui.MenuItemListener
import org.oxycblt.auxio.ui.SimpleItemCallback
import org.oxycblt.auxio.util.getPluralSafe
import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.textSafe
@ -98,7 +98,7 @@ class AlbumDetailAdapter(listener: AlbumDetailItemListener) :
companion object {
private val DIFFER =
object : ItemDiffCallback<Item>() {
object : SimpleItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return when {
oldItem is Album && newItem is Album ->
@ -154,7 +154,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
}
val DIFFER =
object : ItemDiffCallback<Album>() {
object : SimpleItemCallback<Album>() {
override fun areItemsTheSame(oldItem: Album, newItem: Album) =
oldItem.resolvedName == newItem.resolvedName &&
oldItem.resolvedArtistName == newItem.resolvedArtistName &&
@ -214,7 +214,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
}
val DIFFER =
object : ItemDiffCallback<Song>() {
object : SimpleItemCallback<Song>() {
override fun areItemsTheSame(oldItem: Song, newItem: Song) =
oldItem.resolvedName == newItem.resolvedName &&
oldItem.duration == newItem.duration

View file

@ -32,8 +32,8 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.ArtistViewHolder
import org.oxycblt.auxio.ui.BindingViewHolder
import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.ItemDiffCallback
import org.oxycblt.auxio.ui.MenuItemListener
import org.oxycblt.auxio.ui.SimpleItemCallback
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getPluralSafe
import org.oxycblt.auxio.util.inflater
@ -123,7 +123,7 @@ class ArtistDetailAdapter(listener: DetailItemListener) :
companion object {
private val DIFFER =
object : ItemDiffCallback<Item>() {
object : SimpleItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return when {
oldItem is Artist && newItem is Artist ->
@ -209,7 +209,7 @@ private constructor(
}
val DIFFER =
object : ItemDiffCallback<Album>() {
object : SimpleItemCallback<Album>() {
override fun areItemsTheSame(oldItem: Album, newItem: Album) =
oldItem.resolvedName == newItem.resolvedName && oldItem.year == newItem.year
}
@ -249,7 +249,7 @@ private constructor(
}
val DIFFER =
object : ItemDiffCallback<Song>() {
object : SimpleItemCallback<Song>() {
override fun areItemsTheSame(oldItem: Song, newItem: Song) =
oldItem.resolvedName == newItem.resolvedName &&
oldItem.resolvedAlbumName == newItem.resolvedAlbumName

View file

@ -25,13 +25,14 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.databinding.ItemSortHeaderBinding
import org.oxycblt.auxio.ui.AsyncBackingData
import org.oxycblt.auxio.ui.BindingViewHolder
import org.oxycblt.auxio.ui.Header
import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.ItemDiffCallback
import org.oxycblt.auxio.ui.MenuItemListener
import org.oxycblt.auxio.ui.MultiAdapter
import org.oxycblt.auxio.ui.NewHeaderViewHolder
import org.oxycblt.auxio.ui.SimpleItemCallback
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getViewHolderAt
import org.oxycblt.auxio.util.inflater
@ -41,7 +42,7 @@ import org.oxycblt.auxio.util.textSafe
abstract class DetailAdapter<L : DetailItemListener>(
listener: L,
diffCallback: DiffUtil.ItemCallback<Item>
) : MultiAdapter<L>(listener, diffCallback) {
) : MultiAdapter<L>(listener) {
abstract fun onHighlightViewHolder(viewHolder: Highlightable, item: Item)
protected inline fun <reified T : Item> highlightItem(
@ -54,7 +55,7 @@ abstract class DetailAdapter<L : DetailItemListener>(
// Use existing data instead of having to re-sort it.
// We also have to account for the album count when searching for the ViewHolder.
val pos = mCurrentList.indexOfFirst { item -> item.id == newItem.id && item is T }
val pos = data.currentList.indexOfFirst { item -> item.id == newItem.id && item is T }
// Check if the ViewHolder for this song is visible, if it is then highlight it.
// If the ViewHolder is not visible, then the adapter should take care of it if
@ -70,6 +71,8 @@ abstract class DetailAdapter<L : DetailItemListener>(
}
}
@Suppress("LeakingThis") override val data = AsyncBackingData(this, diffCallback)
override fun getCreatorFromItem(item: Item) =
when (item) {
is Header -> NewHeaderViewHolder.CREATOR
@ -97,7 +100,7 @@ abstract class DetailAdapter<L : DetailItemListener>(
companion object {
val DIFFER =
object : ItemDiffCallback<Item>() {
object : SimpleItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return when {
oldItem is Header && newItem is Header ->
@ -134,7 +137,7 @@ class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
}
val DIFFER =
object : ItemDiffCallback<SortHeader>() {
object : SimpleItemCallback<SortHeader>() {
override fun areItemsTheSame(oldItem: SortHeader, newItem: SortHeader) =
oldItem.string == newItem.string
}

View file

@ -29,8 +29,8 @@ import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.BindingViewHolder
import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.ItemDiffCallback
import org.oxycblt.auxio.ui.MenuItemListener
import org.oxycblt.auxio.ui.SimpleItemCallback
import org.oxycblt.auxio.ui.SongViewHolder
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getPluralSafe
@ -100,7 +100,7 @@ class GenreDetailAdapter(listener: DetailItemListener) :
companion object {
val DIFFER =
object : ItemDiffCallback<Item>() {
object : SimpleItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return when {
oldItem is Genre && newItem is Genre ->
@ -137,7 +137,7 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite
}
val DIFFER =
object : ItemDiffCallback<Genre>() {
object : SimpleItemCallback<Genre>() {
override fun areItemsTheSame(oldItem: Genre, newItem: Genre) =
oldItem.resolvedName == newItem.resolvedName &&
oldItem.songs.size == newItem.songs.size &&

View file

@ -18,17 +18,17 @@
package org.oxycblt.auxio.home.list
import android.view.View
import androidx.lifecycle.LiveData
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.HomeFragmentDirections
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.ui.AlbumViewHolder
import org.oxycblt.auxio.ui.BindingViewHolder
import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.MenuItemListener
import org.oxycblt.auxio.ui.MonoAdapter
import org.oxycblt.auxio.ui.PrimitiveBackingData
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.ui.sliceArticle
@ -38,10 +38,16 @@ import org.oxycblt.auxio.ui.sliceArticle
* @author
*/
class AlbumListFragment : HomeListFragment<Album>() {
override val recyclerId: Int = R.id.home_album_list
override val homeAdapter = AlbumAdapter(this)
override val homeData: LiveData<List<Album>>
get() = homeModel.albums
private val homeAdapter = AlbumAdapter(this)
override fun setupRecycler(recycler: RecyclerView) {
recycler.apply {
id = R.id.home_album_list
adapter = homeAdapter
}
homeModel.albums.observe(viewLifecycleOwner) { list -> homeAdapter.data.submitList(list) }
}
override fun getPopup(pos: Int): String? {
val album = homeModel.albums.value!![pos]
@ -72,8 +78,8 @@ class AlbumListFragment : HomeListFragment<Album>() {
}
class AlbumAdapter(listener: MenuItemListener) :
MonoAdapter<Album, MenuItemListener, AlbumViewHolder>(listener, AlbumViewHolder.DIFFER) {
override val creator: BindingViewHolder.Creator<AlbumViewHolder>
get() = AlbumViewHolder.CREATOR
MonoAdapter<Album, MenuItemListener, AlbumViewHolder>(listener) {
override val data = PrimitiveBackingData<Album>(this)
override val creator = AlbumViewHolder.CREATOR
}
}

View file

@ -18,8 +18,8 @@
package org.oxycblt.auxio.home.list
import android.view.View
import androidx.lifecycle.LiveData
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.HomeFragmentDirections
import org.oxycblt.auxio.music.Artist
@ -27,6 +27,7 @@ import org.oxycblt.auxio.ui.ArtistViewHolder
import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.MenuItemListener
import org.oxycblt.auxio.ui.MonoAdapter
import org.oxycblt.auxio.ui.PrimitiveBackingData
import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.ui.sliceArticle
@ -35,10 +36,16 @@ import org.oxycblt.auxio.ui.sliceArticle
* @author
*/
class ArtistListFragment : HomeListFragment<Artist>() {
override val recyclerId: Int = R.id.home_artist_list
override val homeAdapter = ArtistAdapter(this)
override val homeData: LiveData<List<Artist>>
get() = homeModel.artists
private val homeAdapter = ArtistAdapter(this)
override fun setupRecycler(recycler: RecyclerView) {
recycler.apply {
id = R.id.home_artist_list
adapter = homeAdapter
}
homeModel.artists.observe(viewLifecycleOwner) { list -> homeAdapter.data.submitList(list) }
}
override fun getPopup(pos: Int) =
homeModel.artists.value!![pos].resolvedName.sliceArticle().first().uppercase()
@ -53,7 +60,8 @@ class ArtistListFragment : HomeListFragment<Artist>() {
}
class ArtistAdapter(listener: MenuItemListener) :
MonoAdapter<Artist, MenuItemListener, ArtistViewHolder>(listener, ArtistViewHolder.DIFFER) {
MonoAdapter<Artist, MenuItemListener, ArtistViewHolder>(listener) {
override val data = PrimitiveBackingData<Artist>(this)
override val creator = ArtistViewHolder.CREATOR
}
}

View file

@ -18,8 +18,8 @@
package org.oxycblt.auxio.home.list
import android.view.View
import androidx.lifecycle.LiveData
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.HomeFragmentDirections
import org.oxycblt.auxio.music.Genre
@ -27,6 +27,7 @@ import org.oxycblt.auxio.ui.GenreViewHolder
import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.MenuItemListener
import org.oxycblt.auxio.ui.MonoAdapter
import org.oxycblt.auxio.ui.PrimitiveBackingData
import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.ui.sliceArticle
@ -35,10 +36,16 @@ import org.oxycblt.auxio.ui.sliceArticle
* @author
*/
class GenreListFragment : HomeListFragment<Genre>() {
override val recyclerId = R.id.home_genre_list
override val homeAdapter = GenreAdapter(this)
override val homeData: LiveData<List<Genre>>
get() = homeModel.genres
private val homeAdapter = GenreAdapter(this)
override fun setupRecycler(recycler: RecyclerView) {
recycler.apply {
id = R.id.home_genre_list
adapter = homeAdapter
}
homeModel.genres.observe(viewLifecycleOwner) { list -> homeAdapter.data.submitList(list) }
}
override fun getPopup(pos: Int) =
homeModel.genres.value!![pos].resolvedName.sliceArticle().first().uppercase()
@ -53,7 +60,8 @@ class GenreListFragment : HomeListFragment<Genre>() {
}
class GenreAdapter(listener: MenuItemListener) :
MonoAdapter<Genre, MenuItemListener, GenreViewHolder>(listener, GenreViewHolder.DIFFER) {
MonoAdapter<Genre, MenuItemListener, GenreViewHolder>(listener) {
override val data = PrimitiveBackingData<Genre>(this)
override val creator = GenreViewHolder.CREATOR
}
}

View file

@ -21,17 +21,14 @@ import android.os.Bundle
import android.view.LayoutInflater
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.LiveData
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.BindingViewHolder
import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.MenuItemListener
import org.oxycblt.auxio.ui.MonoAdapter
import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.applySpans
/**
* A Base [Fragment] implementing the base features shared across all list fragments in the home UI.
@ -42,11 +39,7 @@ abstract class HomeListFragment<T : Item> :
MenuItemListener,
FastScrollRecyclerView.PopupProvider,
FastScrollRecyclerView.OnFastScrollListener {
/** The popup provider to use for the fast scroller view. */
abstract val recyclerId: Int
abstract val homeAdapter:
MonoAdapter<T, MenuItemListener, out BindingViewHolder<T, MenuItemListener>>
abstract val homeData: LiveData<List<T>>
abstract fun setupRecycler(recycler: RecyclerView)
protected val homeModel: HomeViewModel by activityViewModels()
protected val playbackModel: PlaybackViewModel by activityViewModels()
@ -55,18 +48,9 @@ abstract class HomeListFragment<T : Item> :
FragmentHomeListBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
binding.homeRecycler.apply {
id = recyclerId
adapter = homeAdapter
applySpans()
}
setupRecycler(binding.homeRecycler)
binding.homeRecycler.popupProvider = this
binding.homeRecycler.onDragListener = this
homeData.observe(viewLifecycleOwner) { list ->
homeAdapter.submitListHard(list.toMutableList())
}
}
override fun onDestroyBinding(binding: FragmentHomeListBinding) {

View file

@ -18,13 +18,14 @@
package org.oxycblt.auxio.home.list
import android.view.View
import androidx.lifecycle.LiveData
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.MenuItemListener
import org.oxycblt.auxio.ui.MonoAdapter
import org.oxycblt.auxio.ui.PrimitiveBackingData
import org.oxycblt.auxio.ui.SongViewHolder
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.newMenu
@ -35,10 +36,16 @@ import org.oxycblt.auxio.ui.sliceArticle
* @author
*/
class SongListFragment : HomeListFragment<Song>() {
override val recyclerId = R.id.home_song_list
override val homeAdapter = SongsAdapter(this)
override val homeData: LiveData<List<Song>>
get() = homeModel.songs
private val homeAdapter = SongsAdapter(this)
override fun setupRecycler(recycler: RecyclerView) {
recycler.apply {
id = R.id.home_song_list
adapter = homeAdapter
}
homeModel.songs.observe(viewLifecycleOwner) { list -> homeAdapter.data.submitList(list) }
}
override fun getPopup(pos: Int): String {
val song = homeModel.songs.value!![pos]
@ -71,7 +78,8 @@ class SongListFragment : HomeListFragment<Song>() {
}
inner class SongsAdapter(listener: MenuItemListener) :
MonoAdapter<Song, MenuItemListener, SongViewHolder>(listener, SongViewHolder.DIFFER) {
MonoAdapter<Song, MenuItemListener, SongViewHolder>(listener) {
override val data = PrimitiveBackingData<Song>(this)
override val creator = SongViewHolder.CREATOR
}
}

View file

@ -30,6 +30,7 @@ import org.oxycblt.auxio.coil.bindAlbumCover
import org.oxycblt.auxio.databinding.ItemQueueSongBinding
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.BindingViewHolder
import org.oxycblt.auxio.ui.HybridBackingData
import org.oxycblt.auxio.ui.MonoAdapter
import org.oxycblt.auxio.ui.SongViewHolder
import org.oxycblt.auxio.util.disableDropShadowCompat
@ -37,9 +38,9 @@ import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.stateList
import org.oxycblt.auxio.util.textSafe
class NewQueueAdapter(listener: QueueItemListener) :
MonoAdapter<Song, QueueItemListener, QueueSongViewHolder>(
listener, QueueSongViewHolder.DIFFER) {
class QueueAdapter(listener: QueueItemListener) :
MonoAdapter<Song, QueueItemListener, QueueSongViewHolder>(listener) {
override val data = HybridBackingData(this, QueueSongViewHolder.DIFFER)
override val creator = QueueSongViewHolder.CREATOR
}

View file

@ -40,7 +40,7 @@ import org.oxycblt.auxio.util.logD
*/
class QueueDragCallback(
private val playbackModel: PlaybackViewModel,
private val queueAdapter: NewQueueAdapter
private val queueAdapter: QueueAdapter
) : ItemTouchHelper.Callback() {
private var shouldLift = true
@ -154,12 +154,12 @@ class QueueDragCallback(
val from = viewHolder.bindingAdapterPosition
val to = target.bindingAdapterPosition
return playbackModel.moveQueueDataItems(from, to) { queueAdapter.moveItems(from, to) }
return playbackModel.moveQueueDataItems(from, to) { queueAdapter.data.moveItems(from, to) }
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
playbackModel.removeQueueDataItem(viewHolder.bindingAdapterPosition) {
queueAdapter.removeItem(viewHolder.bindingAdapterPosition)
queueAdapter.data.removeItem(viewHolder.bindingAdapterPosition)
}
}

View file

@ -36,7 +36,7 @@ import org.oxycblt.auxio.util.requireAttached
*/
class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemListener {
private val playbackModel: PlaybackViewModel by activityViewModels()
private var queueAdapter = NewQueueAdapter(this)
private var queueAdapter = QueueAdapter(this)
private var touchHelper: ItemTouchHelper? = null
private var callback: QueueDragCallback? = null
@ -72,7 +72,7 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
return
}
queueAdapter.submitList(queue.toMutableList())
queueAdapter.data.submitList(queue.toMutableList())
}
private fun requireTouchHelper(): ItemTouchHelper {

View file

@ -24,17 +24,19 @@ import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.AlbumViewHolder
import org.oxycblt.auxio.ui.ArtistViewHolder
import org.oxycblt.auxio.ui.AsyncBackingData
import org.oxycblt.auxio.ui.GenreViewHolder
import org.oxycblt.auxio.ui.Header
import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.ItemDiffCallback
import org.oxycblt.auxio.ui.MenuItemListener
import org.oxycblt.auxio.ui.MultiAdapter
import org.oxycblt.auxio.ui.NewHeaderViewHolder
import org.oxycblt.auxio.ui.SimpleItemCallback
import org.oxycblt.auxio.ui.SongViewHolder
class NeoSearchAdapter(listener: MenuItemListener) :
MultiAdapter<MenuItemListener>(listener, DIFFER) {
class SearchAdapter(listener: MenuItemListener) : MultiAdapter<MenuItemListener>(listener) {
override val data = AsyncBackingData(this, DIFFER)
override fun getCreatorFromItem(item: Item) =
when (item) {
is Song -> SongViewHolder.CREATOR
@ -72,7 +74,7 @@ class NeoSearchAdapter(listener: MenuItemListener) :
companion object {
private val DIFFER =
object : ItemDiffCallback<Item>() {
object : SimpleItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item) =
when {
oldItem is Song && newItem is Song ->

View file

@ -58,7 +58,7 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemLis
private val playbackModel: PlaybackViewModel by activityViewModels()
private val detailModel: DetailViewModel by activityViewModels()
private val searchAdapter = NeoSearchAdapter(this)
private val searchAdapter = SearchAdapter(this)
private var imm: InputMethodManager? = null
private var launchedKeyboard = false
@ -103,7 +103,7 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemLis
binding.searchRecycler.apply {
adapter = searchAdapter
applySpans { pos -> searchAdapter.currentList[pos] is Header }
applySpans { pos -> searchAdapter.data.currentList[pos] is Header }
}
// --- VIEWMODEL SETUP ---
@ -161,7 +161,7 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemLis
val binding = requireBinding()
searchAdapter.submitList(results.toMutableList()) {
searchAdapter.data.submitList(results.toMutableList()) {
// I would make it so that the position is only scrolled back to the top when
// the query actually changes instead of once every re-creation event, but sadly
// that doesn't seem possible.

View file

@ -26,31 +26,212 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
/**
* An adapter enabling both asynchronous list updates and synchronous list updates.
*
* DiffUtil is a joke. The animations are chaotic and gaudy, it does not preserve the scroll
* position of the RecyclerView, it refuses to play along with item movements, and the speed gains
* are minimal. We would rather want to use the slower yet more reliable notifyX in nearly all
* cases, however DiffUtil does have some use in places such as search, so we still want the ability
* to use a differ while also having access to the basic adapter primitives as well. This class
* achieves it through some terrible reflection magic, and is more or less the base for all adapters
* in the app.
*
* TODO: Delegate data management to the internal adapters so that we can isolate the horrible hacks
* to the specific adapters that use need them.
* An adapter for one viewholder tied to one type of data. All functionality is derived from the
* overridden values.
*/
abstract class HybridAdapter<T, VH : RecyclerView.ViewHolder>(
diffCallback: DiffUtil.ItemCallback<T>
) : RecyclerView.Adapter<VH>() {
protected var mCurrentList = mutableListOf<T>()
abstract class MonoAdapter<T, L, VH : BindingViewHolder<T, L>>(private val listener: L) :
RecyclerView.Adapter<VH>() {
/** The data that the adapter will source to bind viewholders. */
abstract val data: BackingData<T>
/** The creator instance that all viewholders will be derived from. */
protected abstract val creator: BindingViewHolder.Creator<VH>
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)
}
}
private typealias AnyCreator = BindingViewHolder.Creator<out RecyclerView.ViewHolder>
/**
* 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".
*/
abstract class MultiAdapter<L>(private val listener: L) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
/** The data that the adapter will source to bind viewholders. */
abstract val data: BackingData<Item>
/**
* Get any creator from the given item. This is used to derive the view type. If there is no
* creator for the given item, return null.
*/
protected abstract fun getCreatorFromItem(item: Item): AnyCreator?
/**
* Get any creator from the given view type. This is used to create the viewholder itself.
* Ideally, one should compare the viewType to every creator's view type and return the one that
* matches. In cases where the view type is unexpected, return null.
*/
protected abstract fun getCreatorFromViewType(viewType: Int): AnyCreator?
/**
* 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)
override fun getItemCount(): Int = data.getItemCount()
override fun getItemViewType(position: Int) =
requireNotNull(getCreatorFromItem(data.getItem(position))) {
"Unable to get view type for item ${data.getItem(position)}"
}
.viewType
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
requireNotNull(getCreatorFromViewType(viewType)) {
"Unable to create viewholder for view type $viewType"
}
.create(parent.context)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
onBind(holder, data.getItem(position), listener)
}
}
/**
* A variation of [RecyclerView.ViewHolder] that enables ViewBinding. This is be used to provide a
* universal surface for binding data to a ViewHolder, and can be used with [MonoAdapter] to get an
* entire adapter implementation for free.
*/
abstract class BindingViewHolder<T, L>(root: View) : RecyclerView.ViewHolder(root) {
abstract fun bind(item: T, listener: L)
init {
// Force the layout to *actually* be the screen width
root.layoutParams =
RecyclerView.LayoutParams(
RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
}
interface Creator<VH : RecyclerView.ViewHolder> {
val viewType: Int
fun create(context: Context): VH
}
}
/** An interface for detecting if an item has been clicked once. */
interface ItemClickListener {
/** Called when an item is clicked once. */
fun onItemClick(item: Item)
}
/** An interface for detecting if an item has had it's menu opened. */
interface MenuItemListener : ItemClickListener {
/** Called when an item desires to open a menu relating to it. */
fun onOpenMenu(item: Item, anchor: View)
}
/**
* The base for all items in Auxio. Any datatype can derive this type and gain some behavior not
* provided for free by the normal adapter implementations, such as certain types of diffing.
*/
abstract class Item {
/** A unique ID for this item. ***THIS IS NOT A MEDIASTORE ID!** */
abstract val id: Long
}
/** A data object used solely for the "Header" UI element. */
data class Header(
override val id: Long,
/** The string resource used for the header. */
@StringRes val string: Int
) : Item()
/**
* Represents data that backs a [MonoAdapter] or [MultiAdapter]. This can be implemented by any
* datatype to customize the organization or editing of data in a way that works best for the
* specific adapter.
*/
abstract class BackingData<T> {
/** Get an item at [position]. */
abstract fun getItem(position: Int): T
/** Get the total length of the backing data. */
abstract fun getItemCount(): Int
}
/**
* A list-backed [BackingData] that is modified using adapter primitives. Useful in cases where
* [AsyncBackingData] is not preferable due to bugs involving diffing.
*/
class PrimitiveBackingData<T>(private val adapter: RecyclerView.Adapter<*>) : BackingData<T>() {
private var mCurrentList = mutableListOf<T>()
/** The current list backing this adapter. */
val currentList: List<T>
get() = mCurrentList
// Probably okay to leak this here since it's just a callback.
@Suppress("LeakingThis") private val differ = AsyncListDiffer(this, diffCallback)
override fun getItem(position: Int): T = mCurrentList[position]
override fun getItemCount(): Int = mCurrentList.size
protected fun getItem(position: Int): T = mCurrentList[position]
/**
* Update the list with a [newList]. This calls [RecyclerView.Adapter.notifyDataSetChanged]
* internally, which is inefficient but also the most reliable update callback.
*/
@Suppress("NotifyDatasetChanged")
fun submitList(newList: List<T>) {
mCurrentList = newList.toMutableList()
adapter.notifyDataSetChanged()
}
/**
* Move an item from [from] to [to]. This calls [RecyclerView.Adapter.notifyItemMoved]
* internally.
*/
fun moveItems(from: Int, to: Int) {
mCurrentList.add(to, mCurrentList.removeAt(from))
adapter.notifyItemMoved(from, to)
}
}
/**
* A list-backed [BackingData] that is modified with [AsyncListDiffer]. This is useful in cases
* where data updates are rapid-fire and unpredictable, and where the benefits of asynchronously
* diffing the adapter outweigh the shortcomings.
*/
class AsyncBackingData<T>(
adapter: RecyclerView.Adapter<*>,
diffCallback: DiffUtil.ItemCallback<T>
) : BackingData<T>() {
private var differ = AsyncListDiffer(adapter, diffCallback)
/** The current list backing this adapter. */
val currentList: List<T>
get() = differ.currentList
override fun getItem(position: Int): T = differ.currentList[position]
override fun getItemCount(): Int = differ.currentList.size
/**
* Submit a list for [AsyncListDiffer] to calculate. Any previous calls of [submitList] will be
* dropped.
*/
fun submitList(newList: List<T>, onDone: () -> Unit = {}) {
differ.submitList(newList, onDone)
}
}
/**
* A list-backed [BackingData] that can be modified with both adapter primitives and
* [AsyncListDiffer]. Never use this class unless absolutely necessary, such as when dealing with
* item dragging. This is mostly because the class is a terrible hacky mess that could easily crash
* the app if you are not careful with it. You have been warned.
*/
class HybridBackingData<T>(
private val adapter: RecyclerView.Adapter<*>,
diffCallback: DiffUtil.ItemCallback<T>
) : BackingData<T>() {
private var mCurrentList = mutableListOf<T>()
val currentList: List<T>
get() = mCurrentList
private val differ = AsyncListDiffer(adapter, diffCallback)
override fun getItem(position: Int): T = mCurrentList[position]
override fun getItemCount(): Int = mCurrentList.size
fun submitList(newData: List<T>, onDone: () -> Unit = {}) {
@ -60,25 +241,25 @@ abstract class HybridAdapter<T, VH : RecyclerView.ViewHolder>(
}
}
@Suppress("NotifyDatasetChanged")
fun submitListHard(newList: List<T>) {
if (newList != mCurrentList) {
mCurrentList = newList.toMutableList()
differ.rewriteListUnsafe(mCurrentList)
notifyDataSetChanged()
}
}
// @Suppress("NotifyDatasetChanged")
// fun submitListHard(newList: List<T>) {
// if (newList != mCurrentList) {
// mCurrentList = newList.toMutableList()
// differ.rewriteListUnsafe(mCurrentList)
// adapter.notifyDataSetChanged()
// }
// }
fun moveItems(from: Int, to: Int) {
mCurrentList.add(to, mCurrentList.removeAt(from))
differ.rewriteListUnsafe(mCurrentList)
notifyItemMoved(from, to)
adapter.notifyItemMoved(from, to)
}
fun removeItem(at: Int) {
mCurrentList.removeAt(at)
differ.rewriteListUnsafe(mCurrentList)
notifyItemRemoved(at)
adapter.notifyItemRemoved(at)
}
/**
@ -108,87 +289,13 @@ abstract class HybridAdapter<T, VH : RecyclerView.ViewHolder>(
}
}
abstract class MonoAdapter<T, L, VH : BindingViewHolder<T, L>>(
private val listener: L,
diffCallback: DiffUtil.ItemCallback<T>
) : HybridAdapter<T, VH>(diffCallback) {
protected abstract val creator: BindingViewHolder.Creator<VH>
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
creator.create(parent.context)
override fun onBindViewHolder(viewHolder: VH, position: Int) {
viewHolder.bind(getItem(position), listener)
}
}
abstract class MultiAdapter<L>(private val listener: L, diffCallback: DiffUtil.ItemCallback<Item>) :
HybridAdapter<Item, RecyclerView.ViewHolder>(diffCallback) {
abstract fun getCreatorFromItem(
item: Item
): BindingViewHolder.Creator<out RecyclerView.ViewHolder>?
abstract fun getCreatorFromViewType(
viewType: Int
): BindingViewHolder.Creator<out RecyclerView.ViewHolder>?
abstract fun onBind(viewHolder: RecyclerView.ViewHolder, item: Item, listener: L)
override fun getItemViewType(position: Int) =
requireNotNull(getCreatorFromItem(getItem(position))) {
"Unable to get view type for item ${getItem(position)}"
}
.viewType
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
requireNotNull(getCreatorFromViewType(viewType)) {
"Unable to create viewholder for view type $viewType"
}
.create(parent.context)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
onBind(holder, getItem(position), listener)
}
}
/** The base for all items in Auxio. */
abstract class Item {
/** A unique ID for this item. ***THIS IS NOT A MEDIASTORE ID!** */
abstract val id: Long
}
/** A data object used solely for the "Header" UI element. */
data class Header(
override val id: Long,
/** The string resource used for the header. */
@StringRes val string: Int
) : Item()
abstract class ItemDiffCallback<T : Item> : DiffUtil.ItemCallback<T>() {
/**
* A base [DiffUtil.ItemCallback] that automatically provides an implementation of
* [areContentsTheSame] any object that is derived from [Item].
*/
abstract class SimpleItemCallback<T : Item> : DiffUtil.ItemCallback<T>() {
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
if (oldItem.javaClass != newItem.javaClass) return false
return oldItem.id == newItem.id
}
}
interface ItemClickListener {
fun onItemClick(item: Item)
}
interface MenuItemListener : ItemClickListener {
fun onOpenMenu(item: Item, anchor: View)
}
abstract class BindingViewHolder<T, L>(root: View) : RecyclerView.ViewHolder(root) {
abstract fun bind(item: T, listener: L)
init {
// Force the layout to *actually* be the screen width
root.layoutParams =
RecyclerView.LayoutParams(
RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
}
interface Creator<VH : RecyclerView.ViewHolder> {
val viewType: Int
fun create(context: Context): VH
}
}

View file

@ -35,6 +35,7 @@ import org.oxycblt.auxio.util.getPluralSafe
import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.textSafe
/** The shared ViewHolder for a [Song]. */
class SongViewHolder private constructor(private val binding: ItemSongBinding) :
BindingViewHolder<Song, MenuItemListener>(binding.root) {
override fun bind(item: Song, listener: MenuItemListener) {
@ -61,7 +62,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
}
val DIFFER =
object : ItemDiffCallback<Song>() {
object : SimpleItemCallback<Song>() {
override fun areItemsTheSame(oldItem: Song, newItem: Song) =
oldItem.resolvedName == newItem.resolvedName &&
oldItem.resolvedArtistName == oldItem.resolvedArtistName
@ -69,7 +70,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
}
}
/** The Shared ViewHolder for a [Album]. Instantiation should be done with [from]. */
/** The Shared ViewHolder for a [Album]. */
class AlbumViewHolder
private constructor(
private val binding: ItemParentBinding,
@ -99,7 +100,7 @@ private constructor(
}
val DIFFER =
object : ItemDiffCallback<Album>() {
object : SimpleItemCallback<Album>() {
override fun areItemsTheSame(oldItem: Album, newItem: Album) =
oldItem.resolvedName == newItem.resolvedName &&
oldItem.resolvedArtistName == newItem.resolvedArtistName
@ -139,7 +140,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
}
val DIFFER =
object : ItemDiffCallback<Artist>() {
object : SimpleItemCallback<Artist>() {
override fun areItemsTheSame(oldItem: Artist, newItem: Artist) =
oldItem.resolvedName == newItem.resolvedName &&
oldItem.albums.size == newItem.albums.size &&
@ -179,7 +180,7 @@ private constructor(
}
val DIFFER =
object : ItemDiffCallback<Genre>() {
object : SimpleItemCallback<Genre>() {
override fun areItemsTheSame(oldItem: Genre, newItem: Genre): Boolean =
oldItem.resolvedName == newItem.resolvedName &&
oldItem.songs.size == newItem.songs.size
@ -187,7 +188,7 @@ private constructor(
}
}
/** The Shared ViewHolder for a [Header]. Instantiation should be done with [from] */
/** The Shared ViewHolder for a [Header]. */
class NewHeaderViewHolder private constructor(private val binding: ItemHeaderBinding) :
BindingViewHolder<Header, Unit>(binding.root) {
@ -206,7 +207,7 @@ class NewHeaderViewHolder private constructor(private val binding: ItemHeaderBin
}
val DIFFER =
object : ItemDiffCallback<Header>() {
object : SimpleItemCallback<Header>() {
override fun areItemsTheSame(oldItem: Header, newItem: Header): Boolean =
oldItem.string == newItem.string
}

View file

@ -66,6 +66,7 @@ class WidgetProvider : AppWidgetProvider() {
}
loadWidgetBitmap(context, song) { bitmap ->
logD(bitmap == null)
val state =
WidgetState(
song,

View file

@ -100,9 +100,11 @@ Attempting to use it as a `MediaStore` ID will result in errors.
- Any field or method beginning with `internal` is off-limits. These fields are meant for use within `MusicLoader` and generally
provide poor UX to the user. The only reason they are public is to make the loading process not have to rely on separate "Raw"
objects.
- Generally, `name` is used when saving music data to storage, while `resolvedName` is used when displaying music data to the user.
- For `Song` instances in particular, prefer `resolvedAlbumName` and `resolvedArtistName` over `album.resolvedName` and `album.artist.resolvedName`
- For `Album` instances in particular, prefer `resolvedArtistName` over `artist.resolvedName`
- Generally, `rawName` is used when doing internal work, such as saving music data, while `resolvedName` is used when displaying music data to the user.
- For `Song` instances in particular, prefer `resolvedAlbumName` and `resolvedArtistName` over `album.resolvedName` and `album.artist.resolvedName`,
as these resolve the name in context of the song.
- For `Album` instances in particular, prefer `resolvedArtistName` over `artist.resolvedName`, which don't actually do anything but add consistency
to the `Song` function
#### Music Access
All music on a system is asynchronously loaded into the shared object `MusicStore`. Because of this, **`MusicStore` may not be available at all times**.