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:
parent
fc4c7714a2
commit
c5be39774a
13 changed files with 66 additions and 132 deletions
|
@ -6,6 +6,9 @@
|
|||
- Rounded images are more nuanced
|
||||
- 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
|
||||
- All cover art is now cropped to a 1:1 aspect ratio
|
||||
|
||||
|
@ -14,6 +17,7 @@
|
|||
- Switches now have a disabled state
|
||||
- Reworked dynamic color usage
|
||||
- Reworked logging
|
||||
- Upgrade ExoPlayer to v2.17.0 [Eliminates custom fork]
|
||||
|
||||
## v2.2.1
|
||||
#### What's Improved
|
||||
|
|
|
@ -57,6 +57,7 @@ class AlbumDetailFragment : DetailFragment() {
|
|||
): View {
|
||||
detailModel.setAlbum(args.albumId)
|
||||
|
||||
val binding = FragmentDetailBinding.inflate(layoutInflater)
|
||||
val detailAdapter = AlbumDetailAdapter(
|
||||
playbackModel, detailModel,
|
||||
doOnClick = { playbackModel.playSong(it, PlaybackMode.IN_ALBUM) },
|
||||
|
@ -67,7 +68,7 @@ class AlbumDetailFragment : DetailFragment() {
|
|||
|
||||
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) {
|
||||
R.id.action_play_next -> {
|
||||
playbackModel.playNext(detailModel.curAlbum.value!!)
|
||||
|
@ -85,7 +86,7 @@ class AlbumDetailFragment : DetailFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
setupRecycler(detailAdapter) { pos ->
|
||||
setupRecycler(binding, detailAdapter) { pos ->
|
||||
val item = detailAdapter.currentList[pos]
|
||||
item is Header || item is ActionHeader || item is Album
|
||||
}
|
||||
|
@ -113,7 +114,7 @@ class AlbumDetailFragment : DetailFragment() {
|
|||
is Song -> {
|
||||
if (detailModel.curAlbum.value!!.id == item.album.id) {
|
||||
logD("Navigating to a song in this album")
|
||||
scrollToItem(item.id, detailAdapter)
|
||||
scrollToItem(item.id, binding, detailAdapter)
|
||||
detailModel.finishNavToItem()
|
||||
} else {
|
||||
logD("Navigating to another album")
|
||||
|
@ -185,7 +186,11 @@ class AlbumDetailFragment : DetailFragment() {
|
|||
/**
|
||||
* 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
|
||||
val pos = adapter.currentList.indexOfFirst { it.id == id && it is Song }
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import android.view.ViewGroup
|
|||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||
import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
|
||||
import org.oxycblt.auxio.music.ActionHeader
|
||||
import org.oxycblt.auxio.music.Album
|
||||
|
@ -51,6 +52,7 @@ class ArtistDetailFragment : DetailFragment() {
|
|||
): View {
|
||||
detailModel.setArtist(args.artistId)
|
||||
|
||||
val binding = FragmentDetailBinding.inflate(layoutInflater)
|
||||
val detailAdapter = ArtistDetailAdapter(
|
||||
playbackModel,
|
||||
doOnClick = { data ->
|
||||
|
@ -74,8 +76,8 @@ class ArtistDetailFragment : DetailFragment() {
|
|||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
setupToolbar(detailModel.curArtist.value!!)
|
||||
setupRecycler(detailAdapter) { pos ->
|
||||
setupToolbar(detailModel.curArtist.value!!, binding)
|
||||
setupRecycler(binding, detailAdapter) { pos ->
|
||||
// If the item is an ActionHeader we need to also make the item full-width
|
||||
val item = detailAdapter.currentList[pos]
|
||||
item is Header || item is ActionHeader || item is Artist
|
||||
|
|
|
@ -23,13 +23,13 @@ import androidx.appcompat.widget.PopupMenu
|
|||
import androidx.core.view.children
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.Navigation.findNavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.memberBinding
|
||||
import org.oxycblt.auxio.util.applySpans
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
|
@ -40,7 +40,6 @@ import org.oxycblt.auxio.util.logD
|
|||
abstract class DetailFragment : Fragment() {
|
||||
protected val detailModel: DetailViewModel by activityViewModels()
|
||||
protected val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
protected val binding by memberBinding(FragmentDetailBinding::inflate)
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
@ -61,6 +60,7 @@ abstract class DetailFragment : Fragment() {
|
|||
*/
|
||||
protected fun setupToolbar(
|
||||
data: MusicParent,
|
||||
binding: FragmentDetailBinding,
|
||||
@MenuRes menuId: Int = -1,
|
||||
onMenuClick: ((itemId: Int) -> Boolean)? = null
|
||||
) {
|
||||
|
@ -87,6 +87,7 @@ abstract class DetailFragment : Fragment() {
|
|||
* Shortcut method for recyclerview setup
|
||||
*/
|
||||
protected fun setupRecycler(
|
||||
binding: FragmentDetailBinding,
|
||||
detailAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>,
|
||||
gridLookup: (Int) -> Boolean
|
||||
) {
|
||||
|
|
|
@ -24,6 +24,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||
import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter
|
||||
import org.oxycblt.auxio.music.ActionHeader
|
||||
import org.oxycblt.auxio.music.Album
|
||||
|
@ -51,6 +52,7 @@ class GenreDetailFragment : DetailFragment() {
|
|||
): View {
|
||||
detailModel.setGenre(args.genreId)
|
||||
|
||||
val binding = FragmentDetailBinding.inflate(inflater)
|
||||
val detailAdapter = GenreDetailAdapter(
|
||||
playbackModel,
|
||||
doOnClick = { song ->
|
||||
|
@ -65,8 +67,8 @@ class GenreDetailFragment : DetailFragment() {
|
|||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
setupToolbar(detailModel.curGenre.value!!)
|
||||
setupRecycler(detailAdapter) { pos ->
|
||||
setupToolbar(detailModel.curGenre.value!!, binding)
|
||||
setupRecycler(binding, detailAdapter) { pos ->
|
||||
val item = detailAdapter.currentList[pos]
|
||||
item is Header || item is ActionHeader || item is Genre
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.home.HomeFragmentDirections
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.toDate
|
||||
|
@ -43,6 +44,10 @@ class AlbumListFragment : HomeListFragment() {
|
|||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val binding = FragmentHomeListBinding.inflate(layoutInflater)
|
||||
|
||||
// / --- UI SETUP ---
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
val adapter = AlbumAdapter(
|
||||
|
@ -54,7 +59,7 @@ class AlbumListFragment : HomeListFragment() {
|
|||
::newMenu
|
||||
)
|
||||
|
||||
setupRecycler(R.id.home_album_list, adapter, homeModel.albums)
|
||||
setupRecycler(R.id.home_album_list, binding, adapter, homeModel.albums)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.home.HomeFragmentDirections
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.ui.ArtistViewHolder
|
||||
|
@ -40,6 +41,10 @@ class ArtistListFragment : HomeListFragment() {
|
|||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val binding = FragmentHomeListBinding.inflate(layoutInflater)
|
||||
|
||||
// / --- UI SETUP ---
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
val adapter = ArtistAdapter(
|
||||
|
@ -51,7 +56,7 @@ class ArtistListFragment : HomeListFragment() {
|
|||
::newMenu
|
||||
)
|
||||
|
||||
setupRecycler(R.id.home_artist_list, adapter, homeModel.artists)
|
||||
setupRecycler(R.id.home_artist_list, binding, adapter, homeModel.artists)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.home.HomeFragmentDirections
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.ui.GenreViewHolder
|
||||
|
@ -40,6 +41,10 @@ class GenreListFragment : HomeListFragment() {
|
|||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val binding = FragmentHomeListBinding.inflate(layoutInflater)
|
||||
|
||||
// / --- UI SETUP ---
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
val adapter = GenreAdapter(
|
||||
|
@ -51,7 +56,7 @@ class GenreListFragment : HomeListFragment() {
|
|||
::newMenu
|
||||
)
|
||||
|
||||
setupRecycler(R.id.home_genre_list, adapter, homeModel.genres)
|
||||
setupRecycler(R.id.home_genre_list, binding, adapter, homeModel.genres)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
|
|
@ -28,7 +28,6 @@ import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
|||
import org.oxycblt.auxio.home.HomeViewModel
|
||||
import org.oxycblt.auxio.music.Item
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.memberBinding
|
||||
import org.oxycblt.auxio.util.applySpans
|
||||
|
||||
/**
|
||||
|
@ -36,10 +35,6 @@ import org.oxycblt.auxio.util.applySpans
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
abstract class HomeListFragment : Fragment() {
|
||||
protected val binding: FragmentHomeListBinding by memberBinding(
|
||||
FragmentHomeListBinding::inflate
|
||||
)
|
||||
|
||||
protected val homeModel: HomeViewModel by activityViewModels()
|
||||
protected val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
|
||||
|
@ -50,6 +45,7 @@ abstract class HomeListFragment : Fragment() {
|
|||
|
||||
protected fun <T : Item, VH : RecyclerView.ViewHolder> setupRecycler(
|
||||
@IdRes uniqueId: Int,
|
||||
binding: FragmentHomeListBinding,
|
||||
homeAdapter: HomeAdapter<T, VH>,
|
||||
homeData: LiveData<List<T>>,
|
||||
) {
|
||||
|
|
|
@ -23,6 +23,7 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.toDate
|
||||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
|
@ -41,6 +42,10 @@ class SongListFragment : HomeListFragment() {
|
|||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val binding = FragmentHomeListBinding.inflate(layoutInflater)
|
||||
|
||||
// / --- UI SETUP ---
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
val adapter = SongsAdapter(
|
||||
|
@ -50,7 +55,7 @@ class SongListFragment : HomeListFragment() {
|
|||
::newMenu
|
||||
)
|
||||
|
||||
setupRecycler(R.id.home_song_list, adapter, homeModel.songs)
|
||||
setupRecycler(R.id.home_song_list, binding, adapter, homeModel.songs)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
|
|
@ -32,7 +32,6 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.databinding.FragmentPlaybackBinding
|
||||
import org.oxycblt.auxio.detail.DetailViewModel
|
||||
import org.oxycblt.auxio.playback.state.LoopMode
|
||||
import org.oxycblt.auxio.ui.memberBinding
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||
|
||||
|
@ -44,17 +43,19 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
|||
class PlaybackFragment : Fragment() {
|
||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
private val binding by memberBinding(FragmentPlaybackBinding::inflate) {
|
||||
playbackSong.isSelected = false // Clear marquee to prevent a memory leak
|
||||
}
|
||||
private var mLastBinding: FragmentPlaybackBinding? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val binding = FragmentPlaybackBinding.inflate(layoutInflater)
|
||||
val queueItem: MenuItem
|
||||
|
||||
// See onDestroyView for why we do this
|
||||
mLastBinding = binding
|
||||
|
||||
// --- UI SETUP ---
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
@ -92,6 +93,8 @@ class PlaybackFragment : Fragment() {
|
|||
// Make marquee of song title work
|
||||
binding.playbackSong.isSelected = true
|
||||
binding.playbackSeekBar.onConfirmListener = playbackModel::setPosition
|
||||
|
||||
// Abuse the play/pause FAB (see style definition for more info)
|
||||
binding.playbackPlayPause.post {
|
||||
binding.playbackPlayPause.stateListAnimator = null
|
||||
}
|
||||
|
@ -156,6 +159,15 @@ class PlaybackFragment : Fragment() {
|
|||
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() {
|
||||
// This is a dumb and fragile hack but this fragment isn't part of the navigation stack
|
||||
// so we can't really do much
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -53,9 +53,7 @@ is separated into three phases:
|
|||
- Set up ViewModel instances and LiveData observers
|
||||
|
||||
`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
|
||||
a binding `by memberBinding(BindingClass::inflate)` in order to have a binding that properly disposes itself
|
||||
on lifecycle events.
|
||||
used in all cases. Avoid usages of databinding outside of the `onCreateView` step unless absolutely necessary.
|
||||
|
||||
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.
|
||||
|
@ -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
|
||||
- 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
|
||||
- `memberBinding` and `MemberBinder`, which allows for ViewBindings to be used as a member variable without memory leaks or nullability issues.
|
||||
|
||||
#### `.util`
|
||||
Shared utilities. This is primarily for QoL when developing Auxio. Documentation is provided on each method.
|
||||
|
|
Loading…
Reference in a new issue