At first, i had the issue for ViewPager2
inside a tab of BottomNavigationView
and data binding , data binding also leaks with ViewPager2
and should be nulled in onDestroyView
, leaking and managed to narrow the issue to ViewPager2
while navigating from fragment that contains ViewPager2 to another fragment using findNavController().navigate
.
Here is how it occurs, it occurs when i navigate to another fragment that replaces the current one with ViewPager2.
Here is the code
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
nav_graph.xml
<?xml version="1.0" encoding="utf-8"?>
<navigation 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"
android:id="@+id/nav_graph_parent"
app:startDestination="@id/parent_dest">
<fragment
android:id="@+id/parent_dest"
android:name="com.smarttoolfactory.tutorial6_7navigationui_memoryleakcheck.viewpagerfragment.ViewPagerContainerFragment"
android:label="MainFragment"
tools:layout="@layout/fragment_viewpager_container">
<!-- Login -->
<action
android:id="@+id/action_main_dest_to_loginFragment2"
app:destination="@id/loginFragment2" />
</fragment>
<!-- Login -->
<fragment
android:id="@+id/loginFragment2"
android:name="com.smarttoolfactory.tutorial6_7navigationui_memoryleakcheck.blankfragment.LoginFragment2"
android:label="LoginFragment2"
tools:layout="@layout/fragment_login2"/>
</navigation>
Fragment that contains ViewPager2
and TabLayout
class ViewPagerContainerFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_viewpager_container, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// ViewPager2
val viewPager = view.findViewById<ViewPager2>(R.id.viewPager)
/*
Set Adapter for ViewPager inside this fragment using this Fragment,
more specifically childFragmentManager as param
*/
viewPager.adapter = ChildFragmentStateAdapter(this)
// TabLayout
val tabLayout = view.findViewById<TabLayout>(R.id.tabLayout)
// Bind tabs and viewpager
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
when (position) {
0 -> tab.text = "Home"
1 -> tab.text = "Dashboard"
2 -> tab.text = "Notification"
3 -> tab.text = "Login"
}
}.attach()
}
}
fragment_viewpager_container
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tabMode="scrollable" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tabLayout" />
</androidx.constraintlayout.widget.ConstraintLayout>
Nothing special with fragments but i add one of the layouts, maybe Material widgets are leaking, i don't know
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/fragment_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorHome1"
android:padding="8dp">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Home Fragment1"
android:textColor="#fff"
android:textSize="32sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.3" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnNextPage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Next Page"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvTitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
And heap dump from Leak Canary
┬───
│ 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.mTopActivityClient
├─ android.app.ActivityThread$ActivityClientRecord instance
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ ActivityThread$ActivityClientRecord.activity
├─ com.smarttoolfactory.tutorial6_7navigationui_memoryleakcheck.MainActivity instance
│ Leaking: NO (NavHostFragment↓ is not leaking and Activity#mDestroyed is false)
│ ↓ MainActivity.mFragments
├─ androidx.fragment.app.FragmentController instance
│ Leaking: NO (NavHostFragment↓ is not leaking)
│ ↓ FragmentController.mHost
├─ androidx.fragment.app.FragmentActivity$HostCallbacks instance
│ Leaking: NO (NavHostFragment↓ is not leaking)
│ ↓ FragmentActivity$HostCallbacks.mFragmentManager
├─ androidx.fragment.app.FragmentManagerImpl instance
│ Leaking: NO (NavHostFragment↓ is not leaking)
│ ↓ FragmentManagerImpl.mPrimaryNav
├─ androidx.navigation.fragment.NavHostFragment instance
│ Leaking: NO (ViewPagerContainerFragment↓ is not leaking and Fragment#mFragmentManager is not null)
│ ↓ NavHostFragment.mChildFragmentManager
├─ androidx.fragment.app.FragmentManagerImpl instance
│ Leaking: NO (ViewPagerContainerFragment↓ is not leaking)
│ ↓ FragmentManagerImpl.mFragmentStore
├─ androidx.fragment.app.FragmentStore instance
│ Leaking: NO (ViewPagerContainerFragment↓ is not leaking)
│ ↓ FragmentStore.mActive
├─ java.util.HashMap instance
│ Leaking: NO (ViewPagerContainerFragment↓ is not leaking)
│ ↓ HashMap.table
├─ java.util.HashMap$Node[] array
│ Leaking: NO (ViewPagerContainerFragment↓ is not leaking)
│ ↓ HashMap$Node[].[0]
├─ java.util.HashMap$Node instance
│ Leaking: NO (ViewPagerContainerFragment↓ is not leaking)
│ ↓ HashMap$Node.value
├─ androidx.fragment.app.FragmentStateManager instance
│ Leaking: NO (ViewPagerContainerFragment↓ is not leaking)
│ ↓ FragmentStateManager.mFragment
├─ com.smarttoolfactory.tutorial6_7navigationui_memoryleakcheck.viewpagerfragment.ViewPagerContainerFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ ↓ ViewPagerContainerFragment.mLifecycleRegistry
│ ~~~~~~
├─ androidx.lifecycle.LifecycleRegistry instance
│ Leaking: UNKNOWN
│ ↓ LifecycleRegistry.mObserverMap
│ ~~~~
├─ androidx.arch.core.internal.FastSafeIterableMap instance
│ Leaking: UNKNOWN
│ ↓ FastSafeIterableMap.mEnd
│ ~~
├─ androidx.arch.core.internal.SafeIterableMap$Entry instance
│ Leaking: UNKNOWN
│ ↓ SafeIterableMap$Entry.mKey
│ ~~
├─ 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 com.smarttoolfactory.tutorial6_7navigationui_memoryleakcheck.MainActivity with mDestroyed = false
│ View#mParent is set
│ View#mAttachInfo is null (view detached)
│ View.mID = R.id.viewPager
│ View.mWindowAttachCount = 1
│ ↓ ViewPager
I also add github link if you wish to check for yourself or recreate the issue.
Removing adapter from ViewPager2 in onDestroyView
method of fragment solved the memory leak issue with FragmentStateAdapter
override fun onDestroyView() {
val viewPager2 = dataBinding?.viewPager
viewPager2?.let {
it.adapter = null
}
super.onDestroyView()
}
Also set data binding to null in onDestroyView
of fragment, i did it in base fragment, which was causing data binding related memory leak. Or use it as mentioned here for viewBinding, it applies to data binding.
private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = ResultProfileBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
Note: Fragments outlive their views. Make sure you clean up any references to the binding class instance in the fragment's onDestroyView() method.
Another thing to prevent memory leaks with ViewPager2
inside a fragment is to use viewLifeCycleOwner
's lifeCycle which is between onCreateView
and onDestroyView
instead of this
with FragmentStateAdapter as mentioned here.
FragmentManager fm = getChildFragmentManager();
Lifecycle lifecycle = getViewLifecycleOwner().getLifecycle();
fragmentAdapter = new FragmentAdapter(fm, lifecycle);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With