ui: use event everywhere

Replace all prior uses of the StateFlow + finish method pattern with
the new event class.
This commit is contained in:
Alexander Capehart 2023-03-17 15:29:36 -06:00
parent f0d62e8176
commit 1df15a32cd
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
10 changed files with 67 additions and 81 deletions

View file

@ -129,12 +129,12 @@ class MainFragment :
} }
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
collect(navModel.mainNavigationAction, ::handleMainNavigation) collect(navModel.mainNavigationAction.flow, ::handleMainNavigation)
collect(navModel.exploreNavigationItem, ::handleExploreNavigation) collect(navModel.exploreNavigationItem.flow, ::handleExploreNavigation)
collect(navModel.exploreArtistNavigationItem, ::handleArtistNavigationPicker) collect(navModel.exploreArtistNavigationItem.flow, ::handleArtistNavigationPicker)
collectImmediately(playbackModel.song, ::updateSong) collectImmediately(playbackModel.song, ::updateSong)
collect(playbackModel.artistPickerSong, ::handlePlaybackArtistPicker) collect(playbackModel.artistPickerSong.flow, ::handlePlaybackArtistPicker)
collect(playbackModel.genrePickerSong, ::handlePlaybackGenrePicker) collect(playbackModel.genrePickerSong.flow, ::handlePlaybackGenrePicker)
} }
override fun onStart() { override fun onStart() {
@ -273,7 +273,7 @@ class MainFragment :
is MainNavigationAction.Directions -> findNavController().navigate(action.directions) is MainNavigationAction.Directions -> findNavController().navigate(action.directions)
} }
navModel.finishMainNavigation() navModel.mainNavigationAction.consume()
} }
private fun handleExploreNavigation(item: Music?) { private fun handleExploreNavigation(item: Music?) {
@ -287,7 +287,7 @@ class MainFragment :
navModel.mainNavigateTo( navModel.mainNavigateTo(
MainNavigationAction.Directions( MainNavigationAction.Directions(
MainFragmentDirections.actionPickNavigationArtist(item.uid))) MainFragmentDirections.actionPickNavigationArtist(item.uid)))
navModel.finishExploreNavigation() navModel.exploreArtistNavigationItem.consume()
} }
} }
@ -304,7 +304,7 @@ class MainFragment :
navModel.mainNavigateTo( navModel.mainNavigateTo(
MainNavigationAction.Directions( MainNavigationAction.Directions(
MainFragmentDirections.actionPickPlaybackArtist(song.uid))) MainFragmentDirections.actionPickPlaybackArtist(song.uid)))
playbackModel.finishPlaybackArtistPicker() playbackModel.artistPickerSong.consume()
} }
} }
@ -313,7 +313,7 @@ class MainFragment :
navModel.mainNavigateTo( navModel.mainNavigateTo(
MainNavigationAction.Directions( MainNavigationAction.Directions(
MainFragmentDirections.actionPickPlaybackGenre(song.uid))) MainFragmentDirections.actionPickPlaybackGenre(song.uid)))
playbackModel.finishPlaybackGenrePicker() playbackModel.genrePickerSong.consume()
} }
} }

View file

@ -96,7 +96,7 @@ class AlbumDetailFragment :
collectImmediately(detailModel.albumList, ::updateList) collectImmediately(detailModel.albumList, ::updateList)
collectImmediately( collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(navModel.exploreNavigationItem, ::handleNavigation) collect(navModel.exploreNavigationItem.flow, ::handleNavigation)
collectImmediately(selectionModel.selected, ::updateSelection) collectImmediately(selectionModel.selected, ::updateSelection)
} }
@ -205,7 +205,7 @@ class AlbumDetailFragment :
if (unlikelyToBeNull(detailModel.currentAlbum.value) == item.album) { if (unlikelyToBeNull(detailModel.currentAlbum.value) == item.album) {
logD("Navigating to a song in this album") logD("Navigating to a song in this album")
scrollToAlbumSong(item) scrollToAlbumSong(item)
navModel.finishExploreNavigation() navModel.exploreNavigationItem.consume()
} else { } else {
logD("Navigating to another album") logD("Navigating to another album")
findNavController() findNavController()
@ -219,7 +219,7 @@ class AlbumDetailFragment :
if (unlikelyToBeNull(detailModel.currentAlbum.value) == item) { if (unlikelyToBeNull(detailModel.currentAlbum.value) == item) {
logD("Navigating to the top of this album") logD("Navigating to the top of this album")
binding.detailRecycler.scrollToPosition(0) binding.detailRecycler.scrollToPosition(0)
navModel.finishExploreNavigation() navModel.exploreNavigationItem.consume()
} else { } else {
logD("Navigating to another album") logD("Navigating to another album")
findNavController() findNavController()

View file

@ -99,7 +99,7 @@ class ArtistDetailFragment :
collectImmediately(detailModel.artistList, ::updateList) collectImmediately(detailModel.artistList, ::updateList)
collectImmediately( collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(navModel.exploreNavigationItem, ::handleNavigation) collect(navModel.exploreNavigationItem.flow, ::handleNavigation)
collectImmediately(selectionModel.selected, ::updateSelection) collectImmediately(selectionModel.selected, ::updateSelection)
} }
@ -240,7 +240,7 @@ class ArtistDetailFragment :
if (item.uid == detailModel.currentArtist.value?.uid) { if (item.uid == detailModel.currentArtist.value?.uid) {
logD("Navigating to the top of this artist") logD("Navigating to the top of this artist")
binding.detailRecycler.scrollToPosition(0) binding.detailRecycler.scrollToPosition(0)
navModel.finishExploreNavigation() navModel.exploreNavigationItem.consume()
} else { } else {
logD("Navigating to another artist") logD("Navigating to another artist")
findNavController() findNavController()

View file

@ -98,7 +98,7 @@ class GenreDetailFragment :
collectImmediately(detailModel.genreList, ::updateList) collectImmediately(detailModel.genreList, ::updateList)
collectImmediately( collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(navModel.exploreNavigationItem, ::handleNavigation) collect(navModel.exploreNavigationItem.flow, ::handleNavigation)
collectImmediately(selectionModel.selected, ::updateSelection) collectImmediately(selectionModel.selected, ::updateSelection)
} }
@ -229,7 +229,7 @@ class GenreDetailFragment :
.navigate(GenreDetailFragmentDirections.actionShowArtist(item.uid)) .navigate(GenreDetailFragmentDirections.actionShowArtist(item.uid))
} }
is Genre -> { is Genre -> {
navModel.finishExploreNavigation() navModel.exploreNavigationItem.consume()
} }
null -> {} null -> {}
} }

View file

@ -155,11 +155,11 @@ class HomeFragment :
binding.homeFab.setOnClickListener { playbackModel.shuffleAll() } binding.homeFab.setOnClickListener { playbackModel.shuffleAll() }
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
collect(homeModel.shouldRecreate.flow, ::handleRecreate) collect(homeModel.recreateTabs.flow, ::handleRecreate)
collectImmediately(homeModel.currentTabMode, ::updateCurrentTab) collectImmediately(homeModel.currentTabMode, ::updateCurrentTab)
collectImmediately(homeModel.songsList, homeModel.isFastScrolling, ::updateFab) collectImmediately(homeModel.songsList, homeModel.isFastScrolling, ::updateFab)
collectImmediately(musicModel.indexerState, ::updateIndexerState) collectImmediately(musicModel.indexerState, ::updateIndexerState)
collect(navModel.exploreNavigationItem, ::handleNavigation) collect(navModel.exploreNavigationItem.flow, ::handleNavigation)
collectImmediately(selectionModel.selected, ::updateSelection) collectImmediately(selectionModel.selected, ::updateSelection)
} }
@ -337,7 +337,7 @@ class HomeFragment :
binding.homePager.currentItem = 0 binding.homePager.currentItem = 0
// Make sure tabs are set up to also follow the new ViewPager configuration. // Make sure tabs are set up to also follow the new ViewPager configuration.
setupPager(binding) setupPager(binding)
homeModel.shouldRecreate.consume() homeModel.recreateTabs.consume()
} }
private fun updateIndexerState(state: Indexer.State?) { private fun updateIndexerState(state: Indexer.State?) {

View file

@ -109,7 +109,7 @@ constructor(
* flag is true, all tabs (and their respective ViewPager2 fragments) will be re-created from * flag is true, all tabs (and their respective ViewPager2 fragments) will be re-created from
* scratch. * scratch.
*/ */
val shouldRecreate: Event<Unit> val recreateTabs: Event<Unit>
get() = _shouldRecreate get() = _shouldRecreate
private val _isFastScrolling = MutableStateFlow(false) private val _isFastScrolling = MutableStateFlow(false)

View file

@ -31,6 +31,8 @@ import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.playback.persist.PersistenceRepository import org.oxycblt.auxio.playback.persist.PersistenceRepository
import org.oxycblt.auxio.playback.queue.Queue import org.oxycblt.auxio.playback.queue.Queue
import org.oxycblt.auxio.playback.state.* import org.oxycblt.auxio.playback.state.*
import org.oxycblt.auxio.util.Event
import org.oxycblt.auxio.util.MutableEvent
/** /**
* An [ViewModel] that provides a safe UI frontend for the current playback state. * An [ViewModel] that provides a safe UI frontend for the current playback state.
@ -74,22 +76,22 @@ constructor(
val isShuffled: StateFlow<Boolean> val isShuffled: StateFlow<Boolean>
get() = _isShuffled get() = _isShuffled
private val _artistPlaybackPickerSong = MutableStateFlow<Song?>(null) private val _artistPlaybackPickerSong = MutableEvent<Song>()
/** /**
* Flag signaling to open a picker dialog in order to resolve an ambiguous choice when playing a * Flag signaling to open a picker dialog in order to resolve an ambiguous choice when playing a
* [Song] from one of it's [Artist]s. * [Song] from one of it's [Artist]s.
* *
* @see playFromArtist * @see playFromArtist
*/ */
val artistPickerSong: StateFlow<Song?> val artistPickerSong: Event<Song>
get() = _artistPlaybackPickerSong get() = _artistPlaybackPickerSong
private val _genrePlaybackPickerSong = MutableStateFlow<Song?>(null) private val _genrePlaybackPickerSong = MutableEvent<Song>()
/** /**
* Flag signaling to open a picker dialog in order to resolve an ambiguous choice when playing a * Flag signaling to open a picker dialog in order to resolve an ambiguous choice when playing a
* [Song] from one of it's [Genre]s. * [Song] from one of it's [Genre]s.
*/ */
val genrePickerSong: StateFlow<Song?> val genrePickerSong: Event<Song>
get() = _genrePlaybackPickerSong get() = _genrePlaybackPickerSong
/** The current action to show on the playback bar. */ /** The current action to show on the playback bar. */
@ -192,20 +194,10 @@ constructor(
} else if (song.artists.size == 1) { } else if (song.artists.size == 1) {
playImpl(song, song.artists[0]) playImpl(song, song.artists[0])
} else { } else {
_artistPlaybackPickerSong.value = song _artistPlaybackPickerSong.put(song)
} }
} }
/**
* Mark the [Artist] playback choice process as complete. This should occur when the [Artist]
* choice dialog is opened after this flag is detected.
*
* @see playFromArtist
*/
fun finishPlaybackArtistPicker() {
_artistPlaybackPickerSong.value = null
}
/** /**
* PLay a [Song] from one of it's [Genre]s. * PLay a [Song] from one of it's [Genre]s.
* *
@ -219,20 +211,10 @@ constructor(
} else if (song.genres.size == 1) { } else if (song.genres.size == 1) {
playImpl(song, song.genres[0]) playImpl(song, song.genres[0])
} else { } else {
_genrePlaybackPickerSong.value = song _genrePlaybackPickerSong.put(song)
} }
} }
/**
* Mark the [Genre] playback choice process as complete. This should occur when the [Genre]
* choice dialog is opened after this flag is detected.
*
* @see playFromGenre
*/
fun finishPlaybackGenrePicker() {
_genrePlaybackPickerSong.value = null
}
/** /**
* Play an [Album]. * Play an [Album].
* *

View file

@ -115,7 +115,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
collectImmediately(searchModel.searchResults, ::updateSearchResults) collectImmediately(searchModel.searchResults, ::updateSearchResults)
collectImmediately( collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(navModel.exploreNavigationItem, ::handleNavigation) collect(navModel.exploreNavigationItem.flow, ::handleNavigation)
collectImmediately(selectionModel.selected, ::updateSelection) collectImmediately(selectionModel.selected, ::updateSelection)
} }

View file

@ -20,38 +20,38 @@ package org.oxycblt.auxio.ui
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.Event
import org.oxycblt.auxio.util.MutableEvent
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** A [ViewModel] that handles complicated navigation functionality. */ /** A [ViewModel] that handles complicated navigation functionality. */
class NavigationViewModel : ViewModel() { class NavigationViewModel : ViewModel() {
private val _mainNavigationAction = MutableStateFlow<MainNavigationAction?>(null) private val _mainNavigationAction = MutableEvent<MainNavigationAction>()
/** /**
* Flag for navigation within the main navigation graph. Only intended for use by MainFragment. * Flag for navigation within the main navigation graph. Only intended for use by MainFragment.
*/ */
val mainNavigationAction: StateFlow<MainNavigationAction?> val mainNavigationAction: Event<MainNavigationAction>
get() = _mainNavigationAction get() = _mainNavigationAction
private val _exploreNavigationItem = MutableStateFlow<Music?>(null) private val _exploreNavigationItem = MutableEvent<Music>()
/** /**
* Flag for navigation within the explore navigation graph. Observe this to coordinate * Flag for navigation within the explore navigation graph. Observe this to coordinate
* navigation to a specific [Music] item. * navigation to a specific [Music] item.
*/ */
val exploreNavigationItem: StateFlow<Music?> val exploreNavigationItem: Event<Music>
get() = _exploreNavigationItem get() = _exploreNavigationItem
private val _exploreArtistNavigationItem = MutableStateFlow<Music?>(null) private val _exploreArtistNavigationItem = MutableEvent<Music>()
/** /**
* Variation of [exploreNavigationItem] for situations where the choice of parent [Artist] to * Variation of [exploreNavigationItem] for situations where the choice of parent [Artist] to
* navigate to is ambiguous. Only intended for use by MainFragment, as the resolved choice will * navigate to is ambiguous. Only intended for use by MainFragment, as the resolved choice will
* eventually be assigned to [exploreNavigationItem]. * eventually be assigned to [exploreNavigationItem].
*/ */
val exploreArtistNavigationItem: StateFlow<Music?> val exploreArtistNavigationItem: Event<Music>
get() = _exploreArtistNavigationItem get() = _exploreArtistNavigationItem
/** /**
@ -62,21 +62,12 @@ class NavigationViewModel : ViewModel() {
* @param action The [MainNavigationAction] to perform. * @param action The [MainNavigationAction] to perform.
*/ */
fun mainNavigateTo(action: MainNavigationAction) { fun mainNavigateTo(action: MainNavigationAction) {
if (_mainNavigationAction.value != null) { if (_mainNavigationAction.flow.value != null) {
logD("Already navigating, not doing main action") logD("Already navigating, not doing main action")
return return
} }
logD("Navigating with action $action") logD("Navigating with action $action")
_mainNavigationAction.value = action _mainNavigationAction.put(action)
}
/**
* Mark that the navigation process within the main navigation graph (initiated by
* [mainNavigateTo]) was completed.
*/
fun finishMainNavigation() {
logD("Finishing main navigation process")
_mainNavigationAction.value = null
} }
/** /**
@ -85,12 +76,12 @@ class NavigationViewModel : ViewModel() {
* @param music The [Music] to navigate to. * @param music The [Music] to navigate to.
*/ */
fun exploreNavigateTo(music: Music) { fun exploreNavigateTo(music: Music) {
if (_exploreNavigationItem.value != null) { if (_exploreNavigationItem.flow.value != null) {
logD("Already navigating, not doing explore action") logD("Already navigating, not doing explore action")
return return
} }
logD("Navigating to ${music.rawName}") logD("Navigating to ${music.rawName}")
_exploreNavigationItem.value = music _exploreNavigationItem.put(music)
} }
/** /**
@ -114,7 +105,7 @@ class NavigationViewModel : ViewModel() {
} }
private fun exploreNavigateToParentArtistImpl(item: Music, artists: List<Artist>) { private fun exploreNavigateToParentArtistImpl(item: Music, artists: List<Artist>) {
if (_exploreArtistNavigationItem.value != null) { if (_exploreArtistNavigationItem.flow.value != null) {
logD("Already navigating, not doing explore action") logD("Already navigating, not doing explore action")
return return
} }
@ -123,19 +114,9 @@ class NavigationViewModel : ViewModel() {
exploreNavigateTo(artists[0]) exploreNavigateTo(artists[0])
} else { } else {
logD("Navigating to a choice of ${artists.map { it.rawName }}") logD("Navigating to a choice of ${artists.map { it.rawName }}")
_exploreArtistNavigationItem.value = item _exploreArtistNavigationItem.put(item)
} }
} }
/**
* Mark that the navigation process within the explore navigation graph (initiated by
* [exploreNavigateTo]) was completed.
*/
fun finishExploreNavigation() {
logD("Finishing explore navigation process")
_exploreNavigationItem.value = null
_exploreArtistNavigationItem.value = null
}
} }
/** /**

View file

@ -28,17 +28,40 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
/**
* A wrapper around [StateFlow] exposing a one-time consumable event.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface Event<T> { interface Event<T> {
/** The inner [StateFlow] contained by the [Event]. */
val flow: StateFlow<T?> val flow: StateFlow<T?>
/**
* Consume whatever value is currently contained by this instance.
*
* @return A value placed into this instance prior, or null if there isn't any.
*/
fun consume(): T? fun consume(): T?
} }
/**
* A wrapper around [StateFlow] exposing a one-time consumable event that can be modified by it's
* owner.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class MutableEvent<T> : Event<T> { class MutableEvent<T> : Event<T> {
override val flow = MutableStateFlow<T?>(null) override val flow = MutableStateFlow<T?>(null)
override fun consume() = flow.value?.also { flow.value = null }
/**
* Place a new value into this instance, replacing any prior value.
*
* @param v The value to update with.
*/
fun put(v: T) { fun put(v: T) {
flow.value = v flow.value = v
} }
override fun consume() = flow.value?.also { flow.value = null }
} }
/** /**