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**.