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

View file

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

View file

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

View file

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

View file

@ -155,11 +155,11 @@ class HomeFragment :
binding.homeFab.setOnClickListener { playbackModel.shuffleAll() }
// --- VIEWMODEL SETUP ---
collect(homeModel.shouldRecreate.flow, ::handleRecreate)
collect(homeModel.recreateTabs.flow, ::handleRecreate)
collectImmediately(homeModel.currentTabMode, ::updateCurrentTab)
collectImmediately(homeModel.songsList, homeModel.isFastScrolling, ::updateFab)
collectImmediately(musicModel.indexerState, ::updateIndexerState)
collect(navModel.exploreNavigationItem, ::handleNavigation)
collect(navModel.exploreNavigationItem.flow, ::handleNavigation)
collectImmediately(selectionModel.selected, ::updateSelection)
}
@ -337,7 +337,7 @@ class HomeFragment :
binding.homePager.currentItem = 0
// Make sure tabs are set up to also follow the new ViewPager configuration.
setupPager(binding)
homeModel.shouldRecreate.consume()
homeModel.recreateTabs.consume()
}
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
* scratch.
*/
val shouldRecreate: Event<Unit>
val recreateTabs: Event<Unit>
get() = _shouldRecreate
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.queue.Queue
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.
@ -74,22 +76,22 @@ constructor(
val isShuffled: StateFlow<Boolean>
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
* [Song] from one of it's [Artist]s.
*
* @see playFromArtist
*/
val artistPickerSong: StateFlow<Song?>
val artistPickerSong: Event<Song>
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
* [Song] from one of it's [Genre]s.
*/
val genrePickerSong: StateFlow<Song?>
val genrePickerSong: Event<Song>
get() = _genrePlaybackPickerSong
/** The current action to show on the playback bar. */
@ -192,20 +194,10 @@ constructor(
} else if (song.artists.size == 1) {
playImpl(song, song.artists[0])
} 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.
*
@ -219,20 +211,10 @@ constructor(
} else if (song.genres.size == 1) {
playImpl(song, song.genres[0])
} 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].
*

View file

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

View file

@ -20,38 +20,38 @@ package org.oxycblt.auxio.ui
import androidx.lifecycle.ViewModel
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.Artist
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.Event
import org.oxycblt.auxio.util.MutableEvent
import org.oxycblt.auxio.util.logD
/** A [ViewModel] that handles complicated navigation functionality. */
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.
*/
val mainNavigationAction: StateFlow<MainNavigationAction?>
val mainNavigationAction: Event<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
* navigation to a specific [Music] item.
*/
val exploreNavigationItem: StateFlow<Music?>
val exploreNavigationItem: Event<Music>
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
* navigate to is ambiguous. Only intended for use by MainFragment, as the resolved choice will
* eventually be assigned to [exploreNavigationItem].
*/
val exploreArtistNavigationItem: StateFlow<Music?>
val exploreArtistNavigationItem: Event<Music>
get() = _exploreArtistNavigationItem
/**
@ -62,21 +62,12 @@ class NavigationViewModel : ViewModel() {
* @param action The [MainNavigationAction] to perform.
*/
fun mainNavigateTo(action: MainNavigationAction) {
if (_mainNavigationAction.value != null) {
if (_mainNavigationAction.flow.value != null) {
logD("Already navigating, not doing main action")
return
}
logD("Navigating with action $action")
_mainNavigationAction.value = 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
_mainNavigationAction.put(action)
}
/**
@ -85,12 +76,12 @@ class NavigationViewModel : ViewModel() {
* @param music The [Music] to navigate to.
*/
fun exploreNavigateTo(music: Music) {
if (_exploreNavigationItem.value != null) {
if (_exploreNavigationItem.flow.value != null) {
logD("Already navigating, not doing explore action")
return
}
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>) {
if (_exploreArtistNavigationItem.value != null) {
if (_exploreArtistNavigationItem.flow.value != null) {
logD("Already navigating, not doing explore action")
return
}
@ -123,19 +114,9 @@ class NavigationViewModel : ViewModel() {
exploreNavigateTo(artists[0])
} else {
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.launch
/**
* A wrapper around [StateFlow] exposing a one-time consumable event.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface Event<T> {
/** The inner [StateFlow] contained by the [Event]. */
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?
}
/**
* 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> {
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) {
flow.value = v
}
override fun consume() = flow.value?.also { flow.value = null }
}
/**