Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ViewPager2 inside a fragment leaks after replacing the fragment it's in by Navigation Component navigate

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.

Memory Leak 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.

like image 351
Thracian Avatar asked Jul 11 '20 15:07

Thracian


1 Answers

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);
like image 68
Thracian Avatar answered Nov 16 '22 01:11

Thracian