androidbottom-sheetleakcanary

How to fix memory leak issue in standard bottom sheet behaviour?


I am using leakcanary and it detects leaks in standard bottom sheet behaviour. But I can't fix this issue,

How can I fix that leak? Ref my leak canary report.

standardBottomSheetBehaviour

  ┬───
    │ GC Root: System class
    │
    ├─ leakcanary.internal.InternalLeakCanary class
    │    Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
    │    ↓ static InternalLeakCanary.resumedActivity
    ├─ com.demoapp.zigmaster.MainActivity instance
    │    Leaking: NO (FragmentTripPlanner↓ is not leaking and Activity#mDestroyed
    │    is false)
    │    mApplication instance of com.demoapp.zigmaster.MyApplication
    │    mBase instance of androidx.appcompat.view.ContextThemeWrapper
    │    ↓ ComponentActivity.mActivityResultRegistry
    ├─ androidx.activity.ComponentActivity$2 instance
    │    Leaking: NO (FragmentTripPlanner↓ is not leaking)
    │    Anonymous subclass of androidx.activity.result.ActivityResultRegistry
    │    this$0 instance of com.demoapp.zigmaster.MainActivity with mDestroyed =
    │    false
    │    ↓ ActivityResultRegistry.mKeyToCallback
    ├─ java.util.HashMap instance
    │    Leaking: NO (FragmentTripPlanner↓ is not leaking)
    │    ↓ HashMap.table
    ├─ java.util.HashMap$HashMapEntry[] array
    │    Leaking: NO (FragmentTripPlanner↓ is not leaking)
    │    ↓ HashMap$HashMapEntry[].[4]
    ├─ java.util.HashMap$HashMapEntry instance
    │    Leaking: NO (FragmentTripPlanner↓ is not leaking)
    │    ↓ HashMap$HashMapEntry.value
    ├─ androidx.activity.result.ActivityResultRegistry$CallbackAndContract instance
    │    Leaking: NO (FragmentTripPlanner↓ is not leaking)
    │    ↓ ActivityResultRegistry$CallbackAndContract.mCallback
    ├─ androidx.fragment.app.FragmentManager$10 instance
    │    Leaking: NO (FragmentTripPlanner↓ is not leaking)
    │    Anonymous class implementing androidx.activity.result.
    │    ActivityResultCallback
    │    ↓ FragmentManager$10.this$0
    ├─ androidx.fragment.app.FragmentManagerImpl instance
    │    Leaking: NO (FragmentTripPlanner↓ is not leaking)
    │    ↓ FragmentManager.mParent
    ├─ com.demoapp.zigmaster.ui.trips.FragmentTripPlanner instance
    │    Leaking: NO (Fragment#mFragmentManager is not null)
    │    ↓ FragmentTripPlanner.standardBottomSheetBehavior
    │                          ~~~~~~~~~~~~~~~~~~~~~~~~~~~
    ├─ com.google.android.material.bottomsheet.BottomSheetBehavior instance
    │    Leaking: UNKNOWN
    │    Retaining 463.1 kB in 6881 objects
    │    ↓ BottomSheetBehavior.viewDragHelper
    │                          ~~~~~~~~~~~~~~
    ├─ androidx.customview.widget.ViewDragHelper instance
    │    Leaking: UNKNOWN
    │    Retaining 462.3 kB in 6859 objects
    │    ↓ ViewDragHelper.mParentView
    │                     ~~~~~~~~~~~
    ╰→ androidx.coordinatorlayout.widget.CoordinatorLayout instance
    ​     Leaking: YES (ObjectWatcher was watching this because com.demoapp.
    ​     zigmaster.ui.trips.FragmentTripPlanner received Fragment#onDestroyView()
    ​     callback (references to its views should be cleared to prevent leaks))
    ​     Retaining 461.9 kB in 6847 objects
    ​     key = 27239e9e-c1f3-4642-b4d8-ab44c954f53b
    ​     watchDurationMillis = 57433
    ​     retainedDurationMillis = 52422
    ​     View not part of a window view hierarchy
    ​     View.mAttachInfo is null (view detached)
    ​     View.mWindowAttachCount = 1
    ​     mContext instance of com.demoapp.zigmaster.MainActivity with
    ​     mDestroyed = false
    
    METADATA
    
    Build.VERSION.SDK_INT: 25
    Build.MANUFACTURER: samsung
    LeakCanary version: 2.7
    App process name: com.demoapp.zigmaster
    Stats: LruCache[maxSize=3000,hits=5750,misses=80700,hitRate=6%]
    RandomAccess[bytes=4089892,reads=80700,travel=60892211355,range=18101017,size=22
    288421]
    Heap dump reason: user request
    Analysis duration: 29922 ms

Here is my code: FragmentTripPlanner.kt

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.google.android.material.bottomsheet.BottomSheetBehavior
import android.widget.LinearLayout

class FragmentTripPlanner : Fragment() {

    private lateinit var standardBottomSheetBehavior: BottomSheetBehavior<LinearLayout>
    private var binding : FragmentTripPlannerBinding ?=null


    override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
    ): View {
        binding = FragmentTripPlannerBinding.inflate(inflater)

        standardBottomSheetBehavior = BottomSheetBehavior.from(binding!!.tripBottomSheet.standardBottomSheet)
        standardBottomSheetBehavior.isHideable = false
        standardBottomSheetBehavior.peekHeight = 560
        standardBottomSheetBehavior.isDraggable = true
        standardBottomSheetBehavior.skipCollapsed = false
        standardBottomSheetBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {

            override fun onSlide(bottomSheet: View, slideOffset: Float) {

            }

            override fun onStateChanged(bottomSheet: View, newState: Int) {
                when (newState) {

                    BottomSheetBehavior.STATE_EXPANDED -> {
                    }
                    else -> {
                    }

                }
            }
        })

        return binding!!.root
    }


    override fun onDestroyView() {
        super.onDestroyView()
        binding=null
    }
}

XML: trip_bottom_sheet.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:id="@+id/bottomSheet"
    app:behavior_hideable="false"
    app:behavior_peekHeight="200dp"
    android:orientation="vertical"
    android:elevation="20dp"
    android:background="@drawable/rounded_dialog_bottom_sheet"
    app:layout_behavior="@string/bottom_sheet_behavior">

    <View
        android:layout_width="match_parent"
        android:layout_height="10dp"
        android:layout_gravity="center"
    />

    </LinearLayout>

fragment_trip_planner.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.trips.FragmentTripPlanner">


    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/map"
        android:name="com.google.android.gms.maps.SupportMapFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
  

    <include
        android:id="@+id/tripBottomSheet"
        layout="@layout/trip_bottom_sheet" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

Solution

  • You need to make standardBottomSheetBehavior nullable then in FragmentTripPlanner.onDestroyView(), you need to clear the reference to standardBottomSheetBehavior since it has a reference to standardBottomSheet

        override fun onDestroyView() {
            super.onDestroyView()
            binding=null
            standardBottomSheetBehavior = null
        }