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.
This commit is contained in:
OxygenCobalt 2022-06-20 12:03:57 -06:00
parent bd92ba2175
commit b041fe68d0
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
17 changed files with 146 additions and 179 deletions

View file

@ -30,7 +30,6 @@ import org.oxycblt.auxio.music.IndexerService
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.system.PlaybackService import org.oxycblt.auxio.playback.system.PlaybackService
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.settings.settings
import org.oxycblt.auxio.util.androidViewModels import org.oxycblt.auxio.util.androidViewModels
import org.oxycblt.auxio.util.getSystemBarInsetsCompat import org.oxycblt.auxio.util.getSystemBarInsetsCompat
import org.oxycblt.auxio.util.isNight import org.oxycblt.auxio.util.isNight

View file

@ -77,18 +77,20 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
private val homeModel: HomeViewModel by androidActivityViewModels() private val homeModel: HomeViewModel by androidActivityViewModels()
private val indexerModel: IndexerViewModel by activityViewModels() private val indexerModel: IndexerViewModel by activityViewModels()
private var storagePermissionLauncher: ActivityResultLauncher<String>? = null // lifecycleObject builds this in the creation step, so doing this is okay.
private var sortItem: MenuItem? = null private val storagePermissionLauncher: ActivityResultLauncher<String> 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 onCreateBinding(inflater: LayoutInflater) = FragmentHomeBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) { 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 { binding.homeAppbar.apply {
addOnOffsetChangedListener { _, offset -> addOnOffsetChangedListener { _, offset ->
val range = binding.homeAppbar.totalScrollRange val range = binding.homeAppbar.totalScrollRange
@ -100,10 +102,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
} }
} }
binding.homeToolbar.apply { binding.homeToolbar.setOnMenuItemClickListener(this@HomeFragment)
sortItem = menu.findItem(R.id.submenu_sorting)
setOnMenuItemClickListener(this@HomeFragment)
}
updateTabConfiguration() updateTabConfiguration()
@ -151,8 +150,6 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
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 {
@ -246,9 +243,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
} }
private fun updateSortMenu(displayMode: DisplayMode, isVisible: (Int) -> Boolean) { private fun updateSortMenu(displayMode: DisplayMode, isVisible: (Int) -> Boolean) {
val sortMenu = val sortMenu = requireNotNull(sortItem.subMenu)
requireNotNull(sortItem?.subMenu) { "Cannot update sort menu while detached" }
val toHighlight = homeModel.getSortForDisplay(displayMode) val toHighlight = homeModel.getSortForDisplay(displayMode)
for (option in sortMenu) { for (option in sortMenu) {
@ -323,18 +318,14 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
} }
} }
is Indexer.Response.NoPerms -> { is Indexer.Response.NoPerms -> {
val launcher =
requireNotNull(storagePermissionLauncher) {
"Cannot access permission launcher while detached"
}
binding.homeIndexingProgress.visibility = View.INVISIBLE binding.homeIndexingProgress.visibility = View.INVISIBLE
binding.homeIndexingStatus.textSafe = getString(R.string.err_no_perms) binding.homeIndexingStatus.textSafe = getString(R.string.err_no_perms)
binding.homeIndexingAction.apply { binding.homeIndexingAction.apply {
visibility = View.VISIBLE visibility = View.VISIBLE
text = getString(R.string.lbl_grant) text = getString(R.string.lbl_grant)
setOnClickListener { setOnClickListener {
launcher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) storagePermissionLauncher.launch(
Manifest.permission.READ_EXTERNAL_STORAGE)
} }
} }
} }

View file

@ -23,7 +23,6 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.settings.settings
import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.MenuItemListener 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.SongViewHolder
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.SyncBackingData import org.oxycblt.auxio.ui.SyncBackingData
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.formatDuration import org.oxycblt.auxio.util.formatDuration
import org.oxycblt.auxio.util.launch import org.oxycblt.auxio.util.launch
import org.oxycblt.auxio.util.logEOrThrow import org.oxycblt.auxio.util.logEOrThrow
@ -41,7 +41,7 @@ import org.oxycblt.auxio.util.logEOrThrow
*/ */
class SongListFragment : HomeListFragment<Song>() { class SongListFragment : HomeListFragment<Song>() {
private val homeAdapter = SongsAdapter(this) 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?) { override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState) super.onBindingCreated(binding, savedInstanceState)

View file

@ -26,11 +26,10 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogTabsBinding import org.oxycblt.auxio.databinding.DialogTabsBinding
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.settings.settings
import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.requireAttached
/** /**
* The dialog for customizing library tabs. * The dialog for customizing library tabs.
@ -38,9 +37,10 @@ import org.oxycblt.auxio.util.requireAttached
*/ */
class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAdapter.Listener { class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAdapter.Listener {
private val tabAdapter = TabAdapter(this) private val tabAdapter = TabAdapter(this)
private val settings: Settings by settings() private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
private var touchHelper: ItemTouchHelper? = null private val touchHelper: ItemTouchHelper by lifecycleObject {
private var callback: TabDragCallback? = null ItemTouchHelper(TabDragCallback(tabAdapter))
}
override fun onCreateBinding(inflater: LayoutInflater) = DialogTabsBinding.inflate(inflater) override fun onCreateBinding(inflater: LayoutInflater) = DialogTabsBinding.inflate(inflater)
@ -65,7 +65,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
binding.tabRecycler.apply { binding.tabRecycler.apply {
adapter = tabAdapter adapter = tabAdapter
requireTouchHelper().attachToRecyclerView(this) touchHelper.attachToRecyclerView(this)
} }
} }
@ -99,7 +99,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
} }
override fun onPickUpTab(viewHolder: RecyclerView.ViewHolder) { override fun onPickUpTab(viewHolder: RecyclerView.ViewHolder) {
requireTouchHelper().startDrag(viewHolder) touchHelper.startDrag(viewHolder)
} }
private fun findSavedTabState(savedInstanceState: Bundle?): Array<Tab>? { private fun findSavedTabState(savedInstanceState: Bundle?): Array<Tab>? {
@ -111,19 +111,6 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
return null 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 { companion object {
const val TAG = BuildConfig.APPLICATION_ID + ".tag.TAB_CUSTOMIZE" const val TAG = BuildConfig.APPLICATION_ID + ".tag.TAB_CUSTOMIZE"
const val KEY_TABS = BuildConfig.APPLICATION_ID + ".key.PENDING_TABS" const val KEY_TABS = BuildConfig.APPLICATION_ID + ".key.PENDING_TABS"

View file

@ -31,9 +31,9 @@ import org.oxycblt.auxio.databinding.DialogMusicDirsBinding
import org.oxycblt.auxio.music.Directory import org.oxycblt.auxio.music.Directory
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.settings.settings
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getSystemServiceSafe import org.oxycblt.auxio.util.getSystemServiceSafe
import org.oxycblt.auxio.util.hardRestart import org.oxycblt.auxio.util.hardRestart
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -48,7 +48,7 @@ class MusicDirsDialog :
private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val dirAdapter = MusicDirAdapter(this) 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 private var storageManager: StorageManager? = null
override fun onCreateBinding(inflater: LayoutInflater) = override fun onCreateBinding(inflater: LayoutInflater) =

View file

@ -57,7 +57,9 @@ class PlaybackPanelFragment :
private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val navModel: NavigationViewModel by activityViewModels() 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) = override fun onCreateBinding(inflater: LayoutInflater) =
FragmentPlaybackPanelBinding.inflate(inflater) FragmentPlaybackPanelBinding.inflate(inflater)
@ -82,7 +84,6 @@ class PlaybackPanelFragment :
binding.playbackToolbar.apply { binding.playbackToolbar.apply {
setNavigationOnClickListener { navModel.mainNavigateTo(MainNavigationAction.Collapse) } setNavigationOnClickListener { navModel.mainNavigateTo(MainNavigationAction.Collapse) }
setOnMenuItemClickListener(this@PlaybackPanelFragment) setOnMenuItemClickListener(this@PlaybackPanelFragment)
queueItem = menu.findItem(R.id.action_queue)
} }
binding.playbackSong.apply { binding.playbackSong.apply {
@ -130,7 +131,6 @@ class PlaybackPanelFragment :
binding.playbackToolbar.setOnMenuItemClickListener(null) binding.playbackToolbar.setOnMenuItemClickListener(null)
binding.playbackSong.isSelected = false binding.playbackSong.isSelected = false
binding.playbackSeekBar.callback = null binding.playbackSeekBar.callback = null
queueItem = null
} }
override fun onMenuItemClick(item: MenuItem): Boolean { override fun onMenuItemClick(item: MenuItem): Boolean {
@ -198,7 +198,6 @@ class PlaybackPanelFragment :
} }
private fun updateNextUp(nextUp: List<Song>) { private fun updateNextUp(nextUp: List<Song>) {
requireNotNull(queueItem) { "Cannot update next up in non-view state" }.isEnabled = queueItem.isEnabled = nextUp.isNotEmpty()
nextUp.isNotEmpty()
} }
} }

View file

@ -36,7 +36,6 @@ import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.settings.settings
import org.oxycblt.auxio.util.application import org.oxycblt.auxio.util.application
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE

View file

@ -29,7 +29,6 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.launch import org.oxycblt.auxio.util.launch
import org.oxycblt.auxio.util.requireAttached
/** /**
* A [Fragment] that shows the queue and enables editing as well. * A [Fragment] that shows the queue and enables editing as well.
@ -37,9 +36,10 @@ import org.oxycblt.auxio.util.requireAttached
*/ */
class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemListener { class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemListener {
private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private var queueAdapter = QueueAdapter(this) private val queueAdapter = QueueAdapter(this)
private var touchHelper: ItemTouchHelper? = null private val touchHelper: ItemTouchHelper by lifecycleObject {
private var callback: QueueDragCallback? = null ItemTouchHelper(QueueDragCallback(playbackModel))
}
override fun onCreateBinding(inflater: LayoutInflater) = FragmentQueueBinding.inflate(inflater) override fun onCreateBinding(inflater: LayoutInflater) = FragmentQueueBinding.inflate(inflater)
@ -48,7 +48,7 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
binding.queueRecycler.apply { binding.queueRecycler.apply {
adapter = queueAdapter adapter = queueAdapter
requireTouchHelper().attachToRecyclerView(this) touchHelper.attachToRecyclerView(this)
} }
// --- VIEWMODEL SETUP ---- // --- VIEWMODEL SETUP ----
@ -59,12 +59,10 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
override fun onDestroyBinding(binding: FragmentQueueBinding) { override fun onDestroyBinding(binding: FragmentQueueBinding) {
super.onDestroyBinding(binding) super.onDestroyBinding(binding)
binding.queueRecycler.adapter = null binding.queueRecycler.adapter = null
touchHelper = null
callback = null
} }
override fun onPickUp(viewHolder: RecyclerView.ViewHolder) { override fun onPickUp(viewHolder: RecyclerView.ViewHolder) {
requireTouchHelper().startDrag(viewHolder) touchHelper.startDrag(viewHolder)
} }
private fun updateQueue(queue: List<Song>) { private fun updateQueue(queue: List<Song>) {
@ -75,17 +73,4 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
queueAdapter.data.submitList(queue.toMutableList()) 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
}
} }

View file

@ -26,8 +26,8 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogPreAmpBinding import org.oxycblt.auxio.databinding.DialogPreAmpBinding
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.settings.settings
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.textSafe import org.oxycblt.auxio.util.textSafe
/** /**
@ -35,7 +35,7 @@ import org.oxycblt.auxio.util.textSafe
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class PreAmpCustomizeDialog : ViewBindingDialogFragment<DialogPreAmpBinding>() { class PreAmpCustomizeDialog : ViewBindingDialogFragment<DialogPreAmpBinding>() {
private val settings: Settings by settings() private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
override fun onCreateBinding(inflater: LayoutInflater) = DialogPreAmpBinding.inflate(inflater) override fun onCreateBinding(inflater: LayoutInflater) = DialogPreAmpBinding.inflate(inflater)

View file

@ -37,17 +37,16 @@ import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.settings.settings
import org.oxycblt.auxio.ui.Header import org.oxycblt.auxio.ui.Header
import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.MenuFragment import org.oxycblt.auxio.ui.MenuFragment
import org.oxycblt.auxio.ui.MenuItemListener import org.oxycblt.auxio.ui.MenuItemListener
import org.oxycblt.auxio.util.androidViewModels import org.oxycblt.auxio.util.androidViewModels
import org.oxycblt.auxio.util.applySpans import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getSystemServiceSafe import org.oxycblt.auxio.util.getSystemServiceSafe
import org.oxycblt.auxio.util.launch import org.oxycblt.auxio.util.launch
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.requireAttached
/** /**
* A [Fragment] that allows for the searching of the entire music library. * 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 searchModel: SearchViewModel by androidViewModels()
private val searchAdapter = SearchAdapter(this) private val searchAdapter = SearchAdapter(this)
private val settings: Settings by settings() private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
private var imm: InputMethodManager? = null private val imm: InputMethodManager by lifecycleObject { binding ->
binding.context.getSystemServiceSafe(InputMethodManager::class)
}
private var launchedKeyboard = false private var launchedKeyboard = false
override fun onCreateBinding(inflater: LayoutInflater) = FragmentSearchBinding.inflate(inflater) 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 menu.findItem(searchModel.filterMode?.itemId ?: R.id.option_filter_all).isChecked = true
setNavigationOnClickListener { setNavigationOnClickListener {
requireImm().hide() imm.hide()
findNavController().navigateUp() findNavController().navigateUp()
} }
@ -86,9 +88,7 @@ class SearchFragment :
if (!launchedKeyboard) { if (!launchedKeyboard) {
// Auto-open the keyboard when this view is shown // Auto-open the keyboard when this view is shown
requestFocus() requestFocus()
postDelayed(200) { postDelayed(200) { imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) }
requireImm().showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
}
launchedKeyboard = true launchedKeyboard = true
} }
@ -108,7 +108,6 @@ class SearchFragment :
override fun onDestroyBinding(binding: FragmentSearchBinding) { override fun onDestroyBinding(binding: FragmentSearchBinding) {
binding.searchToolbar.setOnMenuItemClickListener(null) binding.searchToolbar.setOnMenuItemClickListener(null)
binding.searchRecycler.adapter = null binding.searchRecycler.adapter = null
imm = null
} }
override fun onMenuItemClick(item: MenuItem): Boolean { override fun onMenuItemClick(item: MenuItem): Boolean {
@ -166,18 +165,7 @@ class SearchFragment :
else -> return else -> return
}) })
requireImm().hide() imm.hide()
}
private fun requireImm(): InputMethodManager {
requireAttached()
val instance = imm
if (instance != null) {
return instance
}
val newInstance = requireContext().getSystemServiceSafe(InputMethodManager::class)
imm = newInstance
return newInstance
} }
private fun InputMethodManager.hide() { private fun InputMethodManager.hide() {

View file

@ -22,12 +22,7 @@ import android.content.SharedPreferences
import android.os.storage.StorageManager import android.os.storage.StorageManager
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit import androidx.core.content.edit
import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.tabs.Tab import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.music.Directory 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.Sort
import org.oxycblt.auxio.ui.accent.Accent import org.oxycblt.auxio.ui.accent.Accent
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.requireAttached
import org.oxycblt.auxio.util.unlikelyToBeNull 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<Fragment, Settings> =
object : ReadOnlyProperty<Fragment, Settings>, 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. * Auxio's settings.
* *
* This object wraps [SharedPreferences] in a type-safe manner, allowing access to all of the * This object wraps [SharedPreferences] in a type-safe manner, allowing access to all of the major
* major settings that Auxio uses. Mutability is determined by use, as some values are written * settings that Auxio uses. Mutability is determined by use, as some values are written by
* by PreferenceManager and others are written by Auxio's code. * PreferenceManager and others are written by Auxio's code.
* *
* @author OxygenCobalt * @author OxygenCobalt
*/ */

View file

@ -56,7 +56,6 @@ import org.oxycblt.auxio.util.showToast
@Suppress("UNUSED") @Suppress("UNUSED")
class SettingsListFragment : PreferenceFragmentCompat() { class SettingsListFragment : PreferenceFragmentCompat() {
private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val settings: Settings by settings()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -134,6 +133,8 @@ class SettingsListFragment : PreferenceFragmentCompat() {
/** Recursively handle a preference, doing any specific actions on it. */ /** Recursively handle a preference, doing any specific actions on it. */
private fun recursivelyHandlePreference(preference: Preference) { private fun recursivelyHandlePreference(preference: Preference) {
val settings = Settings(requireContext())
if (!preference.isVisible) return if (!preference.isVisible) return
if (preference is PreferenceCategory) { if (preference is PreferenceCategory) {

View file

@ -22,8 +22,8 @@ import android.util.AttributeSet
import androidx.preference.DialogPreference import androidx.preference.DialogPreference
/** /**
* Wraps [DialogPreference] as to make it type-distinct from other preferences while also * Wraps [DialogPreference] as to make it type-distinct from other preferences while also making it
* making it possible to use in a PreferenceScreen. * possible to use in a PreferenceScreen.
*/ */
class WrappedDialogPreference class WrappedDialogPreference
@JvmOverloads @JvmOverloads

View file

@ -23,53 +23,73 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder 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.logD
import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* A dialog fragment enabling ViewBinding inflation and usage across the dialog fragment lifecycle. * A dialog fragment enabling ViewBinding inflation and usage across the dialog fragment lifecycle.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
abstract class ViewBindingDialogFragment<T : ViewBinding> : DialogFragment() { abstract class ViewBindingDialogFragment<VB : ViewBinding> : DialogFragment() {
private var _binding: T? = null private var _binding: VB? = null
private var lifecycleObjects = mutableListOf<LifecycleObject<VB, *>>()
/**
* 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
/** Called during [onCreateDialog]. Dialog elements should be configured here. */ /** Called during [onCreateDialog]. Dialog elements should be configured here. */
protected open fun onConfigDialog(builder: AlertDialog.Builder) {} 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. * Called during [onViewCreated] when the binding was successfully inflated and set as the view.
* This is where view setup should occur. * 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 * Called during [onDestroyView] when the binding should be destroyed and all callbacks or
* leaking elements be released. * 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. */ /** 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() = _binding
/** /**
* Get the binding under the assumption that the fragment has a view at this state in the * 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. * 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) { return requireNotNull(_binding) {
"ViewBinding was available. Fragment should be a valid state " + "ViewBinding was available. Fragment should be a valid state " +
"right now, but instead it was ${lifecycle.currentState}" "right now, but instead it was ${lifecycle.currentState}"
} }
} }
fun <T> lifecycleObject(create: (VB) -> T): ReadOnlyProperty<Fragment, T> {
lifecycleObjects.add(LifecycleObject(null, create))
return object : ReadOnlyProperty<Fragment, T> {
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( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -84,6 +104,8 @@ abstract class ViewBindingDialogFragment<T : ViewBinding> : DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val binding = unlikelyToBeNull(_binding)
lifecycleObjects.forEach { it.populate(binding) }
onBindingCreated(requireBinding(), savedInstanceState) onBindingCreated(requireBinding(), savedInstanceState)
(requireDialog() as AlertDialog).setView(view) (requireDialog() as AlertDialog).setView(view)
logD("Fragment created") logD("Fragment created")
@ -91,8 +113,19 @@ abstract class ViewBindingDialogFragment<T : ViewBinding> : DialogFragment() {
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
onDestroyBinding(requireBinding()) onDestroyBinding(unlikelyToBeNull(_binding))
lifecycleObjects.forEach { it.clear() }
_binding = null _binding = null
logD("Fragment destroyed") logD("Fragment destroyed")
} }
private data class LifecycleObject<VB, T>(var data: T?, val create: (VB) -> T) {
fun populate(binding: VB) {
data = create(binding)
}
fun clear() {
data = null
}
}
} }

View file

@ -23,48 +23,71 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* A fragment enabling ViewBinding inflation and usage across the fragment lifecycle. * A fragment enabling ViewBinding inflation and usage across the fragment lifecycle.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
abstract class ViewBindingFragment<T : ViewBinding> : Fragment() { abstract class ViewBindingFragment<VB : ViewBinding> : Fragment() {
private var _binding: T? = null private var _binding: VB? = null
private var lifecycleObjects = mutableListOf<LifecycleObject<VB, *>>()
/** /**
* Inflate the binding from the given [inflater]. This should usually be done by the binding * Inflate the binding from the given [inflater]. This should usually be done by the binding
* implementation's inflate function. * 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. * Called during [onViewCreated] when the binding was successfully inflated and set as the view.
* This is where view setup should occur. * 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 * Called during [onDestroyView] when the binding should be destroyed and all callbacks or
* leaking elements be released. * 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. */ /** 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() = _binding
/** /**
* Get the binding under the assumption that the fragment has a view at this state in the * 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. * 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) { return requireNotNull(_binding) {
"ViewBinding was available. Fragment should be a valid state " + "ViewBinding was available. Fragment should be a valid state " +
"right now, but instead it was ${lifecycle.currentState}" "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 <T> lifecycleObject(create: (VB) -> T): ReadOnlyProperty<Fragment, T> {
lifecycleObjects.add(LifecycleObject(null, create))
return object : ReadOnlyProperty<Fragment, T> {
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( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -73,14 +96,27 @@ abstract class ViewBindingFragment<T : ViewBinding> : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
onBindingCreated(requireBinding(), savedInstanceState) val binding = unlikelyToBeNull(_binding)
lifecycleObjects.forEach { it.populate(binding) }
onBindingCreated(binding, savedInstanceState)
logD("Fragment created") logD("Fragment created")
} }
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
onDestroyBinding(requireBinding()) onDestroyBinding(unlikelyToBeNull(_binding))
lifecycleObjects.forEach { it.clear() }
_binding = null _binding = null
logD("Fragment destroyed") logD("Fragment destroyed")
} }
private data class LifecycleObject<VB, T>(var data: T?, val create: (VB) -> T) {
fun populate(binding: VB) {
data = create(binding)
}
fun clear() {
data = null
}
}
} }

View file

@ -24,8 +24,8 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogAccentBinding import org.oxycblt.auxio.databinding.DialogAccentBinding
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.settings.settings
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
@ -36,7 +36,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
class AccentCustomizeDialog : class AccentCustomizeDialog :
ViewBindingDialogFragment<DialogAccentBinding>(), AccentAdapter.Listener { ViewBindingDialogFragment<DialogAccentBinding>(), AccentAdapter.Listener {
private var accentAdapter = AccentAdapter(this) 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) override fun onCreateBinding(inflater: LayoutInflater) = DialogAccentBinding.inflate(inflater)

View file

@ -157,11 +157,6 @@ val RecyclerView.canScroll: Boolean
val @receiver:ColorRes Int.stateList val @receiver:ColorRes Int.stateList
get() = ColorStateList.valueOf(this) 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 * 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 * shortcut intended to correctly launch a co-routine on a fragment in a way that won't cause