From 10afae0bfc1199ab8aa3a36a28ca1f740d69af8a Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Thu, 2 Jun 2022 10:13:27 -0600 Subject: [PATCH] detail: fix highlighting issues Fix two major highlighting bugs based around the janky and stupid way I would handle highlighting previously. Previously, I would index the views of a RecyclerView in order to highlight viewholders. In retrospect this was a pretty bad idea, as viewholders could be in a weird limbo state where they are bound, but not accessible. I mean, it's in the name. It's a Recycling View. Fortunately, google actually knew what they were doing and provided a way to mutate viewholders at runtime using notifyItemChanged. And the API actually makes sense! Wow! Migrate all detail adapters to a system that uses notifyItemChanged instead of the terrible pre-existing system. --- app/build.gradle | 1 + .../java/org/oxycblt/auxio/MainActivity.kt | 2 - .../auxio/detail/AlbumDetailFragment.kt | 19 +++++---- .../auxio/detail/ArtistDetailFragment.kt | 31 +++++++------- .../auxio/detail/DetailAppBarLayout.kt | 4 -- .../oxycblt/auxio/detail/DetailViewModel.kt | 2 +- .../auxio/detail/GenreDetailFragment.kt | 14 +++---- .../detail/recycler/AlbumDetailAdapter.kt | 28 ++++--------- .../detail/recycler/ArtistDetailAdapter.kt | 42 +++++++------------ .../auxio/detail/recycler/DetailAdapter.kt | 28 +++++-------- .../detail/recycler/GenreDetailAdapter.kt | 23 +++------- .../auxio/playback/PlaybackViewModel.kt | 5 +-- .../org/oxycblt/auxio/ui/RecyclerFramework.kt | 2 + .../org/oxycblt/auxio/util/FrameworkUtil.kt | 8 ++++ 14 files changed, 86 insertions(+), 123 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index a858f4898..5919bf7ee 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,6 +56,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2" // --- SUPPORT --- diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index db6741d63..92698e42f 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -44,8 +44,6 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat * * TODO: Custom language support * - * TODO: Fix how selection works in the RecyclerViews (doing it poorly right now) - * * TODO: Rework padding ethos * * @author OxygenCobalt 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 4e44750aa..4fe81945a 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -25,7 +25,6 @@ import androidx.core.view.children import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearSmoothScroller -import kotlinx.coroutines.launch import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter @@ -33,6 +32,7 @@ import org.oxycblt.auxio.detail.recycler.SortHeader import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.ui.Header @@ -40,6 +40,7 @@ import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.util.applySpans import org.oxycblt.auxio.util.canScroll +import org.oxycblt.auxio.util.collectWith import org.oxycblt.auxio.util.launch import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW @@ -70,7 +71,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener { launch { detailModel.albumData.collect(detailAdapter.data::submitList) } launch { navModel.exploreNavigationItem.collect(::handleNavigation) } - launch { playbackModel.song.collect(::updateSong) } + launch { playbackModel.song.collectWith(playbackModel.parent, ::updatePlayback) } } override fun onMenuItemClick(item: MenuItem): Boolean { @@ -188,22 +189,24 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener { } } - /** Updates the queue actions when a song is present or not */ - private fun updateSong(song: Song?) { + private fun updatePlayback(song: Song?, parent: MusicParent?) { val binding = requireBinding() for (item in binding.detailToolbar.menu.children) { + // If there is no playback going in, any queue additions will be wiped as soon as + // something is played. Disable these actions when playback is going on so that + // it isn't possible to add anything during that time. if (item.itemId == R.id.action_play_next || item.itemId == R.id.action_queue_add) { item.isEnabled = song != null } } - if (playbackModel.parent.value is Album && - playbackModel.parent.value?.id == unlikelyToBeNull(detailModel.currentAlbum.value).id) { - detailAdapter.highlightSong(song, binding.detailRecycler) + if (parent is Album && parent.id == unlikelyToBeNull(detailModel.currentAlbum.value).id) { + logD("update $song") + detailAdapter.highlightSong(song) } else { // Clear the ViewHolders if the mode isn't ALL_SONGS - detailAdapter.highlightSong(null, binding.detailRecycler) + detailAdapter.highlightSong(null) } } 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 571131eae..20c70acce 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -38,6 +38,7 @@ import org.oxycblt.auxio.ui.Header import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.util.applySpans +import org.oxycblt.auxio.util.collectWith import org.oxycblt.auxio.util.launch import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW @@ -68,8 +69,7 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener { launch { detailModel.artistData.collect(detailAdapter.data::submitList) } launch { navModel.exploreNavigationItem.collect(::handleNavigation) } - launch { playbackModel.song.collect(::updateSong) } - launch { playbackModel.parent.collect(::updateParent) } + launch { playbackModel.song.collectWith(playbackModel.parent, ::updatePlayback) } } override fun onMenuItemClick(item: MenuItem): Boolean = false @@ -135,23 +135,22 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener { } } - private fun updateSong(song: Song?) { + private fun updatePlayback(song: Song?, parent: MusicParent?) { val binding = requireBinding() - if (playbackModel.parent.value is Artist && - playbackModel.parent.value?.id == detailModel.currentArtist.value?.id) { - detailAdapter.highlightSong(song, binding.detailRecycler) - } else { - // Clear the ViewHolders if the mode isn't ALL_SONGS - detailAdapter.highlightSong(null, binding.detailRecycler) - } - } - private fun updateParent(parent: MusicParent?) { - val binding = requireBinding() - if (parent is Album?) { - detailAdapter.highlightAlbum(parent, binding.detailRecycler) + if (parent is Artist && parent.id == unlikelyToBeNull(detailModel.currentArtist.value).id) { + detailAdapter.highlightSong(song) } else { - detailAdapter.highlightAlbum(null, binding.detailRecycler) + // Clear the ViewHolders if the given song is not part of this artist. + detailAdapter.highlightSong(null) + } + + if (parent is Album && + parent.artist.id == unlikelyToBeNull(detailModel.currentArtist.value).id) { + detailAdapter.highlightAlbum(parent) + } else { + // Clear out the album viewholder if the parent is not an album. + detailAdapter.highlightAlbum(null) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt index 80123b514..c39cb1682 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt @@ -85,13 +85,11 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr private fun findRecyclerView(): RecyclerView { val recycler = recycler - if (recycler != null) { return recycler } val newRecycler = (parent as ViewGroup).findViewById(liftOnScrollTargetViewId) - this.recycler = newRecycler return newRecycler } @@ -124,10 +122,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr this.titleAnimator = ValueAnimator.ofFloat(from, to).apply { addUpdateListener { titleView?.alpha = it.animatedValue as Float } - duration = resources.getInteger(R.integer.detail_app_bar_title_anim_duration).toLong() - start() } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 6c754dc97..3e1c5bf4d 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -123,7 +123,7 @@ class DetailViewModel : ViewModel() { // To create a good user experience regarding disc numbers, we intersperse // items that show the disc number throughout the album's songs. In the case - // that the album does not have disc numbers, we omit the header. + // that the album does not have distinct disc numbers, we omit the header. val songs = albumSort.songs(album.songs) val byDisc = songs.groupBy { it.disc ?: 1 } if (byDisc.size > 1) { 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 11e8a87f9..851d71b58 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -22,7 +22,6 @@ import android.view.MenuItem import android.view.View import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs -import kotlinx.coroutines.launch import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.detail.recycler.DetailAdapter @@ -32,12 +31,14 @@ import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.ui.Header import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.util.applySpans +import org.oxycblt.auxio.util.collectWith import org.oxycblt.auxio.util.launch import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull @@ -66,7 +67,7 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener { launch { detailModel.genreData.collect(detailAdapter.data::submitList) } launch { navModel.exploreNavigationItem.collect(::handleNavigation) } - launch { playbackModel.song.collect(::updateSong) } + launch { playbackModel.song.collectWith(playbackModel.parent, ::updatePlayback) } } override fun onMenuItemClick(item: MenuItem): Boolean = false @@ -124,14 +125,13 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener { } } - private fun updateSong(song: Song?) { + private fun updatePlayback(song: Song?, parent: MusicParent?) { val binding = requireBinding() - if (playbackModel.parent.value is Genre && - playbackModel.parent.value?.id == unlikelyToBeNull(detailModel.currentGenre.value).id) { - detailAdapter.highlightSong(song, binding.detailRecycler) + if (parent is Genre && parent.id == unlikelyToBeNull(detailModel.currentGenre.value).id) { + detailAdapter.highlightSong(song) } else { // Clear the ViewHolders if the mode isn't ALL_SONGS - detailAdapter.highlightSong(null, binding.detailRecycler) + detailAdapter.highlightSong(null) } } } 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 b83412f56..abdd3a7ee 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 @@ -44,8 +44,8 @@ import org.oxycblt.auxio.util.textSafe */ class AlbumDetailAdapter(listener: Listener) : DetailAdapter(listener, DIFFER) { - private var highlightedSong: Song? = null - private var highlightedViewHolder: Highlightable? = null + private var currentSong: Song? = null + private var currentHighlightedSongPos: Int? = null override fun getCreatorFromItem(item: Item) = super.getCreatorFromItem(item) @@ -76,25 +76,15 @@ class AlbumDetailAdapter(listener: Listener) : } override fun onHighlightViewHolder(viewHolder: Highlightable, item: Item) { - if (item is Song && item.id == highlightedSong?.id) { - // Reset the last ViewHolder before assigning the new, correct one to be highlighted - highlightedViewHolder?.setHighlighted(false) - highlightedViewHolder = viewHolder - viewHolder.setHighlighted(true) - } else { - viewHolder.setHighlighted(false) - } + viewHolder.setHighlighted(item.id == currentSong?.id) } - /** - * Update the [song] that this adapter should highlight - * @param recycler The recyclerview the highlighting should act on. - */ - fun highlightSong(song: Song?, recycler: RecyclerView) { - if (song == highlightedSong) return - highlightedSong = song - highlightedViewHolder?.setHighlighted(false) - highlightedViewHolder = highlightItem(song, recycler) + /** Update the [song] that this adapter should highlight */ + fun highlightSong(song: Song?) { + if (song == currentSong) return + currentSong = song + currentHighlightedSongPos?.let { notifyItemChanged(it, PAYLOAD_HIGHLIGHT_CHANGED) } + currentHighlightedSongPos = highlightItem(song) } companion object { 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 07faf46cd..fc5ff177a 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 @@ -45,10 +45,10 @@ import org.oxycblt.auxio.util.textSafe class ArtistDetailAdapter(listener: Listener) : DetailAdapter(listener, DIFFER) { private var currentAlbum: Album? = null - private var currentAlbumHolder: Highlightable? = null + private var currentHighlightedAlbumPos: Int? = null private var currentSong: Song? = null - private var currentSongHolder: Highlightable? = null + private var currentHighlightedSongPos: Int? = null override fun getCreatorFromItem(item: Item) = super.getCreatorFromItem(item) @@ -79,40 +79,26 @@ class ArtistDetailAdapter(listener: Listener) : } override fun onHighlightViewHolder(viewHolder: Highlightable, item: Item) { - // If the item corresponds to a currently playing song/album then highlight it - if (item.id == currentAlbum?.id && item is Album) { - currentAlbumHolder?.setHighlighted(false) - currentAlbumHolder = viewHolder - viewHolder.setHighlighted(true) - } else if (item.id == currentSong?.id && item is Song) { - currentSongHolder?.setHighlighted(false) - currentSongHolder = viewHolder - viewHolder.setHighlighted(true) - } else { - viewHolder.setHighlighted(false) - } + viewHolder.setHighlighted( + (item is Album && item.id == currentAlbum?.id) || + (item is Song && item.id == currentSong?.id) + ) } - /** - * Update the current [album] that this adapter should highlight - * @param recycler The recyclerview the highlighting should act on. - */ - fun highlightAlbum(album: Album?, recycler: RecyclerView) { + /** Update the current [album] that this adapter should highlight */ + fun highlightAlbum(album: Album?) { if (album == currentAlbum) return currentAlbum = album - currentAlbumHolder?.setHighlighted(false) - currentAlbumHolder = highlightItem(album, recycler) + currentHighlightedAlbumPos?.let { notifyItemChanged(it, PAYLOAD_HIGHLIGHT_CHANGED) } + currentHighlightedAlbumPos = highlightItem(album) } - /** - * Update the [song] that this adapter should highlight - * @param recycler The recyclerview the highlighting should act on. - */ - fun highlightSong(song: Song?, recycler: RecyclerView) { + /** Update the [song] that this adapter should highlight */ + fun highlightSong(song: Song?) { if (song == currentSong) return currentSong = song - currentSongHolder?.setHighlighted(false) - currentSongHolder = highlightItem(song, recycler) + currentHighlightedSongPos?.let { notifyItemChanged(it, PAYLOAD_HIGHLIGHT_CHANGED) } + currentHighlightedSongPos = highlightItem(song) } companion object { 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 8c4b3b927..915b15b07 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 @@ -35,7 +35,6 @@ import org.oxycblt.auxio.ui.NewHeaderViewHolder import org.oxycblt.auxio.ui.SimpleItemCallback import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater -import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.textSafe abstract class DetailAdapter( @@ -44,10 +43,7 @@ abstract class DetailAdapter( ) : MultiAdapter(listener) { abstract fun onHighlightViewHolder(viewHolder: Highlightable, item: Item) - protected inline fun highlightItem( - newItem: T?, - recycler: RecyclerView - ): Highlightable? { + protected inline fun highlightItem(newItem: T?): Int? { if (newItem == null) { return null } @@ -55,19 +51,8 @@ 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 = 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 - // it does become visible. - val viewHolder = recycler.findViewHolderForAdapterPosition(pos) - - return if (viewHolder is Highlightable) { - viewHolder.setHighlighted(true) - viewHolder - } else { - logW("ViewHolder intended to highlight was not Highlightable") - null - } + notifyItemChanged(pos, PAYLOAD_HIGHLIGHT_CHANGED) + return pos } @Suppress("LeakingThis") override val data = AsyncBackingData(this, diffCallback) @@ -98,6 +83,13 @@ abstract class DetailAdapter( } companion object { + // This payload value serves two purposes: + // 1. It disables animations from notifyItemChanged, which looks bad when highlighting + // values. + // 2. It instructs adapters to avoid re-binding information, and instead simply + // change the highlight state. + val PAYLOAD_HIGHLIGHT_CHANGED = Any() + val DIFFER = object : SimpleItemCallback() { override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean { 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 dc5e7ae4d..8b1b7d093 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 @@ -43,7 +43,7 @@ import org.oxycblt.auxio.util.textSafe class GenreDetailAdapter(listener: Listener) : DetailAdapter(listener, DIFFER) { private var currentSong: Song? = null - private var currentHolder: Highlightable? = null + private var currentHighlightedSongPos: Int? = null override fun getCreatorFromItem(item: Item) = super.getCreatorFromItem(item) @@ -71,26 +71,15 @@ class GenreDetailAdapter(listener: Listener) : } override fun onHighlightViewHolder(viewHolder: Highlightable, item: Item) { - // If the item corresponds to a currently playing song/album then highlight it - if (item.id == currentSong?.id) { - // Reset the last ViewHolder before assigning the new, correct one to be highlighted - currentHolder?.setHighlighted(false) - currentHolder = viewHolder - viewHolder.setHighlighted(true) - } else { - viewHolder.setHighlighted(false) - } + viewHolder.setHighlighted(item.id == currentSong?.id) } - /** - * Update the [song] that this adapter should highlight - * @param recycler The recyclerview the highlighting should act on. - */ - fun highlightSong(song: Song?, recycler: RecyclerView) { + /** Update the [song] that this adapter should highlight */ + fun highlightSong(song: Song?) { if (song == currentSong) return currentSong = song - currentHolder?.setHighlighted(false) - currentHolder = highlightItem(song, recycler) + currentHighlightedSongPos?.let { notifyItemChanged(it, PAYLOAD_HIGHLIGHT_CHANGED) } + currentHighlightedSongPos = highlightItem(song) } companion object { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 47d572fe9..3210196de 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -58,8 +58,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore get() = _song private val _parent = MutableStateFlow(null) /** The current model that is being played from, such as an [Album] or [Artist] */ - val parent: StateFlow - get() = _parent + val parent: StateFlow = _parent private val _isPlaying = MutableStateFlow(false) val isPlaying: StateFlow get() = _isPlaying @@ -298,8 +297,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore } override fun onNewPlayback(index: Int, queue: List, parent: MusicParent?) { - _parent.value = playbackManager.parent _song.value = playbackManager.song + _parent.value = playbackManager.parent _nextUp.value = queue.slice(index + 1 until queue.size) } 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 1c097ea7c..b63c2a44a 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/RecyclerFramework.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/RecyclerFramework.kt @@ -53,6 +53,8 @@ private typealias AnyCreator = BindingViewHolder.Creator(private val listener: L) : RecyclerView.Adapter() { diff --git a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt index 8d4e73051..b3a3f87b3 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt @@ -38,6 +38,9 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import org.oxycblt.auxio.R @@ -166,6 +169,11 @@ fun Fragment.launch( viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(state, block) } } +/** Combines the called flow with the given flow and then collects them both into [block]. */ +suspend fun Flow.collectWith(other: Flow, block: suspend (T1, T2) -> Unit) { + combine(this, other) { a, b -> a to b }.collect { block(it.first, it.second) } +} + /** * Shortcut for querying all items in a database and running [block] with the cursor returned. Will * not run if the cursor is null.