So I have this Composable that I use to detect if a keyboard is visible:
@Composable
fun keyboardVisibilityAsState(): State<Boolean> {
val keyboardState = remember { mutableStateOf(false) }
val view = LocalView.current
DisposableEffect(view) {
val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {
val rect = Rect()
view.getWindowVisibleDisplayFrame(rect)
val screenHeight = view.rootView.height
val keypadHeight = screenHeight - rect.bottom
keyboardState.value = keypadHeight > screenHeight * 0.15
}
view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener)
onDispose {
view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener)
}
}
return keyboardState
}
...
// usage:
val isKeyboardVisible by keyboardVisibilityAsState()
It's being used inside another Composable, which in turn is used in a Fragment via ComposeView. Whenever I exit from the Fragment that uses this Composable, LeakCanary flags my Composable as the source of a leak, specifically:
┬───
│ 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 (InputMethodManager is a singleton)
│ ↓ InputMethodManager.mCurRootView
│ ~~~~~~~~~~~~
├─ android.view.ViewRootImpl instance
│ Leaking: UNKNOWN
│ Retaining 16.6 kB in 405 objects
│ mContext instance of com.android.internal.policy.DecorContext, wrapping
│ activity com.someapp.ui.home.HomeActivity with mDestroyed
│ = false
│ ViewRootImpl#mView is not null
│ mWindowAttributes.mTitle = "com.someapp.uat/com.someapp.
│ someapp.ui.home.HomeActivity"
│ mWindowAttributes.type = 1
│ ↓ ViewRootImpl.mAttachInfo
│ ~~~~~~~~~~~
├─ android.view.View$AttachInfo instance
│ Leaking: UNKNOWN
│ Retaining 678.8 kB in 11728 objects
│ ↓ View$AttachInfo.mTreeObserver
│ ~~~~~~~~~~~~~
├─ android.view.ViewTreeObserver instance
│ Leaking: UNKNOWN
│ Retaining 677.5 kB in 11691 objects
│ ↓ ViewTreeObserver.mOnGlobalLayoutListeners
│ ~~~~~~~~~~~~~~~~~~~~~~~~
├─ android.view.ViewTreeObserver$CopyOnWriteArray instance
│ Leaking: UNKNOWN
│ Retaining 677.2 kB in 11677 objects
│ ↓ ViewTreeObserver$CopyOnWriteArray.mData
│ ~~~~~
├─ java.util.ArrayList instance
│ Leaking: UNKNOWN
│ Retaining 677.2 kB in 11675 objects
│ ↓ ArrayList[0]
│ ~~~
├─ com.someapp.common.compose.utils.
│ KeyboardUtilsKt$keyboardVisibilityAsState$1$$ExternalSyntheticLambda0
│ instance
│ Leaking: UNKNOWN
│ Retaining 677.1 kB in 11673 objects
│ ↓ KeyboardUtilsKt$keyboardVisibilityAsState$1$$ExternalSyntheticLambda0.f$0
│ ~~~
├─ androidx.compose.ui.platform.AndroidComposeView instance
│ Leaking: UNKNOWN
│ Retaining 677.1 kB in 11669 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mWindowAttachCount = 1
│ mContext instance of dagger.hilt.android.internal.managers.
│ ViewComponentManager$FragmentContextWrapper, wrapping activity com.
│ someapp.ui.home.HomeActivity with mDestroyed = false
│ ↓ View.mParent
│ ~~~~~~~
╰→ androidx.compose.ui.platform.ComposeView instance
Leaking: YES (ObjectWatcher was watching this because com.
someapp.feature.featurea.ui.createpost.
CreatePostFragment received Fragment#onDestroyView() callback (references
to its views should be cleared to prevent leaks))
Retaining 1.6 kB in 29 objects
key = ccfc3149-9a0c-464a-928a-8329be9aa408
watchDurationMillis = 12212
retainedDurationMillis = 7209
View not part of a window view hierarchy
View.mAttachInfo is null (view detached)
View.mWindowAttachCount = 1
mContext instance of dagger.hilt.android.internal.managers.
ViewComponentManager$FragmentContextWrapper, wrapping activity com.
someapp.ui.home.HomeActivity with mDestroyed = false
METADATA
Build.VERSION.SDK_INT: 33
Build.MANUFACTURER: samsung
LeakCanary version: 2.12
App process name: com.someapp.uat
Class count: 35246
Instance count: 261168
Primitive array count: 171598
Object array count: 39671
Thread count: 85
Heap total bytes: 40863658
Bitmap count: 30
Bitmap total bytes: 21878570
Large bitmap count: 0
Large bitmap total bytes: 0
Db 1: open /data/user/0/com.someapp.uat/databases/com.google.android.
datatransport.events
Db 2: closed /data/user/0/com.someapp.
uat/databases/google_app_measurement_local.db
Db 3: open /data/user/0/com.someapp.uat/databases/someapp_db
Stats: LruCache[maxSize=3000,hits=133609,misses=253576,hitRate=34%]
RandomAccess[bytes=12798605,reads=253576,travel=112908047953,range=43409570,size
=57885308]
Analysis duration: 411616 ms
I was just wondering why I'm still getting this leak even if I specifically remove the listener during onDispose. Anyone experienced something similar?