Skip to content

Commit 331293a

Browse files
fix: 2 memory leaks (#966)
## 📜 Description <!-- Describe your changes in detail --> This PR fixes 3 memory leaks: - first is connected to `ModalAttachedWatcher` keeping itself as a listener in `EventDispatcher`, everytime the `EdgeToEdgeReactViewGroup` sets itself to `active`. I fixed it with calling `setActive(false)` in `onDropViewInstance` of its manager. - second is connected to `EdgeToEdgeReactViewGroup` calling `setupWindowDimensionsListener` and not detaching it ever. It is a singleton listener, so I attach it once when the first `EdgeToEdgeReactViewGroup` instance is created in `createViewInstance` and remove it in `invalidate` of the whole managers module. - ~~third is connected to `EdgeToEdgeReactViewGroup` also adding a listener on `rootView` each time when calling `setupWindowInsets` and never detaching it, so we detach it in `onDropViewInstance`.~~ <- will be fixed later on in a separate PR ## 💡 Motivation and Context Fix memory leaks happening e.g. on reload. <!-- Why is this change required? What problem does it solve? --> <!-- If it fixes an open issue, please link to the issue here. --> ## 📢 Changelog ### Android - Add proper detaching on listeners when views or whole modules are destroyed ## 🤔 How Has This Been Tested? Tested in Expensify with hitting reload. <!-- Please describe in detail how you tested your changes. --> <!-- Include details of your testing environment, and the tests you ran to --> <!-- see how your change affects other areas of the code, etc. --> ## 📸 Screenshots (if appropriate): <!-- Add screenshots/video if needed --> <!-- That would be highly appreciated if you can add how it looked before and after your changes --> ## 📝 Checklist - [x] CI successfully passed - [x] I added new mocks and corresponding unit-tests if library API was changed --------- Co-authored-by: kirillzyusko <[email protected]>
1 parent 98df8d8 commit 331293a

File tree

6 files changed

+67
-24
lines changed

6 files changed

+67
-24
lines changed

android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardControllerViewManager.kt

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,21 @@ class KeyboardControllerViewManager(
1818
private val manager = KeyboardControllerViewManagerImpl(mReactContext)
1919
private val mDelegate = KeyboardControllerViewManagerDelegate(this)
2020

21-
override fun getDelegate(): ViewManagerDelegate<ReactViewGroup> = mDelegate
22-
23-
override fun getName(): String = KeyboardControllerViewManagerImpl.NAME
24-
21+
// region Lifecycle
2522
override fun createViewInstance(context: ThemedReactContext): ReactViewGroup = manager.createViewInstance(context)
2623

24+
override fun invalidate() {
25+
super.invalidate()
26+
manager.invalidate()
27+
}
28+
2729
override fun onAfterUpdateTransaction(view: ReactViewGroup) {
2830
super.onAfterUpdateTransaction(view)
2931
manager.setEdgeToEdge(view as EdgeToEdgeReactViewGroup)
3032
}
33+
// endregion
3134

35+
// region Props setters
3236
@ReactProp(name = "statusBarTranslucent")
3337
override fun setStatusBarTranslucent(
3438
view: ReactViewGroup,
@@ -52,7 +56,14 @@ class KeyboardControllerViewManager(
5256
view: ReactViewGroup,
5357
value: Boolean,
5458
) = manager.setEnabled(view as EdgeToEdgeReactViewGroup, value)
59+
// endregion
5560

61+
// region Getters
5662
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> =
5763
manager.getExportedCustomDirectEventTypeConstants()
64+
65+
override fun getDelegate(): ViewManagerDelegate<ReactViewGroup> = mDelegate
66+
67+
override fun getName(): String = KeyboardControllerViewManagerImpl.NAME
68+
// endregion
5869
}

android/src/main/java/com/reactnativekeyboardcontroller/extensions/ThemedReactContext.kt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,8 @@ import com.facebook.react.uimanager.ThemedReactContext
1010
import com.facebook.react.uimanager.UIManagerHelper
1111
import com.facebook.react.uimanager.events.Event
1212
import com.facebook.react.uimanager.events.EventDispatcher
13-
import com.reactnativekeyboardcontroller.listeners.WindowDimensionListener
1413
import com.reactnativekeyboardcontroller.log.Logger
1514

16-
fun ThemedReactContext.setupWindowDimensionsListener() {
17-
WindowDimensionListener(this)
18-
}
19-
2015
fun ThemedReactContext?.dispatchEvent(
2116
viewId: Int,
2217
event: Event<*>,

android/src/main/java/com/reactnativekeyboardcontroller/listeners/WindowDimensionListener.kt

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.reactnativekeyboardcontroller.listeners
22

33
import android.view.ViewGroup
4+
import android.view.ViewTreeObserver
45
import com.facebook.react.bridge.Arguments
56
import com.facebook.react.uimanager.ThemedReactContext
67
import com.reactnativekeyboardcontroller.extensions.content
@@ -16,8 +17,9 @@ class WindowDimensionListener(
1617
private val context: ThemedReactContext?,
1718
) {
1819
private var lastDispatchedDimensions = Dimensions(0.0, 0.0)
20+
private var layoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
1921

20-
init {
22+
public fun attachListener() {
2123
// attach to content view only once per app instance
2224
if (context != null && listenerID != context.hashCode()) {
2325
listenerID = context.hashCode()
@@ -26,12 +28,19 @@ class WindowDimensionListener(
2628

2729
updateWindowDimensions(content)
2830

29-
content?.viewTreeObserver?.addOnGlobalLayoutListener {
30-
updateWindowDimensions(content)
31-
}
31+
layoutListener =
32+
ViewTreeObserver.OnGlobalLayoutListener {
33+
updateWindowDimensions(content)
34+
}
35+
36+
content?.viewTreeObserver?.addOnGlobalLayoutListener(layoutListener)
3237
}
3338
}
3439

40+
public fun detachListener() {
41+
context?.content?.viewTreeObserver?.removeOnGlobalLayoutListener(layoutListener)
42+
}
43+
3544
private fun updateWindowDimensions(content: ViewGroup?) {
3645
if (content == null) {
3746
return

android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardControllerViewManagerImpl.kt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,26 @@ import com.reactnativekeyboardcontroller.events.FocusedInputLayoutChangedEvent
77
import com.reactnativekeyboardcontroller.events.FocusedInputSelectionChangedEvent
88
import com.reactnativekeyboardcontroller.events.FocusedInputTextChangedEvent
99
import com.reactnativekeyboardcontroller.events.KeyboardTransitionEvent
10+
import com.reactnativekeyboardcontroller.listeners.WindowDimensionListener
1011
import com.reactnativekeyboardcontroller.views.EdgeToEdgeReactViewGroup
1112

1213
@Suppress("detekt:UnusedPrivateProperty")
1314
class KeyboardControllerViewManagerImpl(
1415
mReactContext: ReactApplicationContext,
1516
) {
16-
fun createViewInstance(reactContext: ThemedReactContext): EdgeToEdgeReactViewGroup =
17-
EdgeToEdgeReactViewGroup(reactContext)
17+
private var listener: WindowDimensionListener? = null
18+
19+
fun createViewInstance(reactContext: ThemedReactContext): EdgeToEdgeReactViewGroup {
20+
if (listener == null) {
21+
listener = WindowDimensionListener(reactContext)
22+
listener?.attachListener()
23+
}
24+
return EdgeToEdgeReactViewGroup(reactContext)
25+
}
26+
27+
fun invalidate() {
28+
listener?.detachListener()
29+
}
1830

1931
fun setEnabled(
2032
view: EdgeToEdgeReactViewGroup,

android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import com.reactnativekeyboardcontroller.extensions.content
1515
import com.reactnativekeyboardcontroller.extensions.removeSelf
1616
import com.reactnativekeyboardcontroller.extensions.requestApplyInsetsWhenAttached
1717
import com.reactnativekeyboardcontroller.extensions.rootView
18-
import com.reactnativekeyboardcontroller.extensions.setupWindowDimensionsListener
1918
import com.reactnativekeyboardcontroller.listeners.KeyboardAnimationCallback
2019
import com.reactnativekeyboardcontroller.listeners.KeyboardAnimationCallbackConfig
2120
import com.reactnativekeyboardcontroller.log.Logger
@@ -51,7 +50,6 @@ class EdgeToEdgeReactViewGroup(
5150
private val modalAttachedWatcher = ModalAttachedWatcher(this, reactContext, config, ::getKeyboardCallback)
5251

5352
init {
54-
reactContext.setupWindowDimensionsListener()
5553
tag = VIEW_TAG
5654
}
5755

@@ -65,13 +63,13 @@ class EdgeToEdgeReactViewGroup(
6563
return
6664
}
6765

68-
this.setupKeyboardCallbacks()
66+
this.activate()
6967
}
7068

7169
override fun onDetachedFromWindow() {
7270
super.onDetachedFromWindow()
7371

74-
this.removeKeyboardCallbacks()
72+
this.deactivate()
7573
}
7674

7775
override fun onConfigurationChanged(newConfig: Configuration?) {
@@ -189,12 +187,20 @@ class EdgeToEdgeReactViewGroup(
189187
// region State managers
190188
private fun enable() {
191189
this.setupWindowInsets()
192-
this.setupKeyboardCallbacks()
193-
modalAttachedWatcher.enable()
190+
this.activate()
194191
}
195192

196193
private fun disable() {
197194
this.setupWindowInsets()
195+
this.deactivate()
196+
}
197+
198+
private fun activate() {
199+
this.setupKeyboardCallbacks()
200+
modalAttachedWatcher.enable()
201+
}
202+
203+
private fun deactivate() {
198204
this.removeKeyboardCallbacks()
199205
modalAttachedWatcher.disable()
200206
}

android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardControllerViewManager.kt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,21 @@ class KeyboardControllerViewManager(
1313
) : ReactViewManager() {
1414
private val manager = KeyboardControllerViewManagerImpl(mReactContext)
1515

16-
override fun getName(): String = KeyboardControllerViewManagerImpl.NAME
16+
// region Lifecycle
17+
override fun createViewInstance(context: ThemedReactContext): ReactViewGroup = manager.createViewInstance(context)
1718

18-
override fun createViewInstance(reactContext: ThemedReactContext): EdgeToEdgeReactViewGroup =
19-
manager.createViewInstance(reactContext)
19+
override fun invalidate() {
20+
super.invalidate()
21+
manager.invalidate()
22+
}
2023

2124
override fun onAfterUpdateTransaction(view: ReactViewGroup) {
2225
super.onAfterUpdateTransaction(view)
2326
manager.setEdgeToEdge(view as EdgeToEdgeReactViewGroup)
2427
}
28+
// endregion
2529

30+
// region Props setters
2631
@ReactProp(name = "enabled")
2732
fun setEnabled(
2833
view: EdgeToEdgeReactViewGroup,
@@ -54,7 +59,12 @@ class KeyboardControllerViewManager(
5459
) {
5560
manager.setPreserveEdgeToEdge(view, isPreservingEdgeToEdge)
5661
}
62+
// endregion
5763

64+
// region Getters
5865
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> =
5966
manager.getExportedCustomDirectEventTypeConstants()
67+
68+
override fun getName(): String = KeyboardControllerViewManagerImpl.NAME
69+
// endregion
6070
}

0 commit comments

Comments
 (0)