Allow fragments to use member variable bindings
Create a binder delegate so that some fragments can use a binding as a member variable without nullability issues or memory leaks.
This commit is contained in:
parent
71cd15bbf7
commit
4933531ca3
3 changed files with 89 additions and 9 deletions
|
@ -5,7 +5,6 @@ import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageButton
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
@ -15,6 +14,7 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentCompactPlaybackBinding
|
import org.oxycblt.auxio.databinding.FragmentCompactPlaybackBinding
|
||||||
import org.oxycblt.auxio.logD
|
import org.oxycblt.auxio.logD
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
|
import org.oxycblt.auxio.ui.memberBinding
|
||||||
import org.oxycblt.auxio.ui.isLandscape
|
import org.oxycblt.auxio.ui.isLandscape
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,14 +26,15 @@ import org.oxycblt.auxio.ui.isLandscape
|
||||||
*/
|
*/
|
||||||
class CompactPlaybackFragment : Fragment() {
|
class CompactPlaybackFragment : Fragment() {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
private val binding: FragmentCompactPlaybackBinding by memberBinding(
|
||||||
|
FragmentCompactPlaybackBinding::inflate
|
||||||
|
)
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
val binding = FragmentCompactPlaybackBinding.inflate(inflater)
|
|
||||||
|
|
||||||
val isLandscape = isLandscape(resources)
|
val isLandscape = isLandscape(resources)
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
|
@ -94,7 +95,7 @@ class CompactPlaybackFragment : Fragment() {
|
||||||
|
|
||||||
// Use the caveman method of getting a view as storing the binding will cause a memory
|
// Use the caveman method of getting a view as storing the binding will cause a memory
|
||||||
// leak.
|
// leak.
|
||||||
val playbackControls = requireView().findViewById<ImageButton>(R.id.playback_controls)
|
val playbackControls = binding.playbackControls
|
||||||
|
|
||||||
val iconPauseToPlay = ContextCompat.getDrawable(
|
val iconPauseToPlay = ContextCompat.getDrawable(
|
||||||
requireContext(), R.drawable.ic_pause_to_play
|
requireContext(), R.drawable.ic_pause_to_play
|
||||||
|
|
|
@ -8,7 +8,6 @@ import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.SeekBar
|
import android.widget.SeekBar
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
@ -18,6 +17,7 @@ import org.oxycblt.auxio.databinding.FragmentPlaybackBinding
|
||||||
import org.oxycblt.auxio.logD
|
import org.oxycblt.auxio.logD
|
||||||
import org.oxycblt.auxio.playback.state.LoopMode
|
import org.oxycblt.auxio.playback.state.LoopMode
|
||||||
import org.oxycblt.auxio.ui.accent
|
import org.oxycblt.auxio.ui.accent
|
||||||
|
import org.oxycblt.auxio.ui.memberBinding
|
||||||
import org.oxycblt.auxio.ui.toColor
|
import org.oxycblt.auxio.ui.toColor
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,14 +28,13 @@ import org.oxycblt.auxio.ui.toColor
|
||||||
*/
|
*/
|
||||||
class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
private val binding: FragmentPlaybackBinding by memberBinding(FragmentPlaybackBinding::inflate)
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
val binding = FragmentPlaybackBinding.inflate(inflater)
|
|
||||||
|
|
||||||
// TODO: Add a swipe-to-next-track function using a ViewPager
|
// TODO: Add a swipe-to-next-track function using a ViewPager
|
||||||
|
|
||||||
// Create accents & icons to use
|
// Create accents & icons to use
|
||||||
|
@ -203,8 +202,7 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
|
|
||||||
// Stop the marqueeing of the song name to prevent a weird memory leak
|
binding.playbackSong.isSelected = false
|
||||||
requireView().findViewById<TextView>(R.id.playback_song).isSelected = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seeking callbacks
|
// Seeking callbacks
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
package org.oxycblt.auxio.ui
|
||||||
|
|
||||||
|
import android.os.Looper
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.OnLifecycleEvent
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import java.lang.IllegalStateException
|
||||||
|
import kotlin.properties.ReadOnlyProperty
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A delegate that creates a binding that can be used as a member variable without nullability or
|
||||||
|
* memory leaks.
|
||||||
|
*/
|
||||||
|
fun <T : ViewBinding> Fragment.memberBinding(
|
||||||
|
viewBindingFactory: (LayoutInflater) -> T,
|
||||||
|
disposeEvents: T.() -> Unit = {}
|
||||||
|
) = FragmentBinderDelegate(this, viewBindingFactory, disposeEvents)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The delegate for the [binder] shortcut function.
|
||||||
|
* Adapted from KAHelpers (https://github.com/FunkyMuse/KAHelpers/tree/master/viewbinding)
|
||||||
|
*/
|
||||||
|
class FragmentBinderDelegate<T : ViewBinding>(
|
||||||
|
private val fragment: Fragment,
|
||||||
|
private val binder: (LayoutInflater) -> T,
|
||||||
|
private val disposeEvents: T.() -> Unit
|
||||||
|
) : ReadOnlyProperty<Fragment, T>, LifecycleObserver {
|
||||||
|
private var fragmentBinding: T? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
fragment.observeOwnerThroughCreation {
|
||||||
|
lifecycle.addObserver(this@FragmentBinderDelegate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun Fragment.observeOwnerThroughCreation(
|
||||||
|
crossinline viewOwner: LifecycleOwner.() -> Unit
|
||||||
|
) {
|
||||||
|
lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||||
|
override fun onCreate(owner: LifecycleOwner) {
|
||||||
|
super.onCreate(owner)
|
||||||
|
|
||||||
|
viewLifecycleOwnerLiveData.observe(this@observeOwnerThroughCreation) { owner ->
|
||||||
|
owner.viewOwner()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
|
||||||
|
if (Looper.myLooper() != Looper.getMainLooper()) {
|
||||||
|
throw IllegalThreadStateException("View can only be accessed on the main thread.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val binding = fragmentBinding
|
||||||
|
if (binding != null) {
|
||||||
|
return binding
|
||||||
|
}
|
||||||
|
|
||||||
|
val lifecycle = fragment.viewLifecycleOwner.lifecycle
|
||||||
|
|
||||||
|
if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
|
||||||
|
throw IllegalStateException("Fragment views are destroyed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return binder(LayoutInflater.from(thisRef.requireContext())).also { fragmentBinding = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNUSED")
|
||||||
|
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||||
|
fun dispose() {
|
||||||
|
fragmentBinding?.disposeEvents()
|
||||||
|
fragmentBinding = null
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue