From 4933531ca38292ddcfe96e58d00f985b9a97ae8b Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Wed, 16 Dec 2020 19:36:34 -0700 Subject: [PATCH] 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. --- .../auxio/playback/CompactPlaybackFragment.kt | 9 ++- .../auxio/playback/PlaybackFragment.kt | 8 +- .../auxio/ui/FragmentBinderDelegate.kt | 81 +++++++++++++++++++ 3 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/ui/FragmentBinderDelegate.kt diff --git a/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt index c17686689..f5271268e 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt @@ -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(R.id.playback_controls) + val playbackControls = binding.playbackControls val iconPauseToPlay = ContextCompat.getDrawable( requireContext(), R.drawable.ic_pause_to_play diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt index 45bce8776..d9952f588 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt @@ -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(R.id.playback_song).isSelected = false + binding.playbackSong.isSelected = false } // Seeking callbacks diff --git a/app/src/main/java/org/oxycblt/auxio/ui/FragmentBinderDelegate.kt b/app/src/main/java/org/oxycblt/auxio/ui/FragmentBinderDelegate.kt new file mode 100644 index 000000000..19bad4110 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/ui/FragmentBinderDelegate.kt @@ -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 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( + private val fragment: Fragment, + private val binder: (LayoutInflater) -> T, + private val disposeEvents: T.() -> Unit +) : ReadOnlyProperty, 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 + } +}