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:
OxygenCobalt 2020-12-16 19:36:34 -07:00
parent 71cd15bbf7
commit 4933531ca3
3 changed files with 89 additions and 9 deletions

View file

@ -5,7 +5,6 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
@ -15,6 +14,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentCompactPlaybackBinding
import org.oxycblt.auxio.logD
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.ui.memberBinding
import org.oxycblt.auxio.ui.isLandscape
/**
@ -26,14 +26,15 @@ import org.oxycblt.auxio.ui.isLandscape
*/
class CompactPlaybackFragment : Fragment() {
private val playbackModel: PlaybackViewModel by activityViewModels()
private val binding: FragmentCompactPlaybackBinding by memberBinding(
FragmentCompactPlaybackBinding::inflate
)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = FragmentCompactPlaybackBinding.inflate(inflater)
val isLandscape = isLandscape(resources)
// --- 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
// leak.
val playbackControls = requireView().findViewById<ImageButton>(R.id.playback_controls)
val playbackControls = binding.playbackControls
val iconPauseToPlay = ContextCompat.getDrawable(
requireContext(), R.drawable.ic_pause_to_play

View file

@ -8,7 +8,6 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.SeekBar
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
@ -18,6 +17,7 @@ import org.oxycblt.auxio.databinding.FragmentPlaybackBinding
import org.oxycblt.auxio.logD
import org.oxycblt.auxio.playback.state.LoopMode
import org.oxycblt.auxio.ui.accent
import org.oxycblt.auxio.ui.memberBinding
import org.oxycblt.auxio.ui.toColor
/**
@ -28,14 +28,13 @@ import org.oxycblt.auxio.ui.toColor
*/
class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
private val playbackModel: PlaybackViewModel by activityViewModels()
private val binding: FragmentPlaybackBinding by memberBinding(FragmentPlaybackBinding::inflate)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = FragmentPlaybackBinding.inflate(inflater)
// TODO: Add a swipe-to-next-track function using a ViewPager
// Create accents & icons to use
@ -203,8 +202,7 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
override fun onStop() {
super.onStop()
// Stop the marqueeing of the song name to prevent a weird memory leak
requireView().findViewById<TextView>(R.id.playback_song).isSelected = false
binding.playbackSong.isSelected = false
}
// Seeking callbacks

View file

@ -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
}
}