expo-modules-core 57.0.20 → 57.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,14 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 57.0.21 — 2026-10-06
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - Fixed listeners removed with `subscription.remove()` never being garbage-collected, together with their emitter and everything their closure references, when the closure can reach the subscription (as in `useEvent`, `useEventListener` or a `useEffect` that returns `() => subscription.remove()`). ([#50603](https://github.com/expo/expo/pull/50603) by [@gkueny](https://github.com/gkueny))
18
+ - [iOS] Fixed a crash (`The app context has been lost`) when an Expo view mounts during a reload. ([#50721](https://github.com/expo/expo/pull/50721) by [@tsapeta](https://github.com/tsapeta))
19
+ - [Android] Fixed Expo views using Android layout not completing system-forced layout passes. ([#48679](https://github.com/expo/expo/pull/48679) by [@lujjjh](https://github.com/lujjjh))
20
+
13
21
  ## 57.0.20 — 2026-09-29
14
22
 
15
23
  ### 🐛 Bug fixes
@@ -27,7 +27,7 @@ if (shouldIncludeCompose) {
27
27
  }
28
28
 
29
29
  group = 'host.exp.exponent'
30
- version = '57.0.20'
30
+ version = '57.0.21'
31
31
 
32
32
  def isExpoModulesCoreTests = {
33
33
  Gradle gradle = getGradle()
@@ -94,7 +94,7 @@ android {
94
94
  defaultConfig {
95
95
  consumerProguardFiles 'proguard-rules.pro'
96
96
  versionCode 1
97
- versionName "57.0.20"
97
+ versionName "57.0.21"
98
98
  buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
99
99
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"
100
100
 
@@ -2,7 +2,6 @@ package expo.modules.kotlin.views
2
2
 
3
3
  import android.annotation.SuppressLint
4
4
  import android.content.Context
5
- import android.content.res.Configuration
6
5
  import android.util.Log
7
6
  import android.view.View
8
7
  import android.view.ViewGroup
@@ -114,15 +113,6 @@ abstract class ExpoComposeView<T : ComposeProps>(
114
113
  }
115
114
  }
116
115
 
117
- override fun dispatchConfigurationChanged(newConfig: Configuration) {
118
- super.dispatchConfigurationChanged(newConfig)
119
- // React Native owns this view's bounds and may not schedule another Android layout pass
120
- // when a configuration change leaves its Yoga layout unchanged.
121
- if (withHostingView && isAttachedToWindow && isLaidOut) {
122
- requestLayout()
123
- }
124
- }
125
-
126
116
  /**
127
117
  * Validates that this non-hosting Compose view has a valid Compose parent.
128
118
  *
@@ -35,7 +35,8 @@ abstract class ExpoView(
35
35
 
36
36
  /**
37
37
  * Manually trigger measure and layout.
38
- * If [shouldUseAndroidLayout] is set to `true`, this method will be called automatically after [requestLayout].
38
+ * If [shouldUseAndroidLayout] is set to `true`, this method will be called automatically after
39
+ * [requestLayout] or [forceLayout].
39
40
  */
40
41
  @UiThread
41
42
  fun measureAndLayout() {
@@ -54,6 +55,19 @@ abstract class ExpoView(
54
55
  }
55
56
  }
56
57
 
58
+ override fun forceLayout() {
59
+ super.forceLayout()
60
+ if (shouldUseAndroidLayout && isAttachedToWindow && isLaidOut) {
61
+ // ViewRootImpl can force-layout the hierarchy without React Native laying out its
62
+ // managed children. Preserve the Android-layout contract at that boundary.
63
+ post {
64
+ if (isAttachedToWindow && isLayoutRequested) {
65
+ measureAndLayout()
66
+ }
67
+ }
68
+ }
69
+ }
70
+
57
71
  open fun clipToPaddingBox(canvas: Canvas) {
58
72
  // When the border radius is set, we need to clip the content to the padding box.
59
73
  // This is because the border radius is applied to the background drawable, not the view itself.
@@ -188,10 +188,20 @@ jsi::Value createEventSubscription(jsi::Runtime &runtime, const std::string &eve
188
188
  std::shared_ptr<jsi::Value> listenerValue = std::make_shared<jsi::Value>(runtime, listener);
189
189
 
190
190
  jsi::HostFunctionType removeSubscription = [eventName, emitterValue, listenerValue](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *args, size_t count) -> jsi::Value {
191
+ if (!listenerValue->isObject()) {
192
+ // The subscription has already been removed.
193
+ return jsi::Value::undefined();
194
+ }
191
195
  jsi::Object emitter = emitterValue->getObject(runtime);
192
196
  jsi::Function listener = listenerValue->getObject(runtime).getFunction(runtime);
193
197
 
194
198
  removeListener(runtime, emitter, eventName, listener);
199
+
200
+ // Values held by this host function are GC roots. The listener's closure often references the subscription
201
+ // (e.g. a React effect returning `() => subscription.remove()`), so keeping them after the removal would form
202
+ // a cycle through a root that the garbage collector can never break, leaking the listener and the emitter.
203
+ *emitterValue = jsi::Value::undefined();
204
+ *listenerValue = jsi::Value::undefined();
195
205
  return jsi::Value::undefined();
196
206
  };
197
207
 
@@ -120,17 +120,30 @@ public final class AppContext: NSObject, EXAppContextProtocol, @unchecked Sendab
120
120
  }
121
121
  }
122
122
 
123
- /**
124
- The application identifier that is used to distinguish between different `RCTHost`.
125
- It might be equal to `nil`, meaning we couldn't obtain the Id for the current app.
126
- It shouldn't be used on the old architecture.
127
- */
123
+ /// The number of app contexts created so far in this process.
124
+ private static let appContextsCount = Mutex<Int>(0)
125
+
126
+ /// The position of this app context in creation order, starting at 0.
127
+ internal let appIndex: Int = AppContext.appContextsCount.withLock { count in
128
+ defer {
129
+ count += 1
130
+ }
131
+ return count
132
+ }
133
+
134
+ /// The application identifier that distinguishes app contexts that are alive at the same time,
135
+ /// for example during a reload or when more than one `RCTHost` is running. It's `nil` for the first
136
+ /// app context, so its view names have no suffix.
128
137
  @objc
129
138
  public var appIdentifier: String? {
130
- guard let moduleRegistry = reactBridge?.moduleRegistry else {
139
+ return AppContext.appIdentifier(forIndex: appIndex)
140
+ }
141
+
142
+ internal static func appIdentifier(forIndex index: Int) -> String? {
143
+ if index == 0 {
131
144
  return nil
132
145
  }
133
- return "\(abs(ObjectIdentifier(moduleRegistry).hashValue))"
146
+ return String(index)
134
147
  }
135
148
 
136
149
  /**
@@ -18,7 +18,12 @@
18
18
  @compatibility_alias UIImage NSImage;
19
19
  @compatibility_alias UIImageView NSImageView;
20
20
 
21
- #ifndef UIApplication
21
+ // `@compatibility_alias` doesn't define a preprocessor macro, so `#ifndef
22
+ // UIApplication` never detected that `React/RCTUIKit.h` (imported above)
23
+ // already declares this same alias. Under Swift Explicit Modules, that
24
+ // duplicate declaration becomes a real Clang conflict. Guard on the
25
+ // header's presence instead.
26
+ #if !__has_include(<React/RCTUIKit.h>)
22
27
  @compatibility_alias UIApplication NSApplication;
23
28
  #endif
24
29
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-core",
3
- "version": "57.0.20",
3
+ "version": "57.0.21",
4
4
  "description": "The core of Expo Modules architecture",
5
5
  "main": "src/index.ts",
6
6
  "types": "build/index.d.ts",
@@ -66,7 +66,7 @@
66
66
  "@types/invariant": "^2.2.33",
67
67
  "expo-module-scripts": "56.0.3"
68
68
  },
69
- "gitHead": "ad1efa3fa5802156660198ba5d787f91014d8df3",
69
+ "gitHead": "50ac74f1ab5682c8e32b24da6e5e97dd4dbbb80f",
70
70
  "scripts": {
71
71
  "build": "expo-module build",
72
72
  "clean": "expo-module clean",