How to clear Fragments properly and avoid leakage?

I have a Master-Detail layout I have one activity and 5 fragments, I use an Integer MutableLiveData called selectedService (I keep this in shared-preferences) that is set in the ServiceAdapter to the id of the clicked service, and it’s being observed in the activity to open the right corresponding fragment to the service that was selected

here is my shared-preferences class Settings.kt:

object Settings {

private const val NAME = "MyPreferences"
private const val MODE = Context.MODE_PRIVATE

// Keys
private const val LANGUAGE_ID_KEY = "language_id"

private lateinit var preferences: SharedPreferences

fun init(context: Context) {
    preferences = context.getSharedPreferences(NAME, MODE)
    mappingServicesWithFragments()
}

private inline fun SharedPreferences.edit(operation: (SharedPreferences.Editor) -> Unit) {
    val editor = edit()
    operation(editor)
    editor.apply()
}

var languageID: Int
    get() = preferences.getInt(LANGUAGE_ID_KEY, 1)
    set(value) = preferences.edit { it.putInt(LANGUAGE_ID_KEY, value) }



// to handle service clicks and opening the correct fragments associated with those services
var selectedService: MutableLiveData<Int> = MutableLiveData()
private val serviceFragmentMap = HashMap<Int, Fragment>()

private fun mappingServicesWithFragments() {
    serviceFragmentMap[1] = InpatientFragment()
    serviceFragmentMap[2] = OutpatientFragment()
    serviceFragmentMap[3] = ConsultationFragment()
    serviceFragmentMap[4] = ReleasedPatientsFragment()
    serviceFragmentMap[5] = FavoritesListsFragment()
    serviceFragmentMap[6] = PatientProfileFragment()
    serviceFragmentMap[7] = VitalSignsFragment()
    serviceFragmentMap[8] = DiagnosisFragment()
    serviceFragmentMap[9] = NurseNotesFragment()
    serviceFragmentMap[10] = RadiologyFragment()
    serviceFragmentMap[11] = LaboratoryFragment()
    serviceFragmentMap[12] = MedicationsFragment()
    serviceFragmentMap[13] = ProceduresFragment()
    serviceFragmentMap[14] = OperationsFragment()
    serviceFragmentMap[15] = PatientConsultationsFragment()
}

fun getMappedFragment(key: Int): Fragment? {
    return serviceFragmentMap[key]
}

here is the main activity where I observe on selectedService:

class MainDoctorActivity : BaseActivity() {

private lateinit var services: List<Service>

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main_doctor)
    initialViews()
}

private fun initialViews() {
    // Static List of services for the (Doctor)
    services = listOf(
        Service(1, "Inpatient", R.drawable.ic_inpatients, true),
        Service(2, "Outpatient", R.drawable.ic_outpatients, false),
        Service(3, "Consultation", R.drawable.ic_consultation, false),
        Service(4, "Released patients", R.drawable.ic_released_patients, false),
        Service(5, "Favorites lists", R.drawable.ic_favorites_lists, false)
    )

    // Setting up the Name and ID of the doctor in main screen
    doctorNameTv.text = Settings.loggedInDoctor!!.doctorName
    doctorIdTv.text = Settings.loggedInDoctor!!.doctorID

    rvServices.apply {
        adapter = ServicesAdapter(services)
        addItemDecoration(
            DividerItemDecoration(
                [email protected],
                LinearLayoutManager.VERTICAL
            )
        )
    }

    // Observe selectedService
    Settings.selectedService.observe([email protected], {
        openFragment(supportFragmentManager, Settings.getMappedFragment(it))
    })

}

fun logout(view: View) {
    Utils.animateClickingButton(view)
    Settings.loggedInDoctor = null
    val intent = Intent([email protected], LoginActivity::class.java)
    startActivity(intent)
}

override fun onResume() {
    super.onResume()
    // Open "Inpatient Fragment" as soon as you login/open the app
    openFragment(supportFragmentManager, Settings.getMappedFragment(services[0].id))
}

the openFragment() function is an extension function that I use to open up the desired fragment, and here is the code for it:

internal fun  openFragment(manager: FragmentManager, fragment: Fragment?) {
    manager.beginTransaction().replace(R.id.container, fragment!!).commit()
}

Here is a picture of the whole layout (for visualization purposes) enter image description here

Here is ReleasedPatientsFragment.kt:

class ReleasedPatientsFragment : Fragment() {

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    // Inflate the layout for this fragment
    return inflater.inflate(R.layout.fragment_released_patients, container, false)
}

}

as you can tell I’m literally doing nothing except just displaying the fragment in the container that I have set up in the mainActivity, but I’m getting a leak whenever I’m moving between fragments and I can’t seem to solve the problem.

Here is the CanaryLeak analysis:

┬───
│ GC Root: System class
│
├─ android.view.inputmethod.InputMethodManager class
│    Leaking: NO (InputMethodManager↓ is not leaking and a class is never leaking)
│    ↓ static InputMethodManager.sInstance
├─ android.view.inputmethod.InputMethodManager instance
│    Leaking: NO (DecorView↓ is not leaking and InputMethodManager is a singleton)
│    ↓ InputMethodManager.mNextServedView
├─ com.android.internal.policy.DecorView instance
│    Leaking: NO (LinearLayout↓ is not leaking and View attached)
│    mContext instance of com.android.internal.policy.DecorContext, wrapping activity com.example.emr.ui.activities.maindoctor.MainDoctorActivity with mDestroyed = false
│    Parent android.view.ViewRootImpl not a android.view.View
│    View#mParent is set
│    View#mAttachInfo is not null (view attached)
│    View.mWindowAttachCount = 1
│    ↓ DecorView.mContentRoot
├─ android.widget.LinearLayout instance
│    Leaking: NO (MainDoctorActivity↓ is not leaking and View attached)
│    mContext instance of com.example.emr.ui.activities.maindoctor.MainDoctorActivity with mDestroyed = false
│    View.parent com.android.internal.policy.DecorView attached as well
│    View#mParent is set
│    View#mAttachInfo is not null (view attached)
│    View.mWindowAttachCount = 1
│    ↓ LinearLayout.mContext
├─ com.example.emr.ui.activities.maindoctor.MainDoctorActivity instance
│    Leaking: NO (Activity#mDestroyed is false)
│    ↓ MainDoctorActivity.services
│                         ~~~~~~~~
├─ java.util.Arrays$ArrayList instance
│    Leaking: UNKNOWN
│    ↓ Arrays$ArrayList.a
│                       ~
├─ com.example.emr.model.Service[] array
│    Leaking: UNKNOWN
│    ↓ Service[].[1]
│                ~~~
├─ com.example.emr.model.Service instance
│    Leaking: UNKNOWN
│    ↓ Service.fragment
│              ~~~~~~~~
╰→ com.example.emr.ui.fragments.outpatient.OutpatientFragment instance
​     Leaking: YES (ObjectWatcher was watching this because com.example.emr.ui.fragments.outpatient.OutpatientFragment received Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
​     key = ca0075c3-8df2-423f-8adf-48cd230a692f
​     watchDurationMillis = 8513
​     retainedDurationMillis = 3512

METADATA

Build.VERSION.SDK_INT: 29
Build.MANUFACTURER: Google
LeakCanary version: 2.4
App process name: com.example.emr
Analysis duration: 5271 ms

so form this analysis I can tell the reason is this:

Leaking: YES (ObjectWatcher was watching this because 
com.example.emr.ui.fragments.outpatient.OutpatientFragment received Fragment#onDestroy() callback and 
Fragment#mFragmentManager is null)

but I don’t know how to solve it, can someone please explain why is there a leakage and the steps to take to make sure the fragments aren’t causing any leakage?

Answer

I’m not familiar with CanaryLeak, but could it be because you’re holding an instance of each fragment in serviceFragmentMap? So the system never actually gets to garbage collect them (even if they stop being displayed). Or is it only happening for OutpatientFragment?

By the way, you can do this to create a map (instead of initialising it in a function):

val serviceFragmentMap = mapOf(
    1 to InpatientFragment(),
    2 to OutpatientFragment(),
    ...
)

If you don’t actually want to hold instances, and you just want to pass in an ID and get the right kind of fragment back, you could do this instead:

// you don't need the map type really, it can infer it
val serviceFragmentMap = mapOf<Int, Class<out Fragment>(
    1 to InpatientFragment::class.java,
    2 to OutpatientFragment::class.java,
    ...
)

fun getMappedFragment(key: Int): Fragment? {
    return serviceFragmentMap[key]?.newInstance()
}

that way you’re always getting a fresh fragment whenever you call it, which is closer to how the framework works anyway (e.g. if the system destroys and recreates your fragment, it won’t be the instance you have in your map)

Also I’d recommed getting familiar with Heap Dumps as a way to spot memory leaks – it’s easier than it looks! You just do some stuff that might cause leaks, do a Garbage Collection to clean up any loose objects, then capture a dump. Then you can sort by package, drill down into your app’s stuff, and see how many of each thing there are lurking around. If there’s too many of a thing (like a particular fragment type) you can inspect it and see what’s holding a reference to it and keeping it in memory