From b041fe68d06bc6d75c6a5daae6976b408486d625 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Mon, 20 Jun 2022 12:03:57 -0600 Subject: [PATCH] ui: add util for fragment objects Add a utility to easily work with lifecycle-dependent fragment objects. This reduces the code duplication required to maintain objects that would leak after the destruction of a fragment. We normally would not do this as a delegate, as that usually entails some lifecycle wizardry that can easily break and crash the app in esoteric situations. However, this this just extends the normal lifecycle without watching any state, it seems to be pretty safe to use. --- .../java/org/oxycblt/auxio/MainActivity.kt | 1 - .../org/oxycblt/auxio/home/HomeFragment.kt | 37 +++++------- .../auxio/home/list/SongListFragment.kt | 4 +- .../auxio/home/tabs/TabCustomizeDialog.kt | 27 +++------ .../auxio/music/dirs/MusicDirsDialog.kt | 4 +- .../auxio/playback/PlaybackPanelFragment.kt | 9 ++- .../auxio/playback/PlaybackViewModel.kt | 1 - .../auxio/playback/queue/QueueFragment.kt | 27 ++------- .../replaygain/PreAmpCustomizeDialog.kt | 4 +- .../oxycblt/auxio/search/SearchFragment.kt | 30 +++------- .../org/oxycblt/auxio/settings/Settings.kt | 52 +--------------- .../auxio/settings/SettingsListFragment.kt | 3 +- .../settings/ui/WrappedDialogPreference.kt | 4 +- .../auxio/ui/ViewBindingDialogFragment.kt | 59 +++++++++++++++---- .../oxycblt/auxio/ui/ViewBindingFragment.kt | 54 ++++++++++++++--- .../auxio/ui/accent/AccentCustomizeDialog.kt | 4 +- .../org/oxycblt/auxio/util/FrameworkUtil.kt | 5 -- 17 files changed, 146 insertions(+), 179 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index 3a3643098..cf8e10228 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -30,7 +30,6 @@ import org.oxycblt.auxio.music.IndexerService import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.system.PlaybackService import org.oxycblt.auxio.settings.Settings -import org.oxycblt.auxio.settings.settings import org.oxycblt.auxio.util.androidViewModels import org.oxycblt.auxio.util.getSystemBarInsetsCompat import org.oxycblt.auxio.util.isNight diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index 4aa15df15..b86ed26f0 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -77,18 +77,20 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI private val homeModel: HomeViewModel by androidActivityViewModels() private val indexerModel: IndexerViewModel by activityViewModels() - private var storagePermissionLauncher: ActivityResultLauncher? = null - private var sortItem: MenuItem? = null + // lifecycleObject builds this in the creation step, so doing this is okay. + private val storagePermissionLauncher: ActivityResultLauncher by lifecycleObject { + registerForActivityResult(ActivityResultContracts.RequestPermission()) { + indexerModel.reindex() + } + } + + private val sortItem: MenuItem by lifecycleObject { binding -> + binding.homeToolbar.menu.findItem(R.id.submenu_sorting) + } override fun onCreateBinding(inflater: LayoutInflater) = FragmentHomeBinding.inflate(inflater) override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) { - // Build the permission launcher here as you can only do it in onCreateView/onCreate - storagePermissionLauncher = - registerForActivityResult(ActivityResultContracts.RequestPermission()) { - indexerModel.reindex() - } - binding.homeAppbar.apply { addOnOffsetChangedListener { _, offset -> val range = binding.homeAppbar.totalScrollRange @@ -100,10 +102,7 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI } } - binding.homeToolbar.apply { - sortItem = menu.findItem(R.id.submenu_sorting) - setOnMenuItemClickListener(this@HomeFragment) - } + binding.homeToolbar.setOnMenuItemClickListener(this@HomeFragment) updateTabConfiguration() @@ -151,8 +150,6 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI override fun onDestroyBinding(binding: FragmentHomeBinding) { super.onDestroyBinding(binding) binding.homeToolbar.setOnMenuItemClickListener(null) - storagePermissionLauncher = null - sortItem = null } override fun onMenuItemClick(item: MenuItem): Boolean { @@ -246,9 +243,7 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI } private fun updateSortMenu(displayMode: DisplayMode, isVisible: (Int) -> Boolean) { - val sortMenu = - requireNotNull(sortItem?.subMenu) { "Cannot update sort menu while detached" } - + val sortMenu = requireNotNull(sortItem.subMenu) val toHighlight = homeModel.getSortForDisplay(displayMode) for (option in sortMenu) { @@ -323,18 +318,14 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI } } is Indexer.Response.NoPerms -> { - val launcher = - requireNotNull(storagePermissionLauncher) { - "Cannot access permission launcher while detached" - } - binding.homeIndexingProgress.visibility = View.INVISIBLE binding.homeIndexingStatus.textSafe = getString(R.string.err_no_perms) binding.homeIndexingAction.apply { visibility = View.VISIBLE text = getString(R.string.lbl_grant) setOnClickListener { - launcher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) + storagePermissionLauncher.launch( + Manifest.permission.READ_EXTERNAL_STORAGE) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index 8f1a8f49e..4a2ed3fcd 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -23,7 +23,6 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.settings.Settings -import org.oxycblt.auxio.settings.settings import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.MenuItemListener @@ -31,6 +30,7 @@ import org.oxycblt.auxio.ui.MonoAdapter import org.oxycblt.auxio.ui.SongViewHolder import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.SyncBackingData +import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.formatDuration import org.oxycblt.auxio.util.launch import org.oxycblt.auxio.util.logEOrThrow @@ -41,7 +41,7 @@ import org.oxycblt.auxio.util.logEOrThrow */ class SongListFragment : HomeListFragment() { private val homeAdapter = SongsAdapter(this) - private val settings: Settings by settings() + private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) { super.onBindingCreated(binding, savedInstanceState) diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt index b461af70d..056af8321 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt @@ -26,11 +26,10 @@ import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogTabsBinding import org.oxycblt.auxio.settings.Settings -import org.oxycblt.auxio.settings.settings import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.ViewBindingDialogFragment +import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.requireAttached /** * The dialog for customizing library tabs. @@ -38,9 +37,10 @@ import org.oxycblt.auxio.util.requireAttached */ class TabCustomizeDialog : ViewBindingDialogFragment(), TabAdapter.Listener { private val tabAdapter = TabAdapter(this) - private val settings: Settings by settings() - private var touchHelper: ItemTouchHelper? = null - private var callback: TabDragCallback? = null + private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } + private val touchHelper: ItemTouchHelper by lifecycleObject { + ItemTouchHelper(TabDragCallback(tabAdapter)) + } override fun onCreateBinding(inflater: LayoutInflater) = DialogTabsBinding.inflate(inflater) @@ -65,7 +65,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment(), TabAd binding.tabRecycler.apply { adapter = tabAdapter - requireTouchHelper().attachToRecyclerView(this) + touchHelper.attachToRecyclerView(this) } } @@ -99,7 +99,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment(), TabAd } override fun onPickUpTab(viewHolder: RecyclerView.ViewHolder) { - requireTouchHelper().startDrag(viewHolder) + touchHelper.startDrag(viewHolder) } private fun findSavedTabState(savedInstanceState: Bundle?): Array? { @@ -111,19 +111,6 @@ class TabCustomizeDialog : ViewBindingDialogFragment(), TabAd return null } - private fun requireTouchHelper(): ItemTouchHelper { - requireAttached() - val instance = touchHelper - if (instance != null) { - return instance - } - val newCallback = TabDragCallback(tabAdapter) - val newInstance = ItemTouchHelper(newCallback) - callback = newCallback - touchHelper = newInstance - return newInstance - } - companion object { const val TAG = BuildConfig.APPLICATION_ID + ".tag.TAB_CUSTOMIZE" const val KEY_TABS = BuildConfig.APPLICATION_ID + ".key.PENDING_TABS" diff --git a/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt index ae0153a6f..e01c7d67b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt @@ -31,9 +31,9 @@ import org.oxycblt.auxio.databinding.DialogMusicDirsBinding import org.oxycblt.auxio.music.Directory import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.settings.Settings -import org.oxycblt.auxio.settings.settings import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.androidActivityViewModels +import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.getSystemServiceSafe import org.oxycblt.auxio.util.hardRestart import org.oxycblt.auxio.util.logD @@ -48,7 +48,7 @@ class MusicDirsDialog : private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val dirAdapter = MusicDirAdapter(this) - private val settings: Settings by settings() + private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } private var storageManager: StorageManager? = null override fun onCreateBinding(inflater: LayoutInflater) = diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index 4607a1bc5..cca712bd7 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -57,7 +57,9 @@ class PlaybackPanelFragment : private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val navModel: NavigationViewModel by activityViewModels() - private var queueItem: MenuItem? = null + private val queueItem: MenuItem by lifecycleObject { binding -> + binding.playbackToolbar.menu.findItem(R.id.action_queue) + } override fun onCreateBinding(inflater: LayoutInflater) = FragmentPlaybackPanelBinding.inflate(inflater) @@ -82,7 +84,6 @@ class PlaybackPanelFragment : binding.playbackToolbar.apply { setNavigationOnClickListener { navModel.mainNavigateTo(MainNavigationAction.Collapse) } setOnMenuItemClickListener(this@PlaybackPanelFragment) - queueItem = menu.findItem(R.id.action_queue) } binding.playbackSong.apply { @@ -130,7 +131,6 @@ class PlaybackPanelFragment : binding.playbackToolbar.setOnMenuItemClickListener(null) binding.playbackSong.isSelected = false binding.playbackSeekBar.callback = null - queueItem = null } override fun onMenuItemClick(item: MenuItem): Boolean { @@ -198,7 +198,6 @@ class PlaybackPanelFragment : } private fun updateNextUp(nextUp: List) { - requireNotNull(queueItem) { "Cannot update next up in non-view state" }.isEnabled = - nextUp.isNotEmpty() + queueItem.isEnabled = nextUp.isNotEmpty() } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 7e21a6dd3..c49ccd9e2 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -36,7 +36,6 @@ import org.oxycblt.auxio.playback.state.PlaybackStateDatabase import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.settings.Settings -import org.oxycblt.auxio.settings.settings import org.oxycblt.auxio.util.application import org.oxycblt.auxio.util.logE diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt index 28123006f..1f356e209 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt @@ -29,7 +29,6 @@ import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.launch -import org.oxycblt.auxio.util.requireAttached /** * A [Fragment] that shows the queue and enables editing as well. @@ -37,9 +36,10 @@ import org.oxycblt.auxio.util.requireAttached */ class QueueFragment : ViewBindingFragment(), QueueItemListener { private val playbackModel: PlaybackViewModel by androidActivityViewModels() - private var queueAdapter = QueueAdapter(this) - private var touchHelper: ItemTouchHelper? = null - private var callback: QueueDragCallback? = null + private val queueAdapter = QueueAdapter(this) + private val touchHelper: ItemTouchHelper by lifecycleObject { + ItemTouchHelper(QueueDragCallback(playbackModel)) + } override fun onCreateBinding(inflater: LayoutInflater) = FragmentQueueBinding.inflate(inflater) @@ -48,7 +48,7 @@ class QueueFragment : ViewBindingFragment(), QueueItemList binding.queueRecycler.apply { adapter = queueAdapter - requireTouchHelper().attachToRecyclerView(this) + touchHelper.attachToRecyclerView(this) } // --- VIEWMODEL SETUP ---- @@ -59,12 +59,10 @@ class QueueFragment : ViewBindingFragment(), QueueItemList override fun onDestroyBinding(binding: FragmentQueueBinding) { super.onDestroyBinding(binding) binding.queueRecycler.adapter = null - touchHelper = null - callback = null } override fun onPickUp(viewHolder: RecyclerView.ViewHolder) { - requireTouchHelper().startDrag(viewHolder) + touchHelper.startDrag(viewHolder) } private fun updateQueue(queue: List) { @@ -75,17 +73,4 @@ class QueueFragment : ViewBindingFragment(), QueueItemList queueAdapter.data.submitList(queue.toMutableList()) } - - private fun requireTouchHelper(): ItemTouchHelper { - requireAttached() - val instance = touchHelper - if (instance != null) { - return instance - } - val newCallback = QueueDragCallback(playbackModel) - val newInstance = ItemTouchHelper(newCallback) - callback = newCallback - touchHelper = newInstance - return newInstance - } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt index 0d66abab0..07e56a44a 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt @@ -26,8 +26,8 @@ import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogPreAmpBinding import org.oxycblt.auxio.settings.Settings -import org.oxycblt.auxio.settings.settings import org.oxycblt.auxio.ui.ViewBindingDialogFragment +import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.textSafe /** @@ -35,7 +35,7 @@ import org.oxycblt.auxio.util.textSafe * @author OxygenCobalt */ class PreAmpCustomizeDialog : ViewBindingDialogFragment() { - private val settings: Settings by settings() + private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } override fun onCreateBinding(inflater: LayoutInflater) = DialogPreAmpBinding.inflate(inflater) diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt index 89f33127a..e32f2450d 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -37,17 +37,16 @@ import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.settings.Settings -import org.oxycblt.auxio.settings.settings import org.oxycblt.auxio.ui.Header import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.MenuFragment import org.oxycblt.auxio.ui.MenuItemListener import org.oxycblt.auxio.util.androidViewModels import org.oxycblt.auxio.util.applySpans +import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.getSystemServiceSafe import org.oxycblt.auxio.util.launch import org.oxycblt.auxio.util.logW -import org.oxycblt.auxio.util.requireAttached /** * A [Fragment] that allows for the searching of the entire music library. @@ -59,8 +58,11 @@ class SearchFragment : private val searchModel: SearchViewModel by androidViewModels() private val searchAdapter = SearchAdapter(this) - private val settings: Settings by settings() - private var imm: InputMethodManager? = null + private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } + private val imm: InputMethodManager by lifecycleObject { binding -> + binding.context.getSystemServiceSafe(InputMethodManager::class) + } + private var launchedKeyboard = false override fun onCreateBinding(inflater: LayoutInflater) = FragmentSearchBinding.inflate(inflater) @@ -70,7 +72,7 @@ class SearchFragment : menu.findItem(searchModel.filterMode?.itemId ?: R.id.option_filter_all).isChecked = true setNavigationOnClickListener { - requireImm().hide() + imm.hide() findNavController().navigateUp() } @@ -86,9 +88,7 @@ class SearchFragment : if (!launchedKeyboard) { // Auto-open the keyboard when this view is shown requestFocus() - postDelayed(200) { - requireImm().showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) - } + postDelayed(200) { imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) } launchedKeyboard = true } @@ -108,7 +108,6 @@ class SearchFragment : override fun onDestroyBinding(binding: FragmentSearchBinding) { binding.searchToolbar.setOnMenuItemClickListener(null) binding.searchRecycler.adapter = null - imm = null } override fun onMenuItemClick(item: MenuItem): Boolean { @@ -166,18 +165,7 @@ class SearchFragment : else -> return }) - requireImm().hide() - } - - private fun requireImm(): InputMethodManager { - requireAttached() - val instance = imm - if (instance != null) { - return instance - } - val newInstance = requireContext().getSystemServiceSafe(InputMethodManager::class) - imm = newInstance - return newInstance + imm.hide() } private fun InputMethodManager.hide() { diff --git a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt index 53643a2f8..ac29ab6cd 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt @@ -22,12 +22,7 @@ import android.content.SharedPreferences import android.os.storage.StorageManager import androidx.appcompat.app.AppCompatDelegate import androidx.core.content.edit -import androidx.fragment.app.Fragment -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner import androidx.preference.PreferenceManager -import kotlin.properties.ReadOnlyProperty -import kotlin.reflect.KProperty import org.oxycblt.auxio.R import org.oxycblt.auxio.home.tabs.Tab import org.oxycblt.auxio.music.Directory @@ -39,55 +34,14 @@ import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.accent.Accent import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.requireAttached import org.oxycblt.auxio.util.unlikelyToBeNull -/** - * Shortcut delegate in order to receive a [Settings] that will be created/destroyed - * in each lifecycle. - * - * TODO: Replace with generalized method - */ -fun Fragment.settings(): ReadOnlyProperty = - object : ReadOnlyProperty, DefaultLifecycleObserver { - private var settings: Settings? = null - - init { - lifecycle.addObserver( - object : DefaultLifecycleObserver { - override fun onCreate(owner: LifecycleOwner) { - viewLifecycleOwnerLiveData.observe(this@settings) { viewLifecycleOwner -> - viewLifecycleOwner.lifecycle.addObserver(this) - } - } - }) - } - - override fun getValue(thisRef: Fragment, property: KProperty<*>): Settings { - requireAttached() - - val currentSettings = settings - if (currentSettings != null) { - return currentSettings - } - - val newSettings = Settings(requireContext()) - settings = newSettings - return newSettings - } - - override fun onDestroy(owner: LifecycleOwner) { - settings?.release() - settings = null - } - } - /** * Auxio's settings. * - * This object wraps [SharedPreferences] in a type-safe manner, allowing access to all of the - * major settings that Auxio uses. Mutability is determined by use, as some values are written - * by PreferenceManager and others are written by Auxio's code. + * This object wraps [SharedPreferences] in a type-safe manner, allowing access to all of the major + * settings that Auxio uses. Mutability is determined by use, as some values are written by + * PreferenceManager and others are written by Auxio's code. * * @author OxygenCobalt */ diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt index 815cb7009..fee95a865 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt @@ -56,7 +56,6 @@ import org.oxycblt.auxio.util.showToast @Suppress("UNUSED") class SettingsListFragment : PreferenceFragmentCompat() { private val playbackModel: PlaybackViewModel by androidActivityViewModels() - private val settings: Settings by settings() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -134,6 +133,8 @@ class SettingsListFragment : PreferenceFragmentCompat() { /** Recursively handle a preference, doing any specific actions on it. */ private fun recursivelyHandlePreference(preference: Preference) { + val settings = Settings(requireContext()) + if (!preference.isVisible) return if (preference is PreferenceCategory) { diff --git a/app/src/main/java/org/oxycblt/auxio/settings/ui/WrappedDialogPreference.kt b/app/src/main/java/org/oxycblt/auxio/settings/ui/WrappedDialogPreference.kt index 627a0fa2a..38d0ffa58 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/ui/WrappedDialogPreference.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/ui/WrappedDialogPreference.kt @@ -22,8 +22,8 @@ import android.util.AttributeSet import androidx.preference.DialogPreference /** - * Wraps [DialogPreference] as to make it type-distinct from other preferences while also - * making it possible to use in a PreferenceScreen. + * Wraps [DialogPreference] as to make it type-distinct from other preferences while also making it + * possible to use in a PreferenceScreen. */ class WrappedDialogPreference @JvmOverloads diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingDialogFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingDialogFragment.kt index e4e22118e..56127efae 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingDialogFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingDialogFragment.kt @@ -23,53 +23,73 @@ import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment +import androidx.fragment.app.Fragment import androidx.viewbinding.ViewBinding import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.unlikelyToBeNull /** * A dialog fragment enabling ViewBinding inflation and usage across the dialog fragment lifecycle. * @author OxygenCobalt */ -abstract class ViewBindingDialogFragment : DialogFragment() { - private var _binding: T? = null - - /** - * Inflate the binding from the given [inflater]. This should usually be done by the binding - * implementation's inflate function. - */ - protected abstract fun onCreateBinding(inflater: LayoutInflater): T +abstract class ViewBindingDialogFragment : DialogFragment() { + private var _binding: VB? = null + private var lifecycleObjects = mutableListOf>() /** Called during [onCreateDialog]. Dialog elements should be configured here. */ protected open fun onConfigDialog(builder: AlertDialog.Builder) {} + /** + * Inflate the binding from the given [inflater]. This should usually be done by the binding + * implementation's inflate function. + */ + protected abstract fun onCreateBinding(inflater: LayoutInflater): VB + /** * Called during [onViewCreated] when the binding was successfully inflated and set as the view. * This is where view setup should occur. */ - protected open fun onBindingCreated(binding: T, savedInstanceState: Bundle?) {} + protected open fun onBindingCreated(binding: VB, savedInstanceState: Bundle?) {} /** * Called during [onDestroyView] when the binding should be destroyed and all callbacks or * leaking elements be released. */ - protected open fun onDestroyBinding(binding: T) {} + protected open fun onDestroyBinding(binding: VB) {} /** Maybe get the binding. This will be null outside of the fragment view lifecycle. */ - protected val binding: T? + protected val binding: VB? get() = _binding /** * Get the binding under the assumption that the fragment has a view at this state in the * lifecycle. This will throw an exception if the fragment is not in a valid lifecycle. */ - protected fun requireBinding(): T { + protected fun requireBinding(): VB { return requireNotNull(_binding) { "ViewBinding was available. Fragment should be a valid state " + "right now, but instead it was ${lifecycle.currentState}" } } + fun lifecycleObject(create: (VB) -> T): ReadOnlyProperty { + lifecycleObjects.add(LifecycleObject(null, create)) + + return object : ReadOnlyProperty { + private val objIdx = lifecycleObjects.lastIndex + + @Suppress("UNCHECKED_CAST") + override fun getValue(thisRef: Fragment, property: KProperty<*>) = + requireNotNull(lifecycleObjects[objIdx].data) { + "Cannot access lifecycle object when view does not exist" + } + as T + } + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -84,6 +104,8 @@ abstract class ViewBindingDialogFragment : DialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + val binding = unlikelyToBeNull(_binding) + lifecycleObjects.forEach { it.populate(binding) } onBindingCreated(requireBinding(), savedInstanceState) (requireDialog() as AlertDialog).setView(view) logD("Fragment created") @@ -91,8 +113,19 @@ abstract class ViewBindingDialogFragment : DialogFragment() { override fun onDestroyView() { super.onDestroyView() - onDestroyBinding(requireBinding()) + onDestroyBinding(unlikelyToBeNull(_binding)) + lifecycleObjects.forEach { it.clear() } _binding = null logD("Fragment destroyed") } + + private data class LifecycleObject(var data: T?, val create: (VB) -> T) { + fun populate(binding: VB) { + data = create(binding) + } + + fun clear() { + data = null + } + } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt index 3267dc6a2..bef0c47fc 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt @@ -23,48 +23,71 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.viewbinding.ViewBinding +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.unlikelyToBeNull /** * A fragment enabling ViewBinding inflation and usage across the fragment lifecycle. * @author OxygenCobalt */ -abstract class ViewBindingFragment : Fragment() { - private var _binding: T? = null +abstract class ViewBindingFragment : Fragment() { + private var _binding: VB? = null + private var lifecycleObjects = mutableListOf>() /** * Inflate the binding from the given [inflater]. This should usually be done by the binding * implementation's inflate function. */ - protected abstract fun onCreateBinding(inflater: LayoutInflater): T + protected abstract fun onCreateBinding(inflater: LayoutInflater): VB /** * Called during [onViewCreated] when the binding was successfully inflated and set as the view. * This is where view setup should occur. */ - protected open fun onBindingCreated(binding: T, savedInstanceState: Bundle?) {} + protected open fun onBindingCreated(binding: VB, savedInstanceState: Bundle?) {} /** * Called during [onDestroyView] when the binding should be destroyed and all callbacks or * leaking elements be released. */ - protected open fun onDestroyBinding(binding: T) {} + protected open fun onDestroyBinding(binding: VB) {} /** Maybe get the binding. This will be null outside of the fragment view lifecycle. */ - protected val binding: T? + protected val binding: VB? get() = _binding /** * Get the binding under the assumption that the fragment has a view at this state in the * lifecycle. This will throw an exception if the fragment is not in a valid lifecycle. */ - protected fun requireBinding(): T { + protected fun requireBinding(): VB { return requireNotNull(_binding) { "ViewBinding was available. Fragment should be a valid state " + "right now, but instead it was ${lifecycle.currentState}" } } + /** + * Shortcut to create a member bound to the lifecycle of this fragment. This is automatically + * populated in onBindingCreated, and destroyed in onDestroyBinding. + */ + fun lifecycleObject(create: (VB) -> T): ReadOnlyProperty { + lifecycleObjects.add(LifecycleObject(null, create)) + + return object : ReadOnlyProperty { + private val objIdx = lifecycleObjects.lastIndex + + @Suppress("UNCHECKED_CAST") + override fun getValue(thisRef: Fragment, property: KProperty<*>) = + requireNotNull(lifecycleObjects[objIdx].data) { + "Cannot access lifecycle object when view does not exist" + } + as T + } + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -73,14 +96,27 @@ abstract class ViewBindingFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - onBindingCreated(requireBinding(), savedInstanceState) + val binding = unlikelyToBeNull(_binding) + lifecycleObjects.forEach { it.populate(binding) } + onBindingCreated(binding, savedInstanceState) logD("Fragment created") } override fun onDestroyView() { super.onDestroyView() - onDestroyBinding(requireBinding()) + onDestroyBinding(unlikelyToBeNull(_binding)) + lifecycleObjects.forEach { it.clear() } _binding = null logD("Fragment destroyed") } + + private data class LifecycleObject(var data: T?, val create: (VB) -> T) { + fun populate(binding: VB) { + data = create(binding) + } + + fun clear() { + data = null + } + } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt index ed1447646..d90dbb072 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt @@ -24,8 +24,8 @@ import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogAccentBinding import org.oxycblt.auxio.settings.Settings -import org.oxycblt.auxio.settings.settings import org.oxycblt.auxio.ui.ViewBindingDialogFragment +import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull @@ -36,7 +36,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull class AccentCustomizeDialog : ViewBindingDialogFragment(), AccentAdapter.Listener { private var accentAdapter = AccentAdapter(this) - private val settings: Settings by settings() + private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } override fun onCreateBinding(inflater: LayoutInflater) = DialogAccentBinding.inflate(inflater) diff --git a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt index e5df26252..74ac38ae2 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt @@ -157,11 +157,6 @@ val RecyclerView.canScroll: Boolean val @receiver:ColorRes Int.stateList get() = ColorStateList.valueOf(this) -/** Require the fragment is attached to an 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