I have a basic navigation structure that starts with a MainFragment
, which has a ViewPager2
& TabLayout
that are connected with a TabLayoutMediator
.
The layout is as follows:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/tabs"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="@dimen/tab_menu_size"
android:layout_gravity="bottom"
android:background="?android:attr/windowBackground"
android:elevation="@dimen/elevation_normal"
app:layout_constraintBottom_toBottomOf="parent"
app:tabGravity="fill"
app:tabIconTint="?android:attr/colorPrimary"
app:tabIconTintMode="src_in"
app:tabIndicator="@drawable/indicator"
app:tabIndicatorColor="?android:attr/colorPrimary"
app:tabMode="fixed"
app:tabRippleColor="@color/selection_color" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Thats created using this function:
private var binding: FragmentMainBinding? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentMainBinding.inflate(inflater)
binding!!.lifecycleOwner = viewLifecycleOwner
binding!!.viewPager.adapter = PagerAdapter(requireActivity())
// Link the ViewPager & Tab View
TabLayoutMediator(binding!!.tabs, binding!!.viewPager) { tab, position ->
tab.icon = ContextCompat.getDrawable(requireContext(), tabIcons[position])
}.attach()
Log.d(this::class.simpleName, "Fragment Created.")
return binding!!.root
}
From the MainFragment
, you can navigate to ArtistDetailFragment
, which just contains a TextView.
The problem seems to be when one navigates from MainFragment
to ArtistDetailFragment
, a reference to the ViewPager2 leaks according to LeakCanary:
┬───
│ GC Root: System class
│
├─ android.app.ActivityThread class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static ActivityThread.sCurrentActivityThread
├─ android.app.ActivityThread instance
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ ActivityThread.mActivities
├─ android.util.ArrayMap instance
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ ArrayMap.mArray
├─ java.lang.Object[] array
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ Object[].[1]
├─ android.app.ActivityThread$ActivityClientRecord instance
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ ActivityThread$ActivityClientRecord.activity
├─ org.oxycblt.auxio.MainActivity instance
│ Leaking: NO (Activity#mDestroyed is false)
│ ↓ MainActivity.mLifecycleRegistry
│ ~~~~~~~~~~~~~~~~~~
├─ androidx.lifecycle.LifecycleRegistry instance
│ Leaking: UNKNOWN
│ ↓ LifecycleRegistry.mObserverMap
│ ~~~~~~~~~~~~
├─ androidx.arch.core.internal.FastSafeIterableMap instance
│ Leaking: UNKNOWN
│ ↓ FastSafeIterableMap.mHashMap
│ ~~~~~~~~
├─ java.util.HashMap instance
│ Leaking: UNKNOWN
│ ↓ HashMap.table
│ ~~~~~
├─ java.util.HashMap$Node[] array
│ Leaking: UNKNOWN
│ ↓ HashMap$Node[].[9]
│ ~~~
├─ java.util.HashMap$Node instance
│ Leaking: UNKNOWN
│ ↓ HashMap$Node.key
│ ~~~
├─ androidx.viewpager2.adapter.FragmentStateAdapter$FragmentMaxLifecycleEnforcer$3 instance
│ Leaking: UNKNOWN
│ Anonymous class implementing androidx.lifecycle.LifecycleEventObserver
│ ↓ FragmentStateAdapter$FragmentMaxLifecycleEnforcer$3.this$1
│ ~~~~~~
├─ androidx.viewpager2.adapter.FragmentStateAdapter$FragmentMaxLifecycleEnforcer instance
│ Leaking: UNKNOWN
│ ↓ FragmentStateAdapter$FragmentMaxLifecycleEnforcer.mViewPager
│ ~~~~~~~~~~
├─ androidx.viewpager2.widget.ViewPager2 instance
│ Leaking: YES (View detached and has parent)
│ mContext instance of org.oxycblt.auxio.MainActivity with mDestroyed = false
│ View#mParent is set
│ View#mAttachInfo is null (view detached)
│ View.mID = R.id.view_pager
│ View.mWindowAttachCount = 1
│ ↓ ViewPager2.mParent
╰→ android.widget.LinearLayout instance
Leaking: YES (ObjectWatcher was watching this because org.oxycblt.auxio.MainFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks))
key = 0c34604a-80e9-4191-a647-d12e74ec8009
watchDurationMillis = 11086
retainedDurationMillis = 6083
mContext instance of org.oxycblt.auxio.MainActivity with mDestroyed = false
View#mParent is null
View#mAttachInfo is null (view detached)
View.mWindowAttachCount = 1
Ive tried clearing the reference to the binding object in onDestroyView()
from This Answer:
override fun onDestroyView() {
super.onDestroyView()
binding = null
}
But that doesnt seem to solve the memory leak either.
Ive also tried clearing the ViewPager's adapter from This Answer
but that causes a crash if I navigate back to MainFragment
. Is there anything I can do here?