From 6e7f66fe1a6bbeb04c817cc47a5d7a62947907e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Boro=C5=84?= Date: Fri, 10 Apr 2026 09:57:55 +0200 Subject: [PATCH 1/4] Fix memory leak related to ActionBar --- .../rnscreens/ScreenStackFragment.kt | 35 +++++++ .../rnscreens/ScreenStackHeaderConfig.kt | 2 + apps/src/tests/issue-tests/TestXXXX.tsx | 97 +++++++++++++++++++ apps/src/tests/issue-tests/index.ts | 1 + 4 files changed, 135 insertions(+) create mode 100644 apps/src/tests/issue-tests/TestXXXX.tsx diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt index 5445079091..291025aaf2 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt @@ -14,6 +14,8 @@ import android.view.View import android.view.ViewGroup import android.view.animation.Animation import android.widget.LinearLayout +import androidx.appcompat.app.ActionBar +import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.ViewCompat @@ -57,6 +59,8 @@ class ScreenStackFragment : private var isToolbarShadowHidden = false private var isToolbarTranslucent = false + private var ownedActionBar: ActionBar? = null + private lateinit var sheetTransitionCoordinator: BottomSheetTransitionCoordinator private var lastFocusedChild: View? = null @@ -301,6 +305,37 @@ class ScreenStackFragment : super.onViewCreated(view, savedInstanceState) } + internal fun onActionBarSet(actionBar: ActionBar) { + ownedActionBar = actionBar + } + + override fun onDestroyView() { + // ScreenStackHeaderConfig.onUpdate() calls activity.setSupportActionBar(toolbar) each time + // the top screen updates. AppCompatDelegateImpl stores the resulting ToolbarActionBar in + // its mActionBar field for the lifetime of the activity. When a screen is popped and the new + // top screen does not install a replacement action bar (e.g. headerShown: false), + // the stale ToolbarActionBar — and the entire object graph hanging off the toolbar is never released. + // When this fragment is being removed, we're clearing the activity's support action bar if it + // still belongs to us. This will break the retention chain: + // - AppCompatDelegateImpl.mActionBar + // - ToolbarActionBar.mDecorToolbar + // - ToolbarWidgetWrapper.mToolbar + // - DebugMenuToolbar.config + // - ScreenStackHeaderConfig.mParent + // - Screen.fragment + if (isRemoving) { + ownedActionBar?.let { owned -> + (activity as? AppCompatActivity)?.let { appCompat -> + if (appCompat.supportActionBar === owned) { + appCompat.setSupportActionBar(null) + } + } + } + ownedActionBar = null + } + super.onDestroyView() + } + override fun onCreateAnimation( transit: Int, enter: Boolean, diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt index e76bf173ca..5af7372522 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt @@ -244,6 +244,8 @@ class ScreenStackHeaderConfig( activity.setSupportActionBar(toolbar) // non-null toolbar is set in the line above and it is used here val actionBar = requireNotNull(activity.supportActionBar) + // notify the fragment so it can clear this action bar reference when being removed. + screenFragment?.onActionBarSet(actionBar) // hide back button actionBar.setDisplayHomeAsUpEnabled( diff --git a/apps/src/tests/issue-tests/TestXXXX.tsx b/apps/src/tests/issue-tests/TestXXXX.tsx new file mode 100644 index 0000000000..d448660880 --- /dev/null +++ b/apps/src/tests/issue-tests/TestXXXX.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { View, Text, Button, StyleSheet } from 'react-native'; +import { NavigationContainer, ParamListBase } from '@react-navigation/native'; +import { + createNativeStackNavigator, + NativeStackNavigationProp, +} from '@react-navigation/native-stack'; + +type RouteParamList = { + Home: undefined; + Details: undefined; + Settings: undefined; +}; + +type NavigationProp = { + navigation: NativeStackNavigationProp; +}; + +type StackNavigationProp = NavigationProp; + +const Stack = createNativeStackNavigator(); + +function HomeScreen({ navigation }: StackNavigationProp) { + return ( + + Home Screen +