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:
parent
bd92ba2175
commit
b041fe68d0
17 changed files with 146 additions and 179 deletions
|
@ -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
|
||||
|
|
|
@ -77,18 +77,20 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
|||
private val homeModel: HomeViewModel by androidActivityViewModels()
|
||||
private val indexerModel: IndexerViewModel by activityViewModels()
|
||||
|
||||
private var storagePermissionLauncher: ActivityResultLauncher<String>? = null
|
||||
private var sortItem: MenuItem? = null
|
||||
// lifecycleObject builds this in the creation step, so doing this is okay.
|
||||
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 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<FragmentHomeBinding>(), 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<FragmentHomeBinding>(), 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<FragmentHomeBinding>(), 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<FragmentHomeBinding>(), 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Song>() {
|
||||
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)
|
||||
|
|
|
@ -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<DialogTabsBinding>(), 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<DialogTabsBinding>(), TabAd
|
|||
|
||||
binding.tabRecycler.apply {
|
||||
adapter = tabAdapter
|
||||
requireTouchHelper().attachToRecyclerView(this)
|
||||
touchHelper.attachToRecyclerView(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,7 +99,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
|
|||
}
|
||||
|
||||
override fun onPickUpTab(viewHolder: RecyclerView.ViewHolder) {
|
||||
requireTouchHelper().startDrag(viewHolder)
|
||||
touchHelper.startDrag(viewHolder)
|
||||
}
|
||||
|
||||
private fun findSavedTabState(savedInstanceState: Bundle?): Array<Tab>? {
|
||||
|
@ -111,19 +111,6 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), 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"
|
||||
|
|
|
@ -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) =
|
||||
|
|
|
@ -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<Song>) {
|
||||
requireNotNull(queueItem) { "Cannot update next up in non-view state" }.isEnabled =
|
||||
nextUp.isNotEmpty()
|
||||
queueItem.isEnabled = nextUp.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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<FragmentQueueBinding>(), 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<FragmentQueueBinding>(), QueueItemList
|
|||
|
||||
binding.queueRecycler.apply {
|
||||
adapter = queueAdapter
|
||||
requireTouchHelper().attachToRecyclerView(this)
|
||||
touchHelper.attachToRecyclerView(this)
|
||||
}
|
||||
|
||||
// --- VIEWMODEL SETUP ----
|
||||
|
@ -59,12 +59,10 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), 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<Song>) {
|
||||
|
@ -75,17 +73,4 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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)
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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<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.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<T : ViewBinding> : 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<VB : ViewBinding> : DialogFragment() {
|
||||
private var _binding: VB? = null
|
||||
private var lifecycleObjects = mutableListOf<LifecycleObject<VB, *>>()
|
||||
|
||||
/** 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 <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(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
@ -84,6 +104,8 @@ abstract class ViewBindingDialogFragment<T : ViewBinding> : 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<T : ViewBinding> : DialogFragment() {
|
|||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
onDestroyBinding(requireBinding())
|
||||
onDestroyBinding(unlikelyToBeNull(_binding))
|
||||
lifecycleObjects.forEach { it.clear() }
|
||||
_binding = null
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<T : ViewBinding> : Fragment() {
|
||||
private var _binding: T? = null
|
||||
abstract class ViewBindingFragment<VB : ViewBinding> : Fragment() {
|
||||
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
|
||||
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 <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(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
@ -73,14 +96,27 @@ abstract class ViewBindingFragment<T : ViewBinding> : 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<VB, T>(var data: T?, val create: (VB) -> T) {
|
||||
fun populate(binding: VB) {
|
||||
data = create(binding)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
data = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<DialogAccentBinding>(), 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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue