list: make list instructions generic

Make list instructions generic in preparation for the detail list
update.

Detail views need their own instructions datatype, so this is meant to
allow that to be implemented without issue.
This commit is contained in:
Alexander Capehart 2023-01-16 17:43:59 -07:00
parent 4a7bc4e511
commit d38da9b892
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
20 changed files with 102 additions and 103 deletions

View file

@ -4,13 +4,13 @@
#### What's New
- Added ability to play/shuffle selections
- Settings UI has been visually refreshed
#### What's Improved
- Added ability to edit previously played or currently playing items in the queue
- Added support for date values formatted as "YYYYMMDD"
- Pressing the button will now clear the current selection before navigating back
- Added support for non-standard `ARTISTS` tags
- Reworked music folders dialog to be more visually straightforward
- Play Next and Add To Queue now start playback if there is no queue to add
#### What's Fixed

View file

@ -35,7 +35,7 @@ import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.ui.UISettings
/**
* Auxio: A simple, rational music player for android.
* A simple, rational music player for android.
* @author Alexander Capehart (OxygenCobalt)
*/
class Auxio : Application(), ImageLoaderFactory {

View file

@ -29,7 +29,9 @@ import com.google.android.material.transition.MaterialSharedAxis
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.UpdateInstructions
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music
@ -82,7 +84,7 @@ class AlbumDetailFragment :
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setAlbumUid(args.albumUid)
collectImmediately(detailModel.currentAlbum, ::updateAlbum)
collectImmediately(detailModel.albumList, detailAdapter::diffList)
collectImmediately(detailModel.albumList, ::updateList)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(navModel.exploreNavigationItem, ::handleNavigation)
@ -257,6 +259,10 @@ class AlbumDetailFragment :
}
}
private fun updateList(items: List<Item>) {
detailAdapter.submitList(items, UpdateInstructions.DIFF)
}
private fun updateSelection(selected: List<Music>) {
detailAdapter.setSelected(selected.toSet())
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)

View file

@ -29,7 +29,9 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
import org.oxycblt.auxio.detail.recycler.DetailAdapter
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.UpdateInstructions
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music
@ -85,7 +87,7 @@ class ArtistDetailFragment :
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setArtistUid(args.artistUid)
collectImmediately(detailModel.currentArtist, ::updateItem)
collectImmediately(detailModel.artistList, detailAdapter::diffList)
collectImmediately(detailModel.artistList, ::updateList)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(navModel.exploreNavigationItem, ::handleNavigation)
@ -233,6 +235,10 @@ class ArtistDetailFragment :
}
}
private fun updateList(items: List<Item>) {
detailAdapter.submitList(items, UpdateInstructions.DIFF)
}
private fun updateSelection(selected: List<Music>) {
detailAdapter.setSelected(selected.toSet())
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)

View file

@ -29,7 +29,9 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.DetailAdapter
import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.UpdateInstructions
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
@ -84,7 +86,7 @@ class GenreDetailFragment :
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setGenreUid(args.genreUid)
collectImmediately(detailModel.currentGenre, ::updateItem)
collectImmediately(detailModel.genreList, detailAdapter::diffList)
collectImmediately(detailModel.genreList, ::updateList)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(navModel.exploreNavigationItem, ::handleNavigation)
@ -216,6 +218,10 @@ class GenreDetailFragment :
}
}
private fun updateList(items: List<Item>) {
detailAdapter.submitList(items, UpdateInstructions.DIFF)
}
private fun updateSelection(selected: List<Music>) {
detailAdapter.setSelected(selected.toSet())
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)

View file

@ -28,6 +28,7 @@ import org.oxycblt.auxio.detail.SortHeader
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.SelectableListListener
import org.oxycblt.auxio.list.UpdateInstructions
import org.oxycblt.auxio.list.recycler.*
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.util.context
@ -44,7 +45,8 @@ abstract class DetailAdapter(
private val listener: Listener<*>,
diffCallback: DiffUtil.ItemCallback<Item>
) :
SelectionIndicatorAdapter<Item, RecyclerView.ViewHolder>(ListDiffer.Async(diffCallback)),
SelectionIndicatorAdapter<Item, UpdateInstructions, RecyclerView.ViewHolder>(
ListDiffer.Async(diffCallback)),
AuxioRecyclerView.SpanSizeLookup {
override fun getItemViewType(position: Int) =

View file

@ -149,7 +149,7 @@ class AlbumListFragment :
* @param listener An [SelectableListListener] to bind interactions to.
*/
private class AlbumAdapter(private val listener: SelectableListListener<Album>) :
SelectionIndicatorAdapter<Album, AlbumViewHolder>(
SelectionIndicatorAdapter<Album, UpdateInstructions, AlbumViewHolder>(
ListDiffer.Async(AlbumViewHolder.DIFF_CALLBACK)) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =

View file

@ -128,7 +128,7 @@ class ArtistListFragment :
* @param listener An [SelectableListListener] to bind interactions to.
*/
private class ArtistAdapter(private val listener: SelectableListListener<Artist>) :
SelectionIndicatorAdapter<Artist, ArtistViewHolder>(
SelectionIndicatorAdapter<Artist, UpdateInstructions, ArtistViewHolder>(
ListDiffer.Async(ArtistViewHolder.DIFF_CALLBACK)) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =

View file

@ -127,7 +127,7 @@ class GenreListFragment :
* @param listener An [SelectableListListener] to bind interactions to.
*/
private class GenreAdapter(private val listener: SelectableListListener<Genre>) :
SelectionIndicatorAdapter<Genre, GenreViewHolder>(
SelectionIndicatorAdapter<Genre, UpdateInstructions, GenreViewHolder>(
ListDiffer.Async(GenreViewHolder.DIFF_CALLBACK)) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
GenreViewHolder.from(parent)

View file

@ -160,7 +160,7 @@ class SongListFragment :
* @param listener An [SelectableListListener] to bind interactions to.
*/
private class SongAdapter(private val listener: SelectableListListener<Song>) :
SelectionIndicatorAdapter<Song, SongViewHolder>(
SelectionIndicatorAdapter<Song, UpdateInstructions, SongViewHolder>(
ListDiffer.Async(SongViewHolder.DIFF_CALLBACK)) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =

View file

@ -18,14 +18,14 @@
package org.oxycblt.auxio.list.recycler
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.list.UpdateInstructions
/**
* A [RecyclerView.Adapter] with [ListDiffer] integration.
* @param differFactory The [ListDiffer.Factory] that defines the type of [ListDiffer] to use.
*/
abstract class DiffAdapter<T, VH : RecyclerView.ViewHolder>(differFactory: ListDiffer.Factory<T>) :
RecyclerView.Adapter<VH>() {
abstract class DiffAdapter<T, I, VH : RecyclerView.ViewHolder>(
differFactory: ListDiffer.Factory<T, I>
) : RecyclerView.Adapter<VH>() {
private val differ = differFactory.new(@Suppress("LeakingThis") this)
final override fun getItemCount() = differ.currentList.size
@ -42,30 +42,12 @@ abstract class DiffAdapter<T, VH : RecyclerView.ViewHolder>(differFactory: ListD
fun getItem(at: Int) = differ.currentList[at]
/**
* Dynamically determine how to update the list based on the given [UpdateInstructions].
* Dynamically determine how to update the list based on the given instructions.
* @param newList The new list of [T] items to show.
* @param instructions The [UpdateInstructions] specifying how to update the list.
* @param instructions The instructions specifying how to update the list.
* @param onDone Called when the update process is completed. Defaults to a no-op.
*/
fun submitList(newList: List<T>, instructions: UpdateInstructions) {
when (instructions) {
UpdateInstructions.DIFF -> diffList(newList)
UpdateInstructions.REPLACE -> replaceList(newList)
}
fun submitList(newList: List<T>, instructions: I, onDone: () -> Unit = {}) {
differ.submitList(newList, instructions, onDone)
}
/**
* Update this list using DiffUtil. This can simplify the work of updating the list, but can
* also cause erratic behavior.
* @param newList The new list of [T] items to show.
* @param onDone Callback that will be invoked when the update is completed, allowing means to
* reset the state.
*/
fun diffList(newList: List<T>, onDone: () -> Unit = {}) = differ.diffList(newList, onDone)
/**
* Visually replace the previous list with a new list. This is useful for large diffs that are
* too erratic for [diffList].
* @param newList The new list of [T] items to show.
*/
fun replaceList(newList: List<T>) = differ.replaceList(newList)
}

View file

@ -24,6 +24,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback
import androidx.recyclerview.widget.RecyclerView
import java.lang.reflect.Field
import org.oxycblt.auxio.list.UpdateInstructions
import org.oxycblt.auxio.util.lazyReflectedField
import org.oxycblt.auxio.util.requireIs
@ -31,36 +32,28 @@ import org.oxycblt.auxio.util.requireIs
* List differ wrapper that provides more flexibility regarding the way lists are updated.
* @author Alexander Capehart (OxygenCobalt)
*/
interface ListDiffer<T> {
interface ListDiffer<T, I> {
/** The current list of [T] items. */
val currentList: List<T>
/**
* Update this list using [DiffUtil]. This can simplify the work of updating the list, but can
* also cause erratic behavior.
* Dynamically determine how to update the list based on the given instructions.
* @param newList The new list of [T] items to show.
* @param onDone Callback that will be invoked when the update is completed, allowing means to
* reset the state.
* @param instructions The [UpdateInstructions] specifying how to update the list.
* @param onDone Called when the update process is completed.
*/
fun diffList(newList: List<T>, onDone: () -> Unit = {})
/**
* Visually replace the previous list with a new list. This is useful for large diffs that are
* too erratic for [diffList].
* @param newList The new list of [T] items to show.
*/
fun replaceList(newList: List<T>)
fun submitList(newList: List<T>, instructions: I, onDone: () -> Unit)
/**
* Defines the creation of new [ListDiffer] instances. Allows such [ListDiffer]s to be passed as
* arguments without reliance on a `this` [RecyclerView.Adapter].
*/
abstract class Factory<T> {
abstract class Factory<T, I> {
/**
* Create a new [ListDiffer] bound to the given [RecyclerView.Adapter].
* @param adapter The [RecyclerView.Adapter] to bind to.
*/
abstract fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T>
abstract fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T, I>
}
/**
@ -69,8 +62,9 @@ interface ListDiffer<T> {
* @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the
* internal list.
*/
class Async<T>(private val diffCallback: DiffUtil.ItemCallback<T>) : Factory<T>() {
override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T> =
class Async<T>(private val diffCallback: DiffUtil.ItemCallback<T>) :
Factory<T, UpdateInstructions>() {
override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T, UpdateInstructions> =
RealAsyncListDiffer(AdapterListUpdateCallback(adapter), diffCallback)
}
@ -80,16 +74,33 @@ interface ListDiffer<T> {
* @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the
* internal list.
*/
class Blocking<T>(private val diffCallback: DiffUtil.ItemCallback<T>) : Factory<T>() {
override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T> =
class Blocking<T>(private val diffCallback: DiffUtil.ItemCallback<T>) :
Factory<T, UpdateInstructions>() {
override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T, UpdateInstructions> =
RealBlockingListDiffer(AdapterListUpdateCallback(adapter), diffCallback)
}
}
private abstract class RealListDiffer<T>() : ListDiffer<T, UpdateInstructions> {
override fun submitList(
newList: List<T>,
instructions: UpdateInstructions,
onDone: () -> Unit
) {
when (instructions) {
UpdateInstructions.DIFF -> diffList(newList, onDone)
UpdateInstructions.REPLACE -> replaceList(newList, onDone)
}
}
protected abstract fun diffList(newList: List<T>, onDone: () -> Unit)
protected abstract fun replaceList(newList: List<T>, onDone: () -> Unit)
}
private class RealAsyncListDiffer<T>(
private val updateCallback: ListUpdateCallback,
diffCallback: DiffUtil.ItemCallback<T>
) : ListDiffer<T> {
) : RealListDiffer<T>() {
private val inner =
AsyncListDiffer(updateCallback, AsyncDifferConfig.Builder(diffCallback).build())
@ -100,21 +111,21 @@ private class RealAsyncListDiffer<T>(
inner.submitList(newList, onDone)
}
override fun replaceList(newList: List<T>) {
if (inner.currentList == newList) {
// Nothing to do.
return
override fun replaceList(newList: List<T>, onDone: () -> Unit) {
if (inner.currentList != newList) {
// Do possibly the most idiotic thing possible and mutate the internal differ state
// so we don't have to deal with any disjoint list garbage. This should cancel any prior
// updates and correctly set up the list values while still allowing for the same
// visual animation as the blocking replaceList.
val oldListSize = inner.currentList.size
ASD_MAX_GENERATION_FIELD.set(
inner, requireIs<Int>(ASD_MAX_GENERATION_FIELD.get(inner)) + 1)
ASD_MUTABLE_LIST_FIELD.set(inner, newList.ifEmpty { null })
ASD_READ_ONLY_LIST_FIELD.set(inner, newList)
updateCallback.onRemoved(0, oldListSize)
updateCallback.onInserted(0, newList.size)
}
// Do possibly the most idiotic thing possible and mutate the internal differ state
// so we don't have to deal with any disjoint list garbage. This should cancel any prior
// updates and correctly set up the list values while still allowing for the same
// visual animation as the blocking replaceList.
val oldListSize = inner.currentList.size
ASD_MAX_GENERATION_FIELD.set(inner, requireIs<Int>(ASD_MAX_GENERATION_FIELD.get(inner)) + 1)
ASD_MUTABLE_LIST_FIELD.set(inner, newList.ifEmpty { null })
ASD_READ_ONLY_LIST_FIELD.set(inner, newList)
updateCallback.onRemoved(0, oldListSize)
updateCallback.onInserted(0, newList.size)
onDone()
}
private companion object {
@ -129,7 +140,7 @@ private class RealAsyncListDiffer<T>(
private class RealBlockingListDiffer<T>(
private val updateCallback: ListUpdateCallback,
private val diffCallback: DiffUtil.ItemCallback<T>
) : ListDiffer<T> {
) : RealListDiffer<T>() {
override var currentList = listOf<T>()
override fun diffList(newList: List<T>, onDone: () -> Unit) {
@ -212,13 +223,9 @@ private class RealBlockingListDiffer<T>(
onDone()
}
override fun replaceList(newList: List<T>) {
if (currentList == newList) {
// Nothing to do.
return
override fun replaceList(newList: List<T>, onDone: () -> Unit) {
if (currentList != newList) {
diffList(listOf()) { diffList(newList, onDone) }
}
diffList(listOf())
diffList(newList)
}
}

View file

@ -26,9 +26,9 @@ import org.oxycblt.auxio.util.logD
* @param differFactory The [ListDiffer.Factory] that defines the type of [ListDiffer] to use.
* @author Alexander Capehart (OxygenCobalt)
*/
abstract class PlayingIndicatorAdapter<T, VH : RecyclerView.ViewHolder>(
differFactory: ListDiffer.Factory<T>
) : DiffAdapter<T, VH>(differFactory) {
abstract class PlayingIndicatorAdapter<T, I, VH : RecyclerView.ViewHolder>(
differFactory: ListDiffer.Factory<T, I>
) : DiffAdapter<T, I, VH>(differFactory) {
// There are actually two states for this adapter:
// - The currently playing item, which is usually marked as "selected" and becomes accented.
// - Whether playback is ongoing, which corresponds to whether the item's ImageGroup is

View file

@ -27,9 +27,9 @@ import org.oxycblt.auxio.music.Music
* @param differFactory The [ListDiffer.Factory] that defines the type of [ListDiffer] to use.
* @author Alexander Capehart (OxygenCobalt)
*/
abstract class SelectionIndicatorAdapter<T, VH : RecyclerView.ViewHolder>(
differFactory: ListDiffer.Factory<T>
) : PlayingIndicatorAdapter<T, VH>(differFactory) {
abstract class SelectionIndicatorAdapter<T, I, VH : RecyclerView.ViewHolder>(
differFactory: ListDiffer.Factory<T, I>
) : PlayingIndicatorAdapter<T, I, VH>(differFactory) {
private var selectedItems = setOf<T>()
override fun onBindViewHolder(holder: VH, position: Int, payloads: List<Any>) {

View file

@ -27,6 +27,7 @@ import com.google.android.material.shape.MaterialShapeDrawable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemQueueSongBinding
import org.oxycblt.auxio.list.EditableListListener
import org.oxycblt.auxio.list.UpdateInstructions
import org.oxycblt.auxio.list.recycler.DiffAdapter
import org.oxycblt.auxio.list.recycler.ListDiffer
import org.oxycblt.auxio.list.recycler.PlayingIndicatorAdapter
@ -40,7 +41,8 @@ import org.oxycblt.auxio.util.*
* @author Alexander Capehart (OxygenCobalt)
*/
class QueueAdapter(private val listener: EditableListListener<Song>) :
DiffAdapter<Song, QueueSongViewHolder>(ListDiffer.Blocking(QueueSongViewHolder.DIFF_CALLBACK)) {
DiffAdapter<Song, UpdateInstructions, QueueSongViewHolder>(
ListDiffer.Blocking(QueueSongViewHolder.DIFF_CALLBACK)) {
// Since PlayingIndicator adapter relies on an item value, we cannot use it for this
// adapter, as one item can appear at several points in the UI. Use a similar implementation
// with an index value instead.

View file

@ -67,14 +67,14 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Listener {
override fun onQueueReordered(queue: Queue) {
// Queue changed completely -> Replace queue, update index
instructions = Instructions(UpdateInstructions.REPLACE, null)
instructions = Instructions(UpdateInstructions.REPLACE, queue.index)
_queue.value = queue.resolve()
_index.value = queue.index
}
override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
// Entirely new queue -> Replace queue, update index
instructions = Instructions(UpdateInstructions.REPLACE, null)
instructions = Instructions(UpdateInstructions.REPLACE, queue.index)
_queue.value = queue.resolve()
_index.value = queue.index
}

View file

@ -29,7 +29,8 @@ import org.oxycblt.auxio.music.*
* @author Alexander Capehart (OxygenCobalt)
*/
class SearchAdapter(private val listener: SelectableListListener<Music>) :
SelectionIndicatorAdapter<Item, RecyclerView.ViewHolder>(ListDiffer.Async(DIFF_CALLBACK)),
SelectionIndicatorAdapter<Item, UpdateInstructions, RecyclerView.ViewHolder>(
ListDiffer.Async(DIFF_CALLBACK)),
AuxioRecyclerView.SpanSizeLookup {
override fun getItemViewType(position: Int) =

View file

@ -31,6 +31,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentSearchBinding
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ListFragment
import org.oxycblt.auxio.list.UpdateInstructions
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
@ -153,7 +154,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
// Don't show the RecyclerView (and it's stray overscroll effects) when there
// are no results.
binding.searchRecycler.isInvisible = results.isEmpty()
searchAdapter.diffList(results.toMutableList()) {
searchAdapter.submitList(results.toMutableList(), UpdateInstructions.DIFF) {
// I would make it so that the position is only scrolled back to the top when
// the query actually changes instead of once every re-creation event, but sadly
// that doesn't seem possible.

View file

@ -13,10 +13,4 @@
android:layout_height="wrap_content"
tools:text="Songs" />
<com.google.android.material.divider.MaterialDivider
android:id="@+id/header_divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" />
</LinearLayout>

View file

@ -12,7 +12,6 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_mid_medium"
app:layout_constraintBottom_toTopOf="@id/header_divider"
app:layout_constraintEnd_toStartOf="@+id/header_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
@ -26,14 +25,7 @@
android:layout_marginEnd="@dimen/spacing_mid_medium"
android:contentDescription="@string/lbl_sort"
app:icon="@drawable/ic_sort_24"
app:layout_constraintBottom_toTopOf="@id/header_divider"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.divider.MaterialDivider
android:id="@+id/header_divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>