From 8100f294d71b4778f6a799c452942e9d995f21ff Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Sat, 26 Mar 2022 11:32:54 -0600 Subject: [PATCH] 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. --- .../auxio/coil/SquareFrameTransform.kt | 11 +- .../auxio/detail/AlbumDetailFragment.kt | 8 +- .../auxio/detail/ArtistDetailFragment.kt | 4 +- .../auxio/detail/GenreDetailFragment.kt | 6 +- .../detail/recycler/AlbumDetailAdapter.kt | 8 +- .../detail/recycler/ArtistDetailAdapter.kt | 8 +- .../auxio/detail/recycler/DetailAdapter.kt | 13 +- .../detail/recycler/GenreDetailAdapter.kt | 6 +- .../auxio/home/list/AlbumListFragment.kt | 24 +- .../auxio/home/list/ArtistListFragment.kt | 20 +- .../auxio/home/list/GenreListFragment.kt | 20 +- .../auxio/home/list/HomeListFragment.kt | 22 +- .../auxio/home/list/SongListFragment.kt | 20 +- .../auxio/playback/queue/QueueAdapter.kt | 7 +- .../auxio/playback/queue/QueueDragCallback.kt | 6 +- .../auxio/playback/queue/QueueFragment.kt | 4 +- .../org/oxycblt/auxio/search/SearchAdapter.kt | 10 +- .../oxycblt/auxio/search/SearchFragment.kt | 6 +- .../org/oxycblt/auxio/ui/RecyclerFramework.kt | 323 ++++++++++++------ .../java/org/oxycblt/auxio/ui/ViewHolders.kt | 15 +- .../oxycblt/auxio/widgets/WidgetProvider.kt | 1 + info/ARCHITECTURE.md | 8 +- 22 files changed, 346 insertions(+), 204 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/coil/SquareFrameTransform.kt b/app/src/main/java/org/oxycblt/auxio/coil/SquareFrameTransform.kt index 58606717c..d011ad073 100644 --- a/app/src/main/java/org/oxycblt/auxio/coil/SquareFrameTransform.kt +++ b/app/src/main/java/org/oxycblt/auxio/coil/SquareFrameTransform.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index f27a78dda..d258f06bd 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -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() diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index 314557844..7c96ecbc3 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -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) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index 7f4733773..958c6a7af 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -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) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt index 063e5c875..ed5d81cf1 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt @@ -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() { + object : SimpleItemCallback() { 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() { + object : SimpleItemCallback() { 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() { + object : SimpleItemCallback() { override fun areItemsTheSame(oldItem: Song, newItem: Song) = oldItem.resolvedName == newItem.resolvedName && oldItem.duration == newItem.duration diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt index a23944307..c4c21aa6c 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt @@ -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() { + object : SimpleItemCallback() { 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() { + object : SimpleItemCallback() { 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() { + object : SimpleItemCallback() { override fun areItemsTheSame(oldItem: Song, newItem: Song) = oldItem.resolvedName == newItem.resolvedName && oldItem.resolvedAlbumName == newItem.resolvedAlbumName diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt index 905610d0a..598238632 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt @@ -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( listener: L, diffCallback: DiffUtil.ItemCallback -) : MultiAdapter(listener, diffCallback) { +) : MultiAdapter(listener) { abstract fun onHighlightViewHolder(viewHolder: Highlightable, item: Item) protected inline fun highlightItem( @@ -54,7 +55,7 @@ abstract class DetailAdapter( // 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( } } + @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( companion object { val DIFFER = - object : ItemDiffCallback() { + object : SimpleItemCallback() { 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() { + object : SimpleItemCallback() { override fun areItemsTheSame(oldItem: SortHeader, newItem: SortHeader) = oldItem.string == newItem.string } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt index d9902a5f7..6ca49d509 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt @@ -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() { + object : SimpleItemCallback() { 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() { + object : SimpleItemCallback() { override fun areItemsTheSame(oldItem: Genre, newItem: Genre) = oldItem.resolvedName == newItem.resolvedName && oldItem.songs.size == newItem.songs.size && diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt index e198a3a15..41e0b3d93 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt @@ -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() { - override val recyclerId: Int = R.id.home_album_list - override val homeAdapter = AlbumAdapter(this) - override val homeData: LiveData> - 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() { } class AlbumAdapter(listener: MenuItemListener) : - MonoAdapter(listener, AlbumViewHolder.DIFFER) { - override val creator: BindingViewHolder.Creator - get() = AlbumViewHolder.CREATOR + MonoAdapter(listener) { + override val data = PrimitiveBackingData(this) + override val creator = AlbumViewHolder.CREATOR } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt index b85792049..85ed5b280 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt @@ -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() { - override val recyclerId: Int = R.id.home_artist_list - override val homeAdapter = ArtistAdapter(this) - override val homeData: LiveData> - 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() { } class ArtistAdapter(listener: MenuItemListener) : - MonoAdapter(listener, ArtistViewHolder.DIFFER) { + MonoAdapter(listener) { + override val data = PrimitiveBackingData(this) override val creator = ArtistViewHolder.CREATOR } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt index 5aaf718e6..c7bc658dd 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt @@ -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() { - override val recyclerId = R.id.home_genre_list - override val homeAdapter = GenreAdapter(this) - override val homeData: LiveData> - 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() { } class GenreAdapter(listener: MenuItemListener) : - MonoAdapter(listener, GenreViewHolder.DIFFER) { + MonoAdapter(listener) { + override val data = PrimitiveBackingData(this) override val creator = GenreViewHolder.CREATOR } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt index 0e3851152..f623a542b 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt @@ -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 : MenuItemListener, FastScrollRecyclerView.PopupProvider, FastScrollRecyclerView.OnFastScrollListener { - /** The popup provider to use for the fast scroller view. */ - abstract val recyclerId: Int - abstract val homeAdapter: - MonoAdapter> - abstract val homeData: LiveData> + abstract fun setupRecycler(recycler: RecyclerView) protected val homeModel: HomeViewModel by activityViewModels() protected val playbackModel: PlaybackViewModel by activityViewModels() @@ -55,18 +48,9 @@ abstract class HomeListFragment : 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) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index 78099d53c..0552a4eab 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -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() { - override val recyclerId = R.id.home_song_list - override val homeAdapter = SongsAdapter(this) - override val homeData: LiveData> - 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() { } inner class SongsAdapter(listener: MenuItemListener) : - MonoAdapter(listener, SongViewHolder.DIFFER) { + MonoAdapter(listener) { + override val data = PrimitiveBackingData(this) override val creator = SongViewHolder.CREATOR } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt index 72fa3e8eb..335a22379 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt @@ -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( - listener, QueueSongViewHolder.DIFFER) { +class QueueAdapter(listener: QueueItemListener) : + MonoAdapter(listener) { + override val data = HybridBackingData(this, QueueSongViewHolder.DIFFER) override val creator = QueueSongViewHolder.CREATOR } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt index eed7a70a9..cac02888f 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt @@ -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) } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt index 258401422..e77844125 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt @@ -36,7 +36,7 @@ import org.oxycblt.auxio.util.requireAttached */ class QueueFragment : ViewBindingFragment(), 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(), QueueItemList return } - queueAdapter.submitList(queue.toMutableList()) + queueAdapter.data.submitList(queue.toMutableList()) } private fun requireTouchHelper(): ItemTouchHelper { diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt index 815960429..934963bb6 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt @@ -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(listener, DIFFER) { +class SearchAdapter(listener: MenuItemListener) : MultiAdapter(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() { + object : SimpleItemCallback() { override fun areItemsTheSame(oldItem: Item, newItem: Item) = when { oldItem is Song && newItem is Song -> diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt index dd21cb001..755da5b26 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -58,7 +58,7 @@ class SearchFragment : ViewBindingFragment(), 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(), 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(), 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. diff --git a/app/src/main/java/org/oxycblt/auxio/ui/RecyclerFramework.kt b/app/src/main/java/org/oxycblt/auxio/ui/RecyclerFramework.kt index 9b775b555..d1e06f067 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/RecyclerFramework.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/RecyclerFramework.kt @@ -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( - diffCallback: DiffUtil.ItemCallback -) : RecyclerView.Adapter() { - protected var mCurrentList = mutableListOf() +abstract class MonoAdapter>(private val listener: L) : + RecyclerView.Adapter() { + /** The data that the adapter will source to bind viewholders. */ + abstract val data: BackingData + /** The creator instance that all viewholders will be derived from. */ + protected abstract val creator: BindingViewHolder.Creator + + 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 + +/** + * 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(private val listener: L) : + RecyclerView.Adapter() { + + /** The data that the adapter will source to bind viewholders. */ + abstract val data: BackingData + + /** + * 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(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 { + 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 { + /** 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(private val adapter: RecyclerView.Adapter<*>) : BackingData() { + private var mCurrentList = mutableListOf() + /** The current list backing this adapter. */ val currentList: List 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) { + 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( + adapter: RecyclerView.Adapter<*>, + diffCallback: DiffUtil.ItemCallback +) : BackingData() { + private var differ = AsyncListDiffer(adapter, diffCallback) + /** The current list backing this adapter. */ + val currentList: List + 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, 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( + private val adapter: RecyclerView.Adapter<*>, + diffCallback: DiffUtil.ItemCallback +) : BackingData() { + private var mCurrentList = mutableListOf() + val currentList: List + 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, onDone: () -> Unit = {}) { @@ -60,25 +241,25 @@ abstract class HybridAdapter( } } - @Suppress("NotifyDatasetChanged") - fun submitListHard(newList: List) { - if (newList != mCurrentList) { - mCurrentList = newList.toMutableList() - differ.rewriteListUnsafe(mCurrentList) - notifyDataSetChanged() - } - } + // @Suppress("NotifyDatasetChanged") + // fun submitListHard(newList: List) { + // 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( } } -abstract class MonoAdapter>( - private val listener: L, - diffCallback: DiffUtil.ItemCallback -) : HybridAdapter(diffCallback) { - protected abstract val creator: BindingViewHolder.Creator - - 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(private val listener: L, diffCallback: DiffUtil.ItemCallback) : - HybridAdapter(diffCallback) { - abstract fun getCreatorFromItem( - item: Item - ): BindingViewHolder.Creator? - abstract fun getCreatorFromViewType( - viewType: Int - ): BindingViewHolder.Creator? - 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 : DiffUtil.ItemCallback() { +/** + * A base [DiffUtil.ItemCallback] that automatically provides an implementation of + * [areContentsTheSame] any object that is derived from [Item]. + */ +abstract class SimpleItemCallback : DiffUtil.ItemCallback() { 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(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 { - val viewType: Int - fun create(context: Context): VH - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ViewHolders.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewHolders.kt index 52484ac1d..919af1367 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ViewHolders.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewHolders.kt @@ -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(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() { + object : SimpleItemCallback() { 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() { + object : SimpleItemCallback() { 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() { + object : SimpleItemCallback() { 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() { + object : SimpleItemCallback() { 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(binding.root) { @@ -206,7 +207,7 @@ class NewHeaderViewHolder private constructor(private val binding: ItemHeaderBin } val DIFFER = - object : ItemDiffCallback
() { + object : SimpleItemCallback
() { override fun areItemsTheSame(oldItem: Header, newItem: Header): Boolean = oldItem.string == newItem.string } diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt index 486911fc0..c6d4683cd 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt @@ -66,6 +66,7 @@ class WidgetProvider : AppWidgetProvider() { } loadWidgetBitmap(context, song) { bitmap -> + logD(bitmap == null) val state = WidgetState( song, diff --git a/info/ARCHITECTURE.md b/info/ARCHITECTURE.md index 575803617..4ad420628 100644 --- a/info/ARCHITECTURE.md +++ b/info/ARCHITECTURE.md @@ -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**.