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