I am using this MaterialDateTimePicker
library in my app. I have a problem with finding the root cause of the leak. DatePickerDialog
is leaking based on what the log shows. I am not sure if the lib has a problem but I think I am doing something wrong.
I have a View
that displays the current date. The user is able to click it, then DatePickerDialog
displays. Then s/he selects a day and click on the ok button of the DatePickerDialog
. I receive the date, format it and then present it. After a few seconds, Leak Canary lib says hey you leaked. It would be great if you can tell me what's wrong with my code.
So, this is my custom widget.
import android.content.Context
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentManager
import com.atco.forsite.R
import com.atco.forsite.screens.utility.DatePickerOFIState.NOW_FUTURE
import com.atco.forsite.screens.utility.DatePickerOFIState.PAST_NOW
import com.jakewharton.rxrelay2.PublishRelay
import com.wdullaer.materialdatetimepicker.date.DatePickerDialog
import java.util.Calendar
enum class DatePickerOFIState {
PAST_NOW,
NOW_FUTURE
}
class DatePickerOFI(
private val context: Context,
private val fm: FragmentManager,
private val state: DatePickerOFIState
) : DatePickerDialog.OnDateSetListener {
/**
* Subscribe to this observer in order to be notified when the result is ready.
*/
val relay: PublishRelay<Calendar> = PublishRelay.create()
private val datePicker: DatePickerDialog
companion object {
const val TAG = "DatePickerDialog"
}
init {
val now = Calendar.getInstance()
datePicker = DatePickerDialog.newInstance(
this,
now.get(Calendar.YEAR),
now.get(Calendar.MONTH),
now.get(Calendar.DAY_OF_MONTH)
)
datePicker.version = DatePickerDialog.Version.VERSION_2
datePicker.accentColor = ContextCompat.getColor(context, R.color.primaryBlue)
datePicker.setOkColor(ContextCompat.getColor(context, R.color.white))
datePicker.setCancelColor(ContextCompat.getColor(context, R.color.white))
when (state) {
PAST_NOW -> datePicker.maxDate = now
NOW_FUTURE -> datePicker.minDate = now
}
}
fun show() {
datePicker.show(fm, TAG)
}
override fun onDateSet(view: DatePickerDialog?, year: Int, monthOfYear: Int, dayOfMonth: Int) {
val calendar = Calendar.getInstance()
calendar.set(year, monthOfYear, dayOfMonth)
relay.accept(calendar)
}
}
There is nothing special in my Activity
. I open the DatePickerDialog
like this:
class SafetyExchangeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_safety_exchange)
val datePicker = DatePickerOFI(this, supportFragmentManager, DatePickerOFIState.PAST_NOW)
datePicker.relay.safeSubscribe(createDefaultObserver(logger) {
setReportDate(this)
})
etReportedDate.setOnClickListener {
datePicker.show()
}
}
}
Leak trace:
┬───
│ 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.mCurRootView
├─ 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.atco.forsite.screens.records.safetyExchange.SafetyExchangeActivity 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 (SafetyExchangeActivity↓ is not leaking and View attached)
│ mContext instance of com.atco.forsite.screens.records.safetyExchange.SafetyExchangeActivity 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.atco.forsite.screens.records.safetyExchange.SafetyExchangeActivity instance
│ Leaking: NO (Activity#mDestroyed is false)
│ ↓ SafetyExchangeActivity.datePicker
│ ~~~~~~~~~~
├─ com.atco.forsite.screens.utility.DatePickerOFI instance
│ Leaking: UNKNOWN
│ ↓ DatePickerOFI.datePicker
│ ~~~~~~~~~~
╰→ com.wdullaer.materialdatetimepicker.date.DatePickerDialog instance
Leaking: YES (ObjectWatcher was watching this because com.wdullaer.materialdatetimepicker.date.DatePickerDialog received Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
key = dd153a49-8e66-45f7-8736-16342134e7f6
watchDurationMillis = 5297
retainedDurationMillis = 297
METADATA
Build.VERSION.SDK_INT: 29
Build.MANUFACTURER: Google
LeakCanary version: 2.2
App process name: com.atco.forsite
Analysis duration: 8193 ms
According to the leak trace, I think the source of the issue is this line
private val datePicker: DatePickerDialog
in DatePickerOFI
class.
As a solution, I think you need to nullify datePicker
instance variable once your job is done i.e the dialog is dismissed.