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.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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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) =
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue