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(
this@MainDoctorActivity,
LinearLayoutManager.VERTICAL
)
)
}
// Observe selectedService
Settings.selectedService.observe(this@MainDoctorActivity, {
openFragment(supportFragmentManager, Settings.getMappedFragment(it))
})
}
fun logout(view: View) {
Utils.animateClickingButton(view)
Settings.loggedInDoctor = null
val intent = Intent(this@MainDoctorActivity, 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)
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?
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 forOutpatientFragment
?By the way, you can do this to create a map (instead of initialising it in a function):
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:
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