all: switch to stateflow

Switch from LiveData to StateFlow.

While LiveData is a pretty good data storage/observer mechanism, it has
a few flaws:
- Values are always nullable in LiveData, even if you make them
non-null.
- LiveData can only be mutated on Dispatchers.Main, which frustrates
possible additions like a more fine-grained music status system.
- LiveData's perks are exclusive to ViewModels, which made coupling
with shared objects somewhat cumbersome.

StateFlow solves all of these by being a native coroutine solution with
proper android bindings. Use it instead.
This commit is contained in:
OxygenCobalt 2022-06-01 11:46:00 -06:00
parent 402a290db7
commit a65d37c421
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
32 changed files with 268 additions and 232 deletions

View file

@ -2,6 +2,9 @@
## dev [v2.3.1, v2.4.0, or v3.0.0] ## dev [v2.3.1, v2.4.0, or v3.0.0]
#### What's Improved
- Loading UI is now more clear and easy-to-use
#### What's Fixed #### What's Fixed
- Fixed crash when seeking to the end of a track as the track changed to a track with a lower duration - Fixed crash when seeking to the end of a track as the track changed to a track with a lower duration

View file

@ -22,7 +22,7 @@ android {
compileSdkVersion 32 compileSdkVersion 32
buildToolsVersion "32.0.0" buildToolsVersion "32.0.0"
// ExoPlayer needs Java 8 to compile. // ExoPlayer, AndroidX, and Material Components all need Java 8 to compile.
compileOptions { compileOptions {
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
@ -76,6 +76,7 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
// Navigation // Navigation
implementation "androidx.navigation:navigation-ui-ktx:$navigation_version" implementation "androidx.navigation:navigation-ui-ktx:$navigation_version"
@ -91,7 +92,7 @@ dependencies {
// --- THIRD PARTY --- // --- THIRD PARTY ---
// Exoplayer // Exoplayer
// WARNING: THE EXOPLAYER VERSION MUST BE KEPT IN LOCK-STEP WITH THE FLAC EXTENSION. // WARNING: THE EXOPLAYER VERSION MUST BE KEPT IN LOCK-STEP WITH THE PRE-BUILD SCRIPT.
// IF NOT, VERY UNFRIENDLY BUILD FAILURES AND CRASHES MAY ENSUE. // IF NOT, VERY UNFRIENDLY BUILD FAILURES AND CRASHES MAY ENSUE.
implementation "com.google.android.exoplayer:exoplayer-core:2.17.1" implementation "com.google.android.exoplayer:exoplayer-core:2.17.1"
implementation fileTree(dir: "libs", include: ["extension-*.aar"]) implementation fileTree(dir: "libs", include: ["extension-*.aar"])
@ -109,7 +110,7 @@ dependencies {
spotless { spotless {
kotlin { kotlin {
target "src/**/*.kt" target "src/**/*.kt"
ktfmt('0.37').dropboxStyle() ktfmt("0.37").dropboxStyle()
licenseHeaderFile("NOTICE") licenseHeaderFile("NOTICE")
} }
} }

View file

@ -22,10 +22,10 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import kotlinx.coroutines.flow.collect
import org.oxycblt.auxio.databinding.FragmentMainBinding import org.oxycblt.auxio.databinding.FragmentMainBinding
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.MusicViewModel
@ -34,6 +34,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.launch
/** /**
* A wrapper around the home fragment that shows the playback fragment and controls the more * A wrapper around the home fragment that shows the playback fragment and controls the more
@ -50,12 +51,6 @@ class MainFragment : ViewBindingFragment<FragmentMainBinding>() {
override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) {
// --- UI SETUP --- // --- UI SETUP ---
// Build the permission launcher here as you can only do it in onCreateView/onCreate
val permLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
musicModel.reloadMusic(requireContext())
}
requireActivity() requireActivity()
.onBackPressedDispatcher.addCallback( .onBackPressedDispatcher.addCallback(
viewLifecycleOwner, DynamicBackPressedCallback().also { callback = it }) viewLifecycleOwner, DynamicBackPressedCallback().also { callback = it })
@ -81,10 +76,9 @@ class MainFragment : ViewBindingFragment<FragmentMainBinding>() {
// TODO: Move this to a service [automatic rescanning] // TODO: Move this to a service [automatic rescanning]
musicModel.loadMusic(requireContext()) musicModel.loadMusic(requireContext())
navModel.mainNavigationAction.observe(viewLifecycleOwner, ::handleMainNavigation) launch { navModel.mainNavigationAction.collect(::handleMainNavigation) }
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleExploreNavigation) launch { navModel.exploreNavigationItem.collect(::handleExploreNavigation) }
launch { playbackModel.song.collect(::updateSong) }
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
} }
override fun onResume() { override fun onResume() {

View file

@ -25,6 +25,7 @@ import androidx.core.view.children
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearSmoothScroller import androidx.recyclerview.widget.LinearSmoothScroller
import kotlinx.coroutines.launch
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter
@ -39,6 +40,7 @@ import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.util.applySpans import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.canScroll import org.oxycblt.auxio.util.canScroll
import org.oxycblt.auxio.util.launch
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast
@ -66,9 +68,9 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
// -- VIEWMODEL SETUP --- // -- VIEWMODEL SETUP ---
detailModel.albumData.observe(viewLifecycleOwner, detailAdapter.data::submitList) launch { detailModel.albumData.collect(detailAdapter.data::submitList) }
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation) launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
playbackModel.song.observe(viewLifecycleOwner, ::updateSong) launch { playbackModel.song.collect(::updateSong) }
} }
override fun onMenuItemClick(item: MenuItem): Boolean { override fun onMenuItemClick(item: MenuItem): Boolean {

View file

@ -22,6 +22,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import kotlinx.coroutines.launch
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
@ -37,6 +38,7 @@ import org.oxycblt.auxio.ui.Header
import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.util.applySpans import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.launch
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
@ -64,10 +66,10 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
detailModel.artistData.observe(viewLifecycleOwner, detailAdapter.data::submitList) launch { detailModel.artistData.collect(detailAdapter.data::submitList) }
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation) launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
playbackModel.song.observe(viewLifecycleOwner, ::updateSong) launch { playbackModel.song.collect(::updateSong) }
playbackModel.parent.observe(viewLifecycleOwner, ::updateParent) launch { playbackModel.parent.collect(::updateParent) }
} }
override fun onMenuItemClick(item: MenuItem): Boolean = false override fun onMenuItemClick(item: MenuItem): Boolean = false

View file

@ -17,9 +17,9 @@
package org.oxycblt.auxio.detail package org.oxycblt.auxio.detail
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.detail.recycler.DiscHeader import org.oxycblt.auxio.detail.recycler.DiscHeader
import org.oxycblt.auxio.detail.recycler.SortHeader import org.oxycblt.auxio.detail.recycler.SortHeader
@ -45,12 +45,12 @@ class DetailViewModel : ViewModel() {
private val musicStore = MusicStore.getInstance() private val musicStore = MusicStore.getInstance()
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
private val _currentAlbum = MutableLiveData<Album?>() private val _currentAlbum = MutableStateFlow<Album?>(null)
val currentAlbum: LiveData<Album?> val currentAlbum: StateFlow<Album?>
get() = _currentAlbum get() = _currentAlbum
private val _albumData = MutableLiveData(listOf<Item>()) private val _albumData = MutableStateFlow(listOf<Item>())
val albumData: LiveData<List<Item>> val albumData: StateFlow<List<Item>>
get() = _albumData get() = _albumData
var albumSort: Sort var albumSort: Sort
@ -60,12 +60,12 @@ class DetailViewModel : ViewModel() {
currentAlbum.value?.let(::refreshAlbumData) currentAlbum.value?.let(::refreshAlbumData)
} }
private val _currentArtist = MutableLiveData<Artist?>() private val _currentArtist = MutableStateFlow<Artist?>(null)
val currentArtist: LiveData<Artist?> val currentArtist: StateFlow<Artist?>
get() = _currentArtist get() = _currentArtist
private val _artistData = MutableLiveData(listOf<Item>()) private val _artistData = MutableStateFlow(listOf<Item>())
val artistData: LiveData<List<Item>> = _artistData val artistData: StateFlow<List<Item>> = _artistData
var artistSort: Sort var artistSort: Sort
get() = settingsManager.detailArtistSort get() = settingsManager.detailArtistSort
@ -74,12 +74,12 @@ class DetailViewModel : ViewModel() {
currentArtist.value?.let(::refreshArtistData) currentArtist.value?.let(::refreshArtistData)
} }
private val _currentGenre = MutableLiveData<Genre?>() private val _currentGenre = MutableStateFlow<Genre?>(null)
val currentGenre: LiveData<Genre?> val currentGenre: StateFlow<Genre?>
get() = _currentGenre get() = _currentGenre
private val _genreData = MutableLiveData(listOf<Item>()) private val _genreData = MutableStateFlow(listOf<Item>())
val genreData: LiveData<List<Item>> = _genreData val genreData: StateFlow<List<Item>> = _genreData
var genreSort: Sort var genreSort: Sort
get() = settingsManager.detailGenreSort get() = settingsManager.detailGenreSort

View file

@ -22,6 +22,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import kotlinx.coroutines.launch
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.DetailAdapter import org.oxycblt.auxio.detail.recycler.DetailAdapter
@ -37,6 +38,7 @@ import org.oxycblt.auxio.ui.Header
import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.util.applySpans import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.launch
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
@ -62,9 +64,9 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
detailModel.genreData.observe(viewLifecycleOwner, detailAdapter.data::submitList) launch { detailModel.genreData.collect(detailAdapter.data::submitList) }
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation) launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
playbackModel.song.observe(viewLifecycleOwner, ::updateSong) launch { playbackModel.song.collect(::updateSong) }
} }
override fun onMenuItemClick(item: MenuItem): Boolean = false override fun onMenuItemClick(item: MenuItem): Boolean = false

View file

@ -35,6 +35,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.launch
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeBinding import org.oxycblt.auxio.databinding.FragmentHomeBinding
import org.oxycblt.auxio.home.list.AlbumListFragment import org.oxycblt.auxio.home.list.AlbumListFragment
@ -53,6 +54,7 @@ import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.launch
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logTraceOrThrow import org.oxycblt.auxio.util.logTraceOrThrow
@ -71,13 +73,14 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
private val homeModel: HomeViewModel by activityViewModels() private val homeModel: HomeViewModel by activityViewModels()
private val musicModel: MusicViewModel by activityViewModels() private val musicModel: MusicViewModel by activityViewModels()
private var storagePermissionLauncher: ActivityResultLauncher<String>? = null
private var sortItem: MenuItem? = null
override fun onCreateBinding(inflater: LayoutInflater) = FragmentHomeBinding.inflate(inflater) override fun onCreateBinding(inflater: LayoutInflater) = FragmentHomeBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) {
val sortItem: MenuItem
// Build the permission launcher here as you can only do it in onCreateView/onCreate // Build the permission launcher here as you can only do it in onCreateView/onCreate
val permLauncher = storagePermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { registerForActivityResult(ActivityResultContracts.RequestPermission()) {
musicModel.reloadMusic(requireContext()) musicModel.reloadMusic(requireContext())
} }
@ -89,8 +92,8 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
updateTabConfiguration() updateTabConfiguration()
binding.homeLoadingContainer.setOnApplyWindowInsetsListener { v, insets -> binding.homeLoadingContainer.setOnApplyWindowInsetsListener { view, insets ->
v.updatePadding(bottom = insets.systemBarInsetsCompat.bottom) view.updatePadding(bottom = insets.systemBarInsetsCompat.bottom)
insets insets
} }
@ -118,20 +121,18 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
homeModel.isFastScrolling.observe(viewLifecycleOwner, ::updateFastScrolling) launch { homeModel.isFastScrolling.collect(::updateFastScrolling) }
homeModel.currentTab.observe(viewLifecycleOwner) { tab -> updateCurrentTab(sortItem, tab) } launch { homeModel.currentTab.collect(::updateCurrentTab) }
homeModel.recreateTabs.observe(viewLifecycleOwner, ::handleRecreateTabs) launch { homeModel.recreateTabs.collect(::handleRecreateTabs) }
launch { musicModel.response.collect(::handleLoaderResponse) }
musicModel.loaderResponse.observe(viewLifecycleOwner) { response -> launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
handleLoaderResponse(response, permLauncher)
}
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation)
} }
override fun onDestroyBinding(binding: FragmentHomeBinding) { override fun onDestroyBinding(binding: FragmentHomeBinding) {
super.onDestroyBinding(binding) super.onDestroyBinding(binding)
binding.homeToolbar.setOnMenuItemClickListener(null) binding.homeToolbar.setOnMenuItemClickListener(null)
storagePermissionLauncher = null
sortItem = null
} }
override fun onMenuItemClick(item: MenuItem): Boolean { override fun onMenuItemClick(item: MenuItem): Boolean {
@ -178,7 +179,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
// Make sure an update here doesn't mess up the FAB state when it comes to the // Make sure an update here doesn't mess up the FAB state when it comes to the
// loader response. // loader response.
if (musicModel.loaderResponse.value !is MusicStore.Response.Ok) { if (musicModel.response.value !is MusicStore.Response.Ok) {
return return
} }
@ -189,21 +190,21 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
} }
} }
private fun updateCurrentTab(sortItem: MenuItem, tab: DisplayMode) { private fun updateCurrentTab(tab: DisplayMode) {
// Make sure that we update the scrolling view and allowed menu items whenever // Make sure that we update the scrolling view and allowed menu items whenever
// the tab changes. // the tab changes.
val binding = requireBinding() val binding = requireBinding()
when (tab) { when (tab) {
DisplayMode.SHOW_SONGS -> { DisplayMode.SHOW_SONGS -> {
updateSortMenu(sortItem, tab) { id -> id != R.id.option_sort_count } updateSortMenu(tab) { id -> id != R.id.option_sort_count }
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_song_list binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_song_list
} }
DisplayMode.SHOW_ALBUMS -> { DisplayMode.SHOW_ALBUMS -> {
updateSortMenu(sortItem, tab) { id -> id != R.id.option_sort_album } updateSortMenu(tab) { id -> id != R.id.option_sort_album }
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_album_list binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_album_list
} }
DisplayMode.SHOW_ARTISTS -> { DisplayMode.SHOW_ARTISTS -> {
updateSortMenu(sortItem, tab) { id -> updateSortMenu(tab) { id ->
id == R.id.option_sort_asc || id == R.id.option_sort_asc ||
id == R.id.option_sort_name || id == R.id.option_sort_name ||
id == R.id.option_sort_count || id == R.id.option_sort_count ||
@ -212,7 +213,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_artist_list binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_artist_list
} }
DisplayMode.SHOW_GENRES -> { DisplayMode.SHOW_GENRES -> {
updateSortMenu(sortItem, tab) { id -> updateSortMenu(tab) { id ->
id == R.id.option_sort_asc || id == R.id.option_sort_asc ||
id == R.id.option_sort_name || id == R.id.option_sort_name ||
id == R.id.option_sort_count || id == R.id.option_sort_count ||
@ -223,14 +224,13 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
} }
} }
private fun updateSortMenu( private fun updateSortMenu(displayMode: DisplayMode, isVisible: (Int) -> Boolean) {
item: MenuItem, val sortItem =
displayMode: DisplayMode, requireNotNull(sortItem) { "Cannot update sort menu when view does not exist" }
isVisible: (Int) -> Boolean
) {
val toHighlight = homeModel.getSortForDisplay(displayMode) val toHighlight = homeModel.getSortForDisplay(displayMode)
for (option in item.subMenu) { for (option in sortItem.subMenu) {
if (option.itemId == toHighlight.itemId) { if (option.itemId == toHighlight.itemId) {
option.isChecked = true option.isChecked = true
} }
@ -257,10 +257,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
} }
} }
private fun handleLoaderResponse( private fun handleLoaderResponse(response: MusicStore.Response?) {
response: MusicStore.Response?,
permLauncher: ActivityResultLauncher<String>
) {
val binding = requireBinding() val binding = requireBinding()
if (response is MusicStore.Response.Ok) { if (response is MusicStore.Response.Ok) {
@ -296,13 +293,18 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
} }
} }
is MusicStore.Response.NoPerms -> { is MusicStore.Response.NoPerms -> {
val launcher =
requireNotNull(storagePermissionLauncher) {
"Cannot access permission launcher while in non-view state"
}
binding.homeLoadingProgress.visibility = View.INVISIBLE binding.homeLoadingProgress.visibility = View.INVISIBLE
binding.homeLoadingStatus.textSafe = getString(R.string.err_no_perms) binding.homeLoadingStatus.textSafe = getString(R.string.err_no_perms)
binding.homeLoadingAction.apply { binding.homeLoadingAction.apply {
visibility = View.VISIBLE visibility = View.VISIBLE
text = getString(R.string.lbl_grant) text = getString(R.string.lbl_grant)
setOnClickListener { setOnClickListener {
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) launcher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
} }
} }
} }

View file

@ -17,9 +17,9 @@
package org.oxycblt.auxio.home package org.oxycblt.auxio.home
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.home.tabs.Tab import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
@ -40,20 +40,20 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
private val musicStore = MusicStore.getInstance() private val musicStore = MusicStore.getInstance()
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
private val _songs = MutableLiveData(listOf<Song>()) private val _songs = MutableStateFlow(listOf<Song>())
val songs: LiveData<List<Song>> val songs: StateFlow<List<Song>>
get() = _songs get() = _songs
private val _albums = MutableLiveData(listOf<Album>()) private val _albums = MutableStateFlow(listOf<Album>())
val albums: LiveData<List<Album>> val albums: StateFlow<List<Album>>
get() = _albums get() = _albums
private val _artists = MutableLiveData(listOf<Artist>()) private val _artists = MutableStateFlow(listOf<Artist>())
val artists: LiveData<List<Artist>> val artists: MutableStateFlow<List<Artist>>
get() = _artists get() = _artists
private val _genres = MutableLiveData(listOf<Genre>()) private val _genres = MutableStateFlow(listOf<Genre>())
val genres: LiveData<List<Genre>> val genres: StateFlow<List<Genre>>
get() = _genres get() = _genres
var tabs: List<DisplayMode> = visibleTabs var tabs: List<DisplayMode> = visibleTabs
@ -63,18 +63,18 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
private val visibleTabs: List<DisplayMode> private val visibleTabs: List<DisplayMode>
get() = settingsManager.libTabs.filterIsInstance<Tab.Visible>().map { it.mode } get() = settingsManager.libTabs.filterIsInstance<Tab.Visible>().map { it.mode }
private val _currentTab = MutableLiveData(tabs[0]) private val _currentTab = MutableStateFlow(tabs[0])
val currentTab: LiveData<DisplayMode> = _currentTab val currentTab: StateFlow<DisplayMode> = _currentTab
/** /**
* Marker to recreate all library tabs, usually initiated by a settings change. When this flag * Marker to recreate all library tabs, usually initiated by a settings change. When this flag
* is set, all tabs (and their respective viewpager fragments) will be recreated from scratch. * is set, all tabs (and their respective viewpager fragments) will be recreated from scratch.
*/ */
private val _shouldRecreateTabs = MutableLiveData(false) private val _shouldRecreateTabs = MutableStateFlow(false)
val recreateTabs: LiveData<Boolean> = _shouldRecreateTabs val recreateTabs: StateFlow<Boolean> = _shouldRecreateTabs
private val _isFastScrolling = MutableLiveData(false) private val _isFastScrolling = MutableStateFlow(false)
val isFastScrolling: LiveData<Boolean> = _isFastScrolling val isFastScrolling: StateFlow<Boolean> = _isFastScrolling
init { init {
musicStore.addCallback(this) musicStore.addCallback(this)
@ -121,7 +121,6 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
settingsManager.libGenreSort = sort settingsManager.libGenreSort = sort
_genres.value = sort.genres(unlikelyToBeNull(_genres.value)) _genres.value = sort.genres(unlikelyToBeNull(_genres.value))
} }
else -> {}
} }
} }

View file

@ -17,9 +17,11 @@
package org.oxycblt.auxio.home.list package org.oxycblt.auxio.home.list
import android.os.Bundle
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.launch
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.ui.AlbumViewHolder import org.oxycblt.auxio.ui.AlbumViewHolder
@ -31,6 +33,7 @@ import org.oxycblt.auxio.ui.PrimitiveBackingData
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.util.formatDuration import org.oxycblt.auxio.util.formatDuration
import org.oxycblt.auxio.util.launch
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
@ -40,13 +43,15 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
class AlbumListFragment : HomeListFragment<Album>() { class AlbumListFragment : HomeListFragment<Album>() {
private val homeAdapter = AlbumAdapter(this) private val homeAdapter = AlbumAdapter(this)
override fun setupRecycler(recycler: RecyclerView) { override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
recycler.apply { super.onBindingCreated(binding, savedInstanceState)
binding.homeRecycler.apply {
id = R.id.home_album_list id = R.id.home_album_list
adapter = homeAdapter adapter = homeAdapter
} }
homeModel.albums.observe(viewLifecycleOwner) { list -> homeAdapter.data.submitList(list) } launch { homeModel.albums.collect(homeAdapter.data::submitList) }
} }
override fun getPopup(pos: Int): String? { override fun getPopup(pos: Int): String? {

View file

@ -17,9 +17,11 @@
package org.oxycblt.auxio.home.list package org.oxycblt.auxio.home.list
import android.os.Bundle
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.launch
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
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.ui.ArtistViewHolder import org.oxycblt.auxio.ui.ArtistViewHolder
@ -31,6 +33,7 @@ import org.oxycblt.auxio.ui.PrimitiveBackingData
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.util.formatDuration import org.oxycblt.auxio.util.formatDuration
import org.oxycblt.auxio.util.launch
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
@ -40,13 +43,15 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
class ArtistListFragment : HomeListFragment<Artist>() { class ArtistListFragment : HomeListFragment<Artist>() {
private val homeAdapter = ArtistAdapter(this) private val homeAdapter = ArtistAdapter(this)
override fun setupRecycler(recycler: RecyclerView) { override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
recycler.apply { super.onBindingCreated(binding, savedInstanceState)
binding.homeRecycler.apply {
id = R.id.home_artist_list id = R.id.home_artist_list
adapter = homeAdapter adapter = homeAdapter
} }
homeModel.artists.observe(viewLifecycleOwner) { list -> homeAdapter.data.submitList(list) } launch { homeModel.artists.collect(homeAdapter.data::submitList) }
} }
override fun getPopup(pos: Int): String? { override fun getPopup(pos: Int): String? {

View file

@ -17,9 +17,11 @@
package org.oxycblt.auxio.home.list package org.oxycblt.auxio.home.list
import android.os.Bundle
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.launch
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.DisplayMode
@ -31,6 +33,7 @@ import org.oxycblt.auxio.ui.PrimitiveBackingData
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.util.formatDuration import org.oxycblt.auxio.util.formatDuration
import org.oxycblt.auxio.util.launch
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
@ -40,13 +43,15 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
class GenreListFragment : HomeListFragment<Genre>() { class GenreListFragment : HomeListFragment<Genre>() {
private val homeAdapter = GenreAdapter(this) private val homeAdapter = GenreAdapter(this)
override fun setupRecycler(recycler: RecyclerView) { override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
recycler.apply { super.onBindingCreated(binding, savedInstanceState)
binding.homeRecycler.apply {
id = R.id.home_genre_list id = R.id.home_genre_list
adapter = homeAdapter adapter = homeAdapter
} }
homeModel.genres.observe(viewLifecycleOwner) { list -> homeAdapter.data.submitList(list) } launch { homeModel.genres.collect(homeAdapter.data::submitList) }
} }
override fun getPopup(pos: Int): String? { override fun getPopup(pos: Int): String? {

View file

@ -21,7 +21,6 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.home.HomeViewModel import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
@ -40,8 +39,6 @@ abstract class HomeListFragment<T : Item> :
MenuItemListener, MenuItemListener,
FastScrollRecyclerView.PopupProvider, FastScrollRecyclerView.PopupProvider,
FastScrollRecyclerView.OnFastScrollListener { FastScrollRecyclerView.OnFastScrollListener {
abstract fun setupRecycler(recycler: RecyclerView)
protected val playbackModel: PlaybackViewModel by activityViewModels() protected val playbackModel: PlaybackViewModel by activityViewModels()
protected val navModel: NavigationViewModel by activityViewModels() protected val navModel: NavigationViewModel by activityViewModels()
protected val homeModel: HomeViewModel by activityViewModels() protected val homeModel: HomeViewModel by activityViewModels()
@ -50,7 +47,6 @@ abstract class HomeListFragment<T : Item> :
FragmentHomeListBinding.inflate(inflater) FragmentHomeListBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
setupRecycler(binding.homeRecycler)
binding.homeRecycler.popupProvider = this binding.homeRecycler.popupProvider = this
binding.homeRecycler.listener = this binding.homeRecycler.listener = this
} }

View file

@ -17,9 +17,11 @@
package org.oxycblt.auxio.home.list package org.oxycblt.auxio.home.list
import android.os.Bundle
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.launch
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.Item
@ -30,6 +32,7 @@ import org.oxycblt.auxio.ui.SongViewHolder
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.util.formatDuration import org.oxycblt.auxio.util.formatDuration
import org.oxycblt.auxio.util.launch
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
@ -39,20 +42,22 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
class SongListFragment : HomeListFragment<Song>() { class SongListFragment : HomeListFragment<Song>() {
private val homeAdapter = SongsAdapter(this) private val homeAdapter = SongsAdapter(this)
override fun setupRecycler(recycler: RecyclerView) { override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
recycler.apply { super.onBindingCreated(binding, savedInstanceState)
binding.homeRecycler.apply {
id = R.id.home_song_list id = R.id.home_song_list
adapter = homeAdapter adapter = homeAdapter
} }
homeModel.songs.observe(viewLifecycleOwner) { list -> homeAdapter.data.submitList(list) } launch { homeModel.songs.collect(homeAdapter.data::submitList) }
} }
override fun getPopup(pos: Int): String? { override fun getPopup(pos: Int): String? {
val song = unlikelyToBeNull(homeModel.songs.value)[pos] val song = unlikelyToBeNull(homeModel.songs.value)[pos]
// Change how we display the popup depending on the mode. // Change how we display the popup depending on the mode.
// We don't use the more correct resolve(Model)Name here, as sorts are largely // Note: We don't use the more correct individual artist name here, as sorts are largely
// based off the names of the parent objects and not the child objects. // based off the names of the parent objects and not the child objects.
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) { return when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) {
// Name -> Use name // Name -> Use name

View file

@ -137,6 +137,10 @@ data class Song(
return result return result
} }
/** Internal field. Do not use. */
val _genreGroupingId: Long
get() = (_genreName ?: MediaStore.UNKNOWN_STRING).hashCode().toLong()
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val _artistGroupingName: String? val _artistGroupingName: String?
get() = _albumArtistName ?: _artistName get() = _albumArtistName ?: _artistName

View file

@ -74,7 +74,7 @@ class MusicStore private constructor() {
return newResponse return newResponse
} }
private suspend fun loadImpl(context: Context): Response { private fun loadImpl(context: Context): Response {
val notGranted = val notGranted =
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) == ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) ==
PackageManager.PERMISSION_DENIED PackageManager.PERMISSION_DENIED
@ -110,19 +110,18 @@ class MusicStore private constructor() {
val albums: List<Album>, val albums: List<Album>,
val songs: List<Song> val songs: List<Song>
) { ) {
/** Find a song in a faster manner using an ID for its album as well. */ /** Find a song in a faster manner by using the album ID as well.. */
fun findSongFast(songId: Long, albumId: Long): Song? { fun findSongFast(songId: Long, albumId: Long) =
return albums.find { it.id == albumId }?.songs?.find { it.id == songId } albums.find { it.id == albumId }.run { songs.find { it.id == songId } }
}
/** /**
* Find a song for a [uri], this is similar to [findSongFast], but with some kind of content * Find a song for a [uri], this is similar to [findSongFast], but with some kind of content
* uri. * uri.
* @return The corresponding [Song] for this [uri], null if there isn't one. * @return The corresponding [Song] for this [uri], null if there isn't one.
*/ */
fun findSongForUri(context: Context, uri: Uri): Song? { fun findSongForUri(context: Context, uri: Uri) =
return context.contentResolverSafe.useQuery( context.contentResolverSafe.useQuery(uri, arrayOf(OpenableColumns.DISPLAY_NAME)) {
uri, arrayOf(OpenableColumns.DISPLAY_NAME)) { cursor -> cursor ->
cursor.moveToFirst() cursor.moveToFirst()
val displayName = val displayName =
@ -130,16 +129,11 @@ class MusicStore private constructor() {
songs.find { it.fileName == displayName } songs.find { it.fileName == displayName }
} }
}
} }
/**
* A response that [MusicStore] returns when loading music. And before you ask, yes, I do like
* rust.
*/
sealed class Response { sealed class Response {
class Ok(val library: Library) : Response() data class Ok(val library: Library) : Response()
class Err(throwable: Throwable) : Response() data class Err(val throwable: Throwable) : Response()
object NoMusic : Response() object NoMusic : Response()
object NoPerms : Response() object NoPerms : Response()
} }

View file

@ -18,42 +18,38 @@
package org.oxycblt.auxio.music package org.oxycblt.auxio.music
import android.content.Context import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** A [ViewModel] that represents the current music indexing state. */ /** A [ViewModel] that represents the current music indexing state. */
class MusicViewModel : ViewModel(), MusicStore.Callback { class MusicViewModel : ViewModel() {
private val musicStore = MusicStore.getInstance() private val musicStore = MusicStore.getInstance()
private val _loaderResponse = MutableLiveData<MusicStore.Response?>(null) private val _response = MutableStateFlow<MusicStore.Response?>(null)
val loaderResponse: LiveData<MusicStore.Response?> = _loaderResponse val response: StateFlow<MusicStore.Response?> = _response
private var isBusy = false private var isBusy = false
init {
musicStore.addCallback(this)
}
/** /**
* Initiate the loading process. This is done here since HomeFragment will be the first fragment * Initiate the loading process. This is done here since HomeFragment will be the first fragment
* navigated to and because SnackBars will have the best UX here. * navigated to and because SnackBars will have the best UX here.
*/ */
fun loadMusic(context: Context) { fun loadMusic(context: Context) {
if (_loaderResponse.value != null || isBusy) { if (_response.value != null || isBusy) {
logD("Loader is busy/already completed, not reloading") logD("Loader is busy/already completed, not reloading")
return return
} }
isBusy = true isBusy = true
_loaderResponse.value = null _response.value = null
viewModelScope.launch { viewModelScope.launch {
val result = musicStore.load(context) val result = musicStore.load(context)
_loaderResponse.value = result _response.value = result
isBusy = false isBusy = false
} }
} }
@ -64,16 +60,7 @@ class MusicViewModel : ViewModel(), MusicStore.Callback {
*/ */
fun reloadMusic(context: Context) { fun reloadMusic(context: Context) {
logD("Reloading music library") logD("Reloading music library")
_loaderResponse.value = null _response.value = null
loadMusic(context) loadMusic(context)
} }
override fun onMusicUpdate(response: MusicStore.Response) {
_loaderResponse.value = response
}
override fun onCleared() {
super.onCleared()
musicStore.removeCallback(this)
}
} }

View file

@ -33,6 +33,7 @@ import org.oxycblt.auxio.databinding.DialogExcludedBinding
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.hardRestart import org.oxycblt.auxio.util.hardRestart
import org.oxycblt.auxio.util.launch
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast
@ -92,7 +93,7 @@ class ExcludedDialog :
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
excludedModel.paths.observe(viewLifecycleOwner, ::updatePaths) launch { excludedModel.paths.collect(::updatePaths) }
logD("Dialog created") logD("Dialog created")
} }

View file

@ -18,12 +18,12 @@
package org.oxycblt.auxio.music.excluded package org.oxycblt.auxio.music.excluded
import android.content.Context import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -37,8 +37,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* TODO: Unify with MusicViewModel * TODO: Unify with MusicViewModel
*/ */
class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewModel() { class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewModel() {
private val _paths = MutableLiveData(mutableListOf<String>()) private val _paths = MutableStateFlow(mutableListOf<String>())
val paths: LiveData<MutableList<String>> val paths: StateFlow<MutableList<String>>
get() = _paths get() = _paths
var isModified: Boolean = false var isModified: Boolean = false

View file

@ -91,6 +91,8 @@ class ExoPlayerBackend(private val inner: MediaStoreBackend) : Indexer.Backend {
// Spin until all tasks are complete // Spin until all tasks are complete
} }
// TODO: Stabilize sorting order
return songs return songs
} }
@ -124,9 +126,8 @@ class ExoPlayerBackend(private val inner: MediaStoreBackend) : Indexer.Backend {
// We only support two formats as it stands: // We only support two formats as it stands:
// - ID3v2 text frames // - ID3v2 text frames
// - Vorbis comments // - Vorbis comments
// This should be enough to cover the vast, vast majority of audio formats. // TODO: Formats like flac can have both ID3v2 and OGG tags, so we might want to split
// It is also assumed that a file only has either ID3v2 text frames or vorbis // up this logic.
// comments.
when (val tag = metadata.get(i)) { when (val tag = metadata.get(i)) {
is TextInformationFrame -> is TextInformationFrame ->
if (tag.value.isNotEmpty()) { if (tag.value.isNotEmpty()) {

View file

@ -191,12 +191,16 @@ object Indexer {
return artists return artists
} }
/** Build genres and link them to their particular songs. */ /**
* Group up songs into genres. This is a relatively simple step compared to the other library
* steps, as there is no demand to deduplicate genres by a lowercase name.
*/
private fun buildGenres(songs: List<Song>): List<Genre> { private fun buildGenres(songs: List<Song>): List<Genre> {
val genres = mutableListOf<Genre>() val genres = mutableListOf<Genre>()
val songsByGenre = songs.groupBy { it._genreName?.hashCode() } val songsByGenre = songs.groupBy { it._genreGroupingId }
for (entry in songsByGenre) { for (entry in songsByGenre) {
// The first song fill suffice for template metadata.
val templateSong = entry.value[0] val templateSong = entry.value[0]
genres.add(Genre(rawName = templateSong._genreName, songs = entry.value)) genres.add(Genre(rawName = templateSong._genreName, songs = entry.value))
} }
@ -214,10 +218,4 @@ object Indexer {
/** Create a list of songs from the [Cursor] queried in [query]. */ /** Create a list of songs from the [Cursor] queried in [query]. */
fun loadSongs(context: Context, cursor: Cursor): Collection<Song> fun loadSongs(context: Context, cursor: Cursor): Collection<Song>
} }
sealed class Event {
object Query : Event()
object LoadSongs : Event()
object BuildLibrary : Event()
}
} }

View file

@ -28,6 +28,7 @@ import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.getColorStateListSafe import org.oxycblt.auxio.util.getColorStateListSafe
import org.oxycblt.auxio.util.launch
import org.oxycblt.auxio.util.systemGestureInsetsCompat import org.oxycblt.auxio.util.systemGestureInsetsCompat
import org.oxycblt.auxio.util.textSafe import org.oxycblt.auxio.util.textSafe
@ -78,10 +79,9 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
// -- VIEWMODEL SETUP --- // -- VIEWMODEL SETUP ---
playbackModel.song.observe(viewLifecycleOwner, ::updateSong) launch { playbackModel.song.collect(::updateSong) }
playbackModel.isPlaying.observe(viewLifecycleOwner, ::updateIsPlaying) launch { playbackModel.isPlaying.collect(::updateIsPlaying) }
launch { playbackModel.positionSecs.collect(::updatePosition) }
playbackModel.positionSecs.observe(viewLifecycleOwner, ::updatePosition)
} }
private fun updateSong(song: Song?) { private fun updateSong(song: Song?) {

View file

@ -32,6 +32,7 @@ import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.launch
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
import org.oxycblt.auxio.util.textSafe import org.oxycblt.auxio.util.textSafe
@ -52,6 +53,8 @@ class PlaybackPanelFragment :
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
private val navModel: NavigationViewModel by activityViewModels() private val navModel: NavigationViewModel by activityViewModels()
private var queueItem: MenuItem? = null
override fun onCreateBinding(inflater: LayoutInflater) = override fun onCreateBinding(inflater: LayoutInflater) =
FragmentPlaybackPanelBinding.inflate(inflater) FragmentPlaybackPanelBinding.inflate(inflater)
@ -67,8 +70,6 @@ class PlaybackPanelFragment :
insets insets
} }
val queueItem: MenuItem
binding.playbackToolbar.apply { binding.playbackToolbar.apply {
setNavigationOnClickListener { navModel.mainNavigateTo(MainNavigationAction.COLLAPSE) } setNavigationOnClickListener { navModel.mainNavigateTo(MainNavigationAction.COLLAPSE) }
setOnMenuItemClickListener(this@PlaybackPanelFragment) setOnMenuItemClickListener(this@PlaybackPanelFragment)
@ -105,18 +106,13 @@ class PlaybackPanelFragment :
// --- VIEWMODEL SETUP -- // --- VIEWMODEL SETUP --
playbackModel.song.observe(viewLifecycleOwner, ::updateSong) launch { playbackModel.song.collect(::updateSong) }
playbackModel.parent.observe(viewLifecycleOwner, ::updateParent) launch { playbackModel.parent.collect(::updateParent) }
playbackModel.positionSecs.observe(viewLifecycleOwner, ::updatePosition) launch { playbackModel.positionSecs.collect(::updatePosition) }
playbackModel.repeatMode.observe(viewLifecycleOwner, ::updateRepeat) launch { playbackModel.repeatMode.collect(::updateRepeat) }
playbackModel.isPlaying.observe(viewLifecycleOwner, ::updatePlaying) launch { playbackModel.isPlaying.collect(::updatePlaying) }
playbackModel.isShuffled.observe(viewLifecycleOwner, ::updateShuffled) launch { playbackModel.isShuffled.collect(::updateShuffled) }
launch { playbackModel.nextUp.collect(::updateNextUp) }
playbackModel.nextUp.observe(viewLifecycleOwner) { nextUp ->
// The queue icon uses a selector that will automatically tint the icon as active or
// inactive. We just need to set the flag.
queueItem.isEnabled = nextUp.isNotEmpty()
}
logD("Fragment Created") logD("Fragment Created")
} }
@ -176,4 +172,9 @@ class PlaybackPanelFragment :
private fun updateShuffled(isShuffled: Boolean) { private fun updateShuffled(isShuffled: Boolean) {
requireBinding().playbackShuffle.isActivated = isShuffled requireBinding().playbackShuffle.isActivated = isShuffled
} }
private fun updateNextUp(nextUp: List<Song>) {
requireNotNull(queueItem) { "Cannot update next up in non-view state" }.isEnabled =
nextUp.isNotEmpty()
}
} }

View file

@ -19,10 +19,10 @@ package org.oxycblt.auxio.playback
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
@ -52,32 +52,32 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore
private var pendingDelayedAction: DelayedActionImpl? = null private var pendingDelayedAction: DelayedActionImpl? = null
private val _song = MutableLiveData<Song?>() private val _song = MutableStateFlow<Song?>(null)
/** The current song. */ /** The current song. */
val song: LiveData<Song?> val song: StateFlow<Song?>
get() = _song get() = _song
private val _parent = MutableLiveData<MusicParent?>() private val _parent = MutableStateFlow<MusicParent?>(null)
/** The current model that is being played from, such as an [Album] or [Artist] */ /** The current model that is being played from, such as an [Album] or [Artist] */
val parent: LiveData<MusicParent?> val parent: StateFlow<MusicParent?>
get() = _parent get() = _parent
private val _isPlaying = MutableLiveData(false) private val _isPlaying = MutableStateFlow(false)
val isPlaying: LiveData<Boolean> val isPlaying: StateFlow<Boolean>
get() = _isPlaying get() = _isPlaying
private val _positionSecs = MutableLiveData(0L) private val _positionSecs = MutableStateFlow(0L)
/** The current playback position, in seconds */ /** The current playback position, in seconds */
val positionSecs: LiveData<Long> val positionSecs: StateFlow<Long>
get() = _positionSecs get() = _positionSecs
private val _repeatMode = MutableLiveData(RepeatMode.NONE) private val _repeatMode = MutableStateFlow(RepeatMode.NONE)
/** The current repeat mode, see [RepeatMode] for more information */ /** The current repeat mode, see [RepeatMode] for more information */
val repeatMode: LiveData<RepeatMode> val repeatMode: StateFlow<RepeatMode>
get() = _repeatMode get() = _repeatMode
private val _isShuffled = MutableLiveData(false) private val _isShuffled = MutableStateFlow(false)
val isShuffled: LiveData<Boolean> val isShuffled: StateFlow<Boolean>
get() = _isShuffled get() = _isShuffled
private val _nextUp = MutableLiveData(listOf<Song>()) private val _nextUp = MutableStateFlow(listOf<Song>())
/** The queue, without the previous items. */ /** The queue, without the previous items. */
val nextUp: LiveData<List<Song>> val nextUp: StateFlow<List<Song>>
get() = _nextUp get() = _nextUp
init { init {

View file

@ -28,6 +28,7 @@ import org.oxycblt.auxio.databinding.FragmentQueueBinding
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.launch
import org.oxycblt.auxio.util.requireAttached import org.oxycblt.auxio.util.requireAttached
/** /**
@ -52,7 +53,7 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
// --- VIEWMODEL SETUP ---- // --- VIEWMODEL SETUP ----
playbackModel.nextUp.observe(viewLifecycleOwner, ::updateQueue) launch { playbackModel.nextUp.collect(::updateQueue) }
} }
override fun onDestroyBinding(binding: FragmentQueueBinding) { override fun onDestroyBinding(binding: FragmentQueueBinding) {

View file

@ -331,8 +331,6 @@ class PlaybackStateManager private constructor() {
logD("State read completed successfully in ${System.currentTimeMillis() - start}ms") logD("State read completed successfully in ${System.currentTimeMillis() - start}ms")
// Get off the IO coroutine since it will cause LiveData updates to throw an exception
if (state != null) { if (state != null) {
index = state.index index = state.index
parent = state.parent parent = state.parent

View file

@ -30,6 +30,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import kotlinx.coroutines.launch
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentSearchBinding import org.oxycblt.auxio.databinding.FragmentSearchBinding
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
@ -49,6 +50,7 @@ import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.newMenu
import org.oxycblt.auxio.util.applySpans import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.getSystemServiceSafe import org.oxycblt.auxio.util.getSystemServiceSafe
import org.oxycblt.auxio.util.launch
import org.oxycblt.auxio.util.requireAttached import org.oxycblt.auxio.util.requireAttached
/** /**
@ -107,9 +109,9 @@ class SearchFragment :
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
searchModel.searchResults.observe(viewLifecycleOwner, ::updateResults) launch { searchModel.searchResults.collect(::updateResults) }
navModel.exploreNavigationItem.observe(viewLifecycleOwner, ::handleNavigation) launch { musicModel.response.collect(::handleLoaderResponse) }
musicModel.loaderResponse.observe(viewLifecycleOwner, ::handleLoaderResponse) launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
} }
override fun onDestroyBinding(binding: FragmentSearchBinding) { override fun onDestroyBinding(binding: FragmentSearchBinding) {
@ -144,10 +146,6 @@ class SearchFragment :
} }
private fun updateResults(results: List<Item>) { private fun updateResults(results: List<Item>) {
if (isDetached) {
error("Fragment not attached to activity")
}
val binding = requireBinding() val binding = requireBinding()
searchAdapter.data.submitList(results.toMutableList()) { searchAdapter.data.submitList(results.toMutableList()) {

View file

@ -19,11 +19,11 @@ package org.oxycblt.auxio.search
import android.content.Context import android.content.Context
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import java.text.Normalizer import java.text.Normalizer
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
@ -43,16 +43,17 @@ class SearchViewModel : ViewModel() {
private val musicStore = MusicStore.getInstance() private val musicStore = MusicStore.getInstance()
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
private val _searchResults = MutableLiveData(listOf<Item>()) private val _searchResults = MutableStateFlow(listOf<Item>())
private var _filterMode: DisplayMode? = null
private var lastQuery: String? = null
/** Current search results from the last [search] call. */ /** Current search results from the last [search] call. */
val searchResults: LiveData<List<Item>> val searchResults: StateFlow<List<Item>>
get() = _searchResults get() = _searchResults
private var _filterMode: DisplayMode? = null
val filterMode: DisplayMode? val filterMode: DisplayMode?
get() = _filterMode get() = _filterMode
private var lastQuery: String? = null
init { init {
_filterMode = settingsManager.searchFilterMode _filterMode = settingsManager.searchFilterMode
} }

View file

@ -28,12 +28,14 @@ import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kotlinx.coroutines.flow.collect
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentAboutBinding import org.oxycblt.auxio.databinding.FragmentAboutBinding
import org.oxycblt.auxio.home.HomeViewModel import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.formatDuration import org.oxycblt.auxio.util.formatDuration
import org.oxycblt.auxio.util.launch
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
@ -61,24 +63,37 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
binding.aboutFaq.setOnClickListener { openLinkInBrowser(LINK_FAQ) } binding.aboutFaq.setOnClickListener { openLinkInBrowser(LINK_FAQ) }
binding.aboutLicenses.setOnClickListener { openLinkInBrowser(LINK_LICENSES) } binding.aboutLicenses.setOnClickListener { openLinkInBrowser(LINK_LICENSES) }
homeModel.songs.observe(viewLifecycleOwner) { songs -> launch {
binding.aboutSongCount.textSafe = getString(R.string.fmt_songs_loaded, songs.size) homeModel.songs.collect { songs ->
binding.aboutTotalDuration.textSafe = binding.aboutSongCount.textSafe = getString(R.string.fmt_songs_loaded, songs.size)
getString( binding.aboutTotalDuration.textSafe =
R.string.fmt_total_duration, getString(
songs.sumOf { it.durationSecs }.formatDuration(false)) R.string.fmt_total_duration,
getString(
R.string.fmt_total_duration,
songs.sumOf { it.durationSecs }.formatDuration(false)))
}
} }
homeModel.albums.observe(viewLifecycleOwner) { albums -> launch {
binding.aboutAlbumCount.textSafe = getString(R.string.fmt_albums_loaded, albums.size) homeModel.albums.collect { albums ->
binding.aboutAlbumCount.textSafe =
getString(R.string.fmt_albums_loaded, albums.size)
}
} }
homeModel.artists.observe(viewLifecycleOwner) { artists -> launch {
binding.aboutArtistCount.textSafe = getString(R.string.fmt_artists_loaded, artists.size) homeModel.artists.collect { artists ->
binding.aboutArtistCount.textSafe =
getString(R.string.fmt_artists_loaded, artists.size)
}
} }
homeModel.genres.observe(viewLifecycleOwner) { genres -> launch {
binding.aboutGenreCount.textSafe = getString(R.string.fmt_genres_loaded, genres.size) homeModel.genres.collect { genres ->
binding.aboutGenreCount.textSafe =
getString(R.string.fmt_genres_loaded, genres.size)
}
} }
} }

View file

@ -17,9 +17,9 @@
package org.oxycblt.auxio.ui package org.oxycblt.auxio.ui
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
/** /**
@ -27,17 +27,17 @@ import org.oxycblt.auxio.music.Music
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class NavigationViewModel : ViewModel() { class NavigationViewModel : ViewModel() {
private val _mainNavigationAction = MutableLiveData<MainNavigationAction?>() private val _mainNavigationAction = MutableStateFlow<MainNavigationAction?>(null)
/** Flag for main fragment navigation. Intended for MainFragment use only. */ /** Flag for main fragment navigation. Intended for MainFragment use only. */
val mainNavigationAction: LiveData<MainNavigationAction?> val mainNavigationAction: StateFlow<MainNavigationAction?>
get() = _mainNavigationAction get() = _mainNavigationAction
private val _exploreNavigationItem = MutableLiveData<Music?>() private val _exploreNavigationItem = MutableStateFlow<Music?>(null)
/** /**
* Flag for navigation within the explore fragments. Observe this to coordinate navigation to an * Flag for navigation within the explore fragments. Observe this to coordinate navigation to an
* item's UI. * item's UI.
*/ */
val exploreNavigationItem: LiveData<Music?> val exploreNavigationItem: StateFlow<Music?>
get() = _exploreNavigationItem get() = _exploreNavigationItem
/** Notify MainFragment to navigate to the location outlined in [MainNavigationAction]. */ /** Notify MainFragment to navigate to the location outlined in [MainNavigationAction]. */

View file

@ -31,9 +31,14 @@ import android.widget.TextView
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
/** /**
@ -147,6 +152,18 @@ val @receiver:ColorRes Int.stateList
/** Require the fragment is attached to an activity. */ /** Require the fragment is attached to an activity. */
fun Fragment.requireAttached() = check(!isDetached) { "Fragment is detached from activity" } fun Fragment.requireAttached() = check(!isDetached) { "Fragment is detached from activity" }
/**
* Launches [block] in a lifecycle-aware coroutine once [state] is reached. This is primarily a
* shortcut intended to correctly launch a co-routine on a fragment in a way that won't cause
* miscellaneous coroutine insanity.
*/
fun Fragment.launch(
state: Lifecycle.State = Lifecycle.State.STARTED,
block: suspend CoroutineScope.() -> Unit
) {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(state, block) }
}
/** /**
* Shortcut for querying all items in a database and running [block] with the cursor returned. Will * Shortcut for querying all items in a database and running [block] with the cursor returned. Will
* not run if the cursor is null. * not run if the cursor is null.

View file

@ -31,7 +31,6 @@
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:animateLayoutChanges="true"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"> app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
@ -40,8 +39,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:animateLayoutChanges="true" android:animateLayoutChanges="true"
android:paddingStart="@dimen/spacing_medium" android:paddingStart="@dimen/spacing_medium"
android:paddingEnd="@dimen/spacing_medium" android:paddingEnd="@dimen/spacing_medium">
tools:visibility="invisible">
<TextView <TextView
android:id="@+id/home_loading_status" android:id="@+id/home_loading_status"
@ -70,6 +68,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/lbl_retry" android:text="@string/lbl_retry"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/home_loading_status" /> app:layout_constraintTop_toBottomOf="@+id/home_loading_status" />