ui: remove memberBinding [#80]

Remove all usages of memberBinding from the app.

For some reason, certain devices running Android 10 and lower will
have a lifecycle race condition whenever the theme is mis-matched.
This ends up resulting in an invalid state whenever memberBinder was
used. Since we can't replace memberBinder with a better solution,
just dumpster the whole thing. This platform is so god damn broken,
jesus christ.

Resolves #80.
This commit is contained in:
OxygenCobalt 2022-02-24 19:31:48 -07:00
parent fc4c7714a2
commit c5be39774a
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
13 changed files with 66 additions and 132 deletions

View file

@ -6,6 +6,9 @@
- Rounded images are more nuanced - Rounded images are more nuanced
- Shuffle and Repeat mode buttons now have more contrast when they are turned on - Shuffle and Repeat mode buttons now have more contrast when they are turned on
#### What's Fixed
- Fixed crash on certain devices running Android 10 and lower when a differing theme from the system theme was used.
#### What's Changed #### What's Changed
- All cover art is now cropped to a 1:1 aspect ratio - All cover art is now cropped to a 1:1 aspect ratio
@ -14,6 +17,7 @@
- Switches now have a disabled state - Switches now have a disabled state
- Reworked dynamic color usage - Reworked dynamic color usage
- Reworked logging - Reworked logging
- Upgrade ExoPlayer to v2.17.0 [Eliminates custom fork]
## v2.2.1 ## v2.2.1
#### What's Improved #### What's Improved

View file

@ -57,6 +57,7 @@ class AlbumDetailFragment : DetailFragment() {
): View { ): View {
detailModel.setAlbum(args.albumId) detailModel.setAlbum(args.albumId)
val binding = FragmentDetailBinding.inflate(layoutInflater)
val detailAdapter = AlbumDetailAdapter( val detailAdapter = AlbumDetailAdapter(
playbackModel, detailModel, playbackModel, detailModel,
doOnClick = { playbackModel.playSong(it, PlaybackMode.IN_ALBUM) }, doOnClick = { playbackModel.playSong(it, PlaybackMode.IN_ALBUM) },
@ -67,7 +68,7 @@ class AlbumDetailFragment : DetailFragment() {
binding.lifecycleOwner = viewLifecycleOwner binding.lifecycleOwner = viewLifecycleOwner
setupToolbar(detailModel.curAlbum.value!!, R.menu.menu_album_detail) { itemId -> setupToolbar(detailModel.curAlbum.value!!, binding, R.menu.menu_album_detail) { itemId ->
when (itemId) { when (itemId) {
R.id.action_play_next -> { R.id.action_play_next -> {
playbackModel.playNext(detailModel.curAlbum.value!!) playbackModel.playNext(detailModel.curAlbum.value!!)
@ -85,7 +86,7 @@ class AlbumDetailFragment : DetailFragment() {
} }
} }
setupRecycler(detailAdapter) { pos -> setupRecycler(binding, detailAdapter) { pos ->
val item = detailAdapter.currentList[pos] val item = detailAdapter.currentList[pos]
item is Header || item is ActionHeader || item is Album item is Header || item is ActionHeader || item is Album
} }
@ -113,7 +114,7 @@ class AlbumDetailFragment : DetailFragment() {
is Song -> { is Song -> {
if (detailModel.curAlbum.value!!.id == item.album.id) { if (detailModel.curAlbum.value!!.id == item.album.id) {
logD("Navigating to a song in this album") logD("Navigating to a song in this album")
scrollToItem(item.id, detailAdapter) scrollToItem(item.id, binding, detailAdapter)
detailModel.finishNavToItem() detailModel.finishNavToItem()
} else { } else {
logD("Navigating to another album") logD("Navigating to another album")
@ -185,7 +186,11 @@ class AlbumDetailFragment : DetailFragment() {
/** /**
* Scroll to an song using its [id]. * Scroll to an song using its [id].
*/ */
private fun scrollToItem(id: Long, adapter: AlbumDetailAdapter) { private fun scrollToItem(
id: Long,
binding: FragmentDetailBinding,
adapter: AlbumDetailAdapter
) {
// Calculate where the item for the currently played song is // Calculate where the item for the currently played song is
val pos = adapter.currentList.indexOfFirst { it.id == id && it is Song } val pos = adapter.currentList.indexOfFirst { it.id == id && it is Song }

View file

@ -25,6 +25,7 @@ import android.view.ViewGroup
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
import org.oxycblt.auxio.music.ActionHeader import org.oxycblt.auxio.music.ActionHeader
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
@ -51,6 +52,7 @@ class ArtistDetailFragment : DetailFragment() {
): View { ): View {
detailModel.setArtist(args.artistId) detailModel.setArtist(args.artistId)
val binding = FragmentDetailBinding.inflate(layoutInflater)
val detailAdapter = ArtistDetailAdapter( val detailAdapter = ArtistDetailAdapter(
playbackModel, playbackModel,
doOnClick = { data -> doOnClick = { data ->
@ -74,8 +76,8 @@ class ArtistDetailFragment : DetailFragment() {
binding.lifecycleOwner = viewLifecycleOwner binding.lifecycleOwner = viewLifecycleOwner
setupToolbar(detailModel.curArtist.value!!) setupToolbar(detailModel.curArtist.value!!, binding)
setupRecycler(detailAdapter) { pos -> setupRecycler(binding, detailAdapter) { pos ->
// If the item is an ActionHeader we need to also make the item full-width // If the item is an ActionHeader we need to also make the item full-width
val item = detailAdapter.currentList[pos] val item = detailAdapter.currentList[pos]
item is Header || item is ActionHeader || item is Artist item is Header || item is ActionHeader || item is Artist

View file

@ -23,13 +23,13 @@ import androidx.appcompat.widget.PopupMenu
import androidx.core.view.children import androidx.core.view.children
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.Navigation.findNavController
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.memberBinding
import org.oxycblt.auxio.util.applySpans import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -40,7 +40,6 @@ import org.oxycblt.auxio.util.logD
abstract class DetailFragment : Fragment() { abstract class DetailFragment : Fragment() {
protected val detailModel: DetailViewModel by activityViewModels() protected val detailModel: DetailViewModel by activityViewModels()
protected val playbackModel: PlaybackViewModel by activityViewModels() protected val playbackModel: PlaybackViewModel by activityViewModels()
protected val binding by memberBinding(FragmentDetailBinding::inflate)
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
@ -61,6 +60,7 @@ abstract class DetailFragment : Fragment() {
*/ */
protected fun setupToolbar( protected fun setupToolbar(
data: MusicParent, data: MusicParent,
binding: FragmentDetailBinding,
@MenuRes menuId: Int = -1, @MenuRes menuId: Int = -1,
onMenuClick: ((itemId: Int) -> Boolean)? = null onMenuClick: ((itemId: Int) -> Boolean)? = null
) { ) {
@ -87,6 +87,7 @@ abstract class DetailFragment : Fragment() {
* Shortcut method for recyclerview setup * Shortcut method for recyclerview setup
*/ */
protected fun setupRecycler( protected fun setupRecycler(
binding: FragmentDetailBinding,
detailAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>, detailAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>,
gridLookup: (Int) -> Boolean gridLookup: (Int) -> Boolean
) { ) {

View file

@ -24,6 +24,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter
import org.oxycblt.auxio.music.ActionHeader import org.oxycblt.auxio.music.ActionHeader
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
@ -51,6 +52,7 @@ class GenreDetailFragment : DetailFragment() {
): View { ): View {
detailModel.setGenre(args.genreId) detailModel.setGenre(args.genreId)
val binding = FragmentDetailBinding.inflate(inflater)
val detailAdapter = GenreDetailAdapter( val detailAdapter = GenreDetailAdapter(
playbackModel, playbackModel,
doOnClick = { song -> doOnClick = { song ->
@ -65,8 +67,8 @@ class GenreDetailFragment : DetailFragment() {
binding.lifecycleOwner = viewLifecycleOwner binding.lifecycleOwner = viewLifecycleOwner
setupToolbar(detailModel.curGenre.value!!) setupToolbar(detailModel.curGenre.value!!, binding)
setupRecycler(detailAdapter) { pos -> setupRecycler(binding, detailAdapter) { pos ->
val item = detailAdapter.currentList[pos] val item = detailAdapter.currentList[pos]
item is Header || item is ActionHeader || item is Genre item is Header || item is ActionHeader || item is Genre
} }

View file

@ -24,6 +24,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.home.HomeFragmentDirections import org.oxycblt.auxio.home.HomeFragmentDirections
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.toDate import org.oxycblt.auxio.music.toDate
@ -43,6 +44,10 @@ class AlbumListFragment : HomeListFragment() {
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
val binding = FragmentHomeListBinding.inflate(layoutInflater)
// / --- UI SETUP ---
binding.lifecycleOwner = viewLifecycleOwner binding.lifecycleOwner = viewLifecycleOwner
val adapter = AlbumAdapter( val adapter = AlbumAdapter(
@ -54,7 +59,7 @@ class AlbumListFragment : HomeListFragment() {
::newMenu ::newMenu
) )
setupRecycler(R.id.home_album_list, adapter, homeModel.albums) setupRecycler(R.id.home_album_list, binding, adapter, homeModel.albums)
return binding.root return binding.root
} }

View file

@ -24,6 +24,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.home.HomeFragmentDirections import org.oxycblt.auxio.home.HomeFragmentDirections
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.ui.ArtistViewHolder import org.oxycblt.auxio.ui.ArtistViewHolder
@ -40,6 +41,10 @@ class ArtistListFragment : HomeListFragment() {
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
val binding = FragmentHomeListBinding.inflate(layoutInflater)
// / --- UI SETUP ---
binding.lifecycleOwner = viewLifecycleOwner binding.lifecycleOwner = viewLifecycleOwner
val adapter = ArtistAdapter( val adapter = ArtistAdapter(
@ -51,7 +56,7 @@ class ArtistListFragment : HomeListFragment() {
::newMenu ::newMenu
) )
setupRecycler(R.id.home_artist_list, adapter, homeModel.artists) setupRecycler(R.id.home_artist_list, binding, adapter, homeModel.artists)
return binding.root return binding.root
} }

View file

@ -24,6 +24,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.home.HomeFragmentDirections import org.oxycblt.auxio.home.HomeFragmentDirections
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.ui.GenreViewHolder import org.oxycblt.auxio.ui.GenreViewHolder
@ -40,6 +41,10 @@ class GenreListFragment : HomeListFragment() {
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
val binding = FragmentHomeListBinding.inflate(layoutInflater)
// / --- UI SETUP ---
binding.lifecycleOwner = viewLifecycleOwner binding.lifecycleOwner = viewLifecycleOwner
val adapter = GenreAdapter( val adapter = GenreAdapter(
@ -51,7 +56,7 @@ class GenreListFragment : HomeListFragment() {
::newMenu ::newMenu
) )
setupRecycler(R.id.home_genre_list, adapter, homeModel.genres) setupRecycler(R.id.home_genre_list, binding, adapter, homeModel.genres)
return binding.root return binding.root
} }

View file

@ -28,7 +28,6 @@ import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.home.HomeViewModel import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.music.Item import org.oxycblt.auxio.music.Item
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.memberBinding
import org.oxycblt.auxio.util.applySpans import org.oxycblt.auxio.util.applySpans
/** /**
@ -36,10 +35,6 @@ import org.oxycblt.auxio.util.applySpans
* @author OxygenCobalt * @author OxygenCobalt
*/ */
abstract class HomeListFragment : Fragment() { abstract class HomeListFragment : Fragment() {
protected val binding: FragmentHomeListBinding by memberBinding(
FragmentHomeListBinding::inflate
)
protected val homeModel: HomeViewModel by activityViewModels() protected val homeModel: HomeViewModel by activityViewModels()
protected val playbackModel: PlaybackViewModel by activityViewModels() protected val playbackModel: PlaybackViewModel by activityViewModels()
@ -50,6 +45,7 @@ abstract class HomeListFragment : Fragment() {
protected fun <T : Item, VH : RecyclerView.ViewHolder> setupRecycler( protected fun <T : Item, VH : RecyclerView.ViewHolder> setupRecycler(
@IdRes uniqueId: Int, @IdRes uniqueId: Int,
binding: FragmentHomeListBinding,
homeAdapter: HomeAdapter<T, VH>, homeAdapter: HomeAdapter<T, VH>,
homeData: LiveData<List<T>>, homeData: LiveData<List<T>>,
) { ) {

View file

@ -23,6 +23,7 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.toDate import org.oxycblt.auxio.music.toDate
import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.DisplayMode
@ -41,6 +42,10 @@ class SongListFragment : HomeListFragment() {
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
val binding = FragmentHomeListBinding.inflate(layoutInflater)
// / --- UI SETUP ---
binding.lifecycleOwner = viewLifecycleOwner binding.lifecycleOwner = viewLifecycleOwner
val adapter = SongsAdapter( val adapter = SongsAdapter(
@ -50,7 +55,7 @@ class SongListFragment : HomeListFragment() {
::newMenu ::newMenu
) )
setupRecycler(R.id.home_song_list, adapter, homeModel.songs) setupRecycler(R.id.home_song_list, binding, adapter, homeModel.songs)
return binding.root return binding.root
} }

View file

@ -32,7 +32,6 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentPlaybackBinding import org.oxycblt.auxio.databinding.FragmentPlaybackBinding
import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.playback.state.LoopMode import org.oxycblt.auxio.playback.state.LoopMode
import org.oxycblt.auxio.ui.memberBinding
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
@ -44,17 +43,19 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
class PlaybackFragment : Fragment() { class PlaybackFragment : Fragment() {
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
private val detailModel: DetailViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels()
private val binding by memberBinding(FragmentPlaybackBinding::inflate) { private var mLastBinding: FragmentPlaybackBinding? = null
playbackSong.isSelected = false // Clear marquee to prevent a memory leak
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
val binding = FragmentPlaybackBinding.inflate(layoutInflater)
val queueItem: MenuItem val queueItem: MenuItem
// See onDestroyView for why we do this
mLastBinding = binding
// --- UI SETUP --- // --- UI SETUP ---
binding.lifecycleOwner = viewLifecycleOwner binding.lifecycleOwner = viewLifecycleOwner
@ -92,6 +93,8 @@ class PlaybackFragment : Fragment() {
// Make marquee of song title work // Make marquee of song title work
binding.playbackSong.isSelected = true binding.playbackSong.isSelected = true
binding.playbackSeekBar.onConfirmListener = playbackModel::setPosition binding.playbackSeekBar.onConfirmListener = playbackModel::setPosition
// Abuse the play/pause FAB (see style definition for more info)
binding.playbackPlayPause.post { binding.playbackPlayPause.post {
binding.playbackPlayPause.stateListAnimator = null binding.playbackPlayPause.stateListAnimator = null
} }
@ -156,6 +159,15 @@ class PlaybackFragment : Fragment() {
return binding.root return binding.root
} }
override fun onDestroyView() {
super.onDestroyView()
// playbackSong will leak if we don't disable marquee, keep the binding around
// so that we can turn it off when we destroy the view.
mLastBinding?.playbackSong?.isSelected = false
mLastBinding = null
}
private fun navigateUp() { private fun navigateUp() {
// This is a dumb and fragile hack but this fragment isn't part of the navigation stack // This is a dumb and fragile hack but this fragment isn't part of the navigation stack
// so we can't really do much // so we can't really do much

View file

@ -1,105 +0,0 @@
/*
* Copyright (c) 2021 Auxio Project
* MemberBinder.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui
import android.view.LayoutInflater
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import org.oxycblt.auxio.util.assertMainThread
import org.oxycblt.auxio.util.inflater
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.
* @param inflate The ViewBinding inflation method that should be used
* @param onDestroy What to do when the binding is destroyed
*/
fun <T : ViewDataBinding> Fragment.memberBinding(
inflate: (LayoutInflater) -> T,
onDestroy: T.() -> Unit = {}
) = MemberBinder(this, inflate, onDestroy)
/**
* The delegate for the [memberBinding] shortcut function.
* Adapted from KAHelpers (https://github.com/FunkyMuse/KAHelpers/tree/master/viewbinding)
* @author OxygenCobalt
*/
class MemberBinder<T : ViewDataBinding>(
private val fragment: Fragment,
private val inflate: (LayoutInflater) -> T,
private val onDestroy: T.() -> Unit
) : ReadOnlyProperty<Fragment, T>, LifecycleObserver, LifecycleEventObserver {
private var fragmentBinding: T? = null
init {
fragment.observeOwnerThroughCreation {
lifecycle.addObserver(this@MemberBinder)
}
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
assertMainThread()
val binding = fragmentBinding
// If the fragment is already initialized, then just return that.
if (binding != null) {
return binding
}
val lifecycle = fragment.viewLifecycleOwner.lifecycle
check(lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
"Fragment views are destroyed"
}
// Otherwise create the binding and return that.
return inflate(thisRef.requireContext().inflater).also {
fragmentBinding = it
}
}
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_DESTROY) {
fragmentBinding?.onDestroy()
fragmentBinding = null
}
}
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) {
it.viewOwner()
}
}
})
}
}

View file

@ -53,9 +53,7 @@ is separated into three phases:
- Set up ViewModel instances and LiveData observers - Set up ViewModel instances and LiveData observers
`findViewById` is to **only** be used when interfacing with non-Auxio views. Otherwise, view-binding should be `findViewById` is to **only** be used when interfacing with non-Auxio views. Otherwise, view-binding should be
used in all cases. If one needs to keep track of a view-binding outside of `onCreateView`, then one can declare used in all cases. Avoid usages of databinding outside of the `onCreateView` step unless absolutely necessary.
a binding `by memberBinding(BindingClass::inflate)` in order to have a binding that properly disposes itself
on lifecycle events.
At times it may be more appropriate to use a `View` instead of a full blown fragment. This is okay as long as At times it may be more appropriate to use a `View` instead of a full blown fragment. This is okay as long as
view-binding is still used. view-binding is still used.
@ -290,7 +288,6 @@ Shared views and view configuration models. This contains:
- Customized views such as `EdgeAppBarLayout` and `EdgeRecyclerView`, which add some extra functionality not provided by default - Customized views such as `EdgeAppBarLayout` and `EdgeRecyclerView`, which add some extra functionality not provided by default
- Configuration models like `DisplayMode` and `Sort`, which are used in many places but aren't tied to a specific feature. - Configuration models like `DisplayMode` and `Sort`, which are used in many places but aren't tied to a specific feature.
- `newMenu` and `ActionMenu`, which automates menu creation for most data types - `newMenu` and `ActionMenu`, which automates menu creation for most data types
- `memberBinding` and `MemberBinder`, which allows for ViewBindings to be used as a member variable without memory leaks or nullability issues.
#### `.util` #### `.util`
Shared utilities. This is primarily for QoL when developing Auxio. Documentation is provided on each method. Shared utilities. This is primarily for QoL when developing Auxio. Documentation is provided on each method.