diff --git a/node_modules/react-native-unistyles/lib/commonjs/web/registry.js b/node_modules/react-native-unistyles/lib/commonjs/web/registry.js index 120ff60..d1ff102 100644 --- a/node_modules/react-native-unistyles/lib/commonjs/web/registry.js +++ b/node_modules/react-native-unistyles/lib/commonjs/web/registry.js @@ -10,6 +10,17 @@ class UnistylesRegistry { stylesheets = new Map(); stylesCache = new Set(); stylesCounter = new Map(); + cleanupGeneration = 0; + finalizerTokens = new WeakMap(); + finalizationRegistry = typeof FinalizationRegistry === 'undefined' ? undefined : new FinalizationRegistry(({ + hash, + generation + }) => { + if (generation !== this.cleanupGeneration) { + return; + } + this.decrementAndMaybeCleanup(hash); + }); disposeListenersMap = new Map(); dependenciesMap = new Map(); constructor(services) { @@ -50,26 +61,48 @@ class UnistylesRegistry { this.disposeListenersMap.set(stylesheet, dispose); }; connect = (ref, hash) => { - const stylesCounter = this.stylesCounter.get(hash) ?? new Set(); - stylesCounter.add(ref); - this.stylesCounter.set(hash, stylesCounter); + this.stylesCounter.set(hash, (this.stylesCounter.get(hash) ?? 0) + 1); + if (!this.finalizationRegistry) { + return; + } + const token = {}; + const tokensByHash = this.finalizerTokens.get(ref) ?? new Map(); + const tokens = tokensByHash.get(hash) ?? []; + tokens.push(token); + tokensByHash.set(hash, tokens); + this.finalizerTokens.set(ref, tokensByHash); + this.finalizationRegistry.register(ref, { + hash, + generation: this.cleanupGeneration + }, token); }; remove = (ref, hash) => { - const stylesCounter = this.stylesCounter.get(hash) ?? new Set(); - stylesCounter.delete(ref); - if (stylesCounter.size === 0) { - // Move this to the end of the event loop so the element is removed from the DOM - return Promise.resolve().then(() => { - // Check if element is still in the DOM - if (document.querySelector(`.${hash}`)) { - return false; - } - this.css.remove(hash); - this.stylesCache.delete(hash); - return true; - }); + const token = this.finalizerTokens.get(ref)?.get(hash)?.pop(); + if (token) { + this.finalizationRegistry?.unregister(token); + } + return this.decrementAndMaybeCleanup(hash); + }; + decrementAndMaybeCleanup = hash => { + const next = (this.stylesCounter.get(hash) ?? 0) - 1; + if (next > 0) { + this.stylesCounter.set(hash, next); + return Promise.resolve(false); } - return Promise.resolve(false); + this.stylesCounter.delete(hash); + // Move this to the end of the event loop so the element is removed from the DOM + return Promise.resolve().then(() => { + // Check if element is still in the DOM + if ((this.stylesCounter.get(hash) ?? 0) > 0) { + return false; + } + if (document.querySelector(`.${hash}`)) { + return false; + } + this.css.remove(hash); + this.stylesCache.delete(hash); + return true; + }); }; add = (value, forChild) => { const generatedHash = (0, _utils.generateHash)(value); @@ -91,7 +124,10 @@ class UnistylesRegistry { this.css.add(hash, value); }; reset = () => { + this.cleanupGeneration += 1; + this.disposeListenersMap.forEach(dispose => dispose()); this.css.reset(); + this.stylesheets.clear(); this.stylesCache.clear(); this.dependenciesMap.clear(); this.disposeListenersMap.clear(); diff --git a/node_modules/react-native-unistyles/lib/module/web/registry.js b/node_modules/react-native-unistyles/lib/module/web/registry.js index 6362f43..8e769f8 100644 --- a/node_modules/react-native-unistyles/lib/module/web/registry.js +++ b/node_modules/react-native-unistyles/lib/module/web/registry.js @@ -6,6 +6,17 @@ export class UnistylesRegistry { stylesheets = new Map(); stylesCache = new Set(); stylesCounter = new Map(); + cleanupGeneration = 0; + finalizerTokens = new WeakMap(); + finalizationRegistry = typeof FinalizationRegistry === 'undefined' ? undefined : new FinalizationRegistry(({ + hash, + generation + }) => { + if (generation !== this.cleanupGeneration) { + return; + } + this.decrementAndMaybeCleanup(hash); + }); disposeListenersMap = new Map(); dependenciesMap = new Map(); constructor(services) { @@ -46,26 +57,48 @@ export class UnistylesRegistry { this.disposeListenersMap.set(stylesheet, dispose); }; connect = (ref, hash) => { - const stylesCounter = this.stylesCounter.get(hash) ?? new Set(); - stylesCounter.add(ref); - this.stylesCounter.set(hash, stylesCounter); + this.stylesCounter.set(hash, (this.stylesCounter.get(hash) ?? 0) + 1); + if (!this.finalizationRegistry) { + return; + } + const token = {}; + const tokensByHash = this.finalizerTokens.get(ref) ?? new Map(); + const tokens = tokensByHash.get(hash) ?? []; + tokens.push(token); + tokensByHash.set(hash, tokens); + this.finalizerTokens.set(ref, tokensByHash); + this.finalizationRegistry.register(ref, { + hash, + generation: this.cleanupGeneration + }, token); }; remove = (ref, hash) => { - const stylesCounter = this.stylesCounter.get(hash) ?? new Set(); - stylesCounter.delete(ref); - if (stylesCounter.size === 0) { - // Move this to the end of the event loop so the element is removed from the DOM - return Promise.resolve().then(() => { - // Check if element is still in the DOM - if (document.querySelector(`.${hash}`)) { - return false; - } - this.css.remove(hash); - this.stylesCache.delete(hash); - return true; - }); + const token = this.finalizerTokens.get(ref)?.get(hash)?.pop(); + if (token) { + this.finalizationRegistry?.unregister(token); + } + return this.decrementAndMaybeCleanup(hash); + }; + decrementAndMaybeCleanup = hash => { + const next = (this.stylesCounter.get(hash) ?? 0) - 1; + if (next > 0) { + this.stylesCounter.set(hash, next); + return Promise.resolve(false); } - return Promise.resolve(false); + this.stylesCounter.delete(hash); + // Move this to the end of the event loop so the element is removed from the DOM + return Promise.resolve().then(() => { + // Check if element is still in the DOM + if ((this.stylesCounter.get(hash) ?? 0) > 0) { + return false; + } + if (document.querySelector(`.${hash}`)) { + return false; + } + this.css.remove(hash); + this.stylesCache.delete(hash); + return true; + }); }; add = (value, forChild) => { const generatedHash = generateHash(value); @@ -87,7 +120,10 @@ export class UnistylesRegistry { this.css.add(hash, value); }; reset = () => { + this.cleanupGeneration += 1; + this.disposeListenersMap.forEach(dispose => dispose()); this.css.reset(); + this.stylesheets.clear(); this.stylesCache.clear(); this.dependenciesMap.clear(); this.disposeListenersMap.clear(); diff --git a/node_modules/react-native-unistyles/src/web/registry.ts b/node_modules/react-native-unistyles/src/web/registry.ts index 39e0d9d..a21ba95 100644 --- a/node_modules/react-native-unistyles/src/web/registry.ts +++ b/node_modules/react-native-unistyles/src/web/registry.ts @@ -9,7 +9,24 @@ import { error, extractUnistyleDependencies, generateHash } from './utils' export class UnistylesRegistry { private readonly stylesheets = new Map, StyleSheet>() private readonly stylesCache = new Set() - private readonly stylesCounter = new Map>() + // Use counter instead of Set to avoid retaining detached DOM nodes + // FR is a fallback when remove() is missed + private readonly stylesCounter = new Map() + // Bumped on reset() so stale FR callbacks are ignored + private cleanupGeneration = 0 + // Per-registration unregister token, so remove() cancels only its own registration + private readonly finalizerTokens = new WeakMap>>() + // FR isn't available on every runtime - fall back to counter-only cleanup + private readonly finalizationRegistry = + typeof FinalizationRegistry === 'undefined' + ? undefined + : new FinalizationRegistry<{ hash: string; generation: number }>(({ hash, generation }) => { + if (generation !== this.cleanupGeneration) { + return + } + + this.decrementAndMaybeCleanup(hash) + }) private readonly disposeListenersMap = new Map() private readonly dependenciesMap = new Map, Set>() readonly css: CSSState @@ -78,33 +95,61 @@ export class UnistylesRegistry { } connect = (ref: HTMLElement, hash: string) => { - const stylesCounter = this.stylesCounter.get(hash) ?? new Set() + this.stylesCounter.set(hash, (this.stylesCounter.get(hash) ?? 0) + 1) - stylesCounter.add(ref) - this.stylesCounter.set(hash, stylesCounter) + if (!this.finalizationRegistry) { + return + } + + const token = {} + const tokensByHash = this.finalizerTokens.get(ref) ?? new Map>() + const tokens = tokensByHash.get(hash) ?? [] + + tokens.push(token) + tokensByHash.set(hash, tokens) + this.finalizerTokens.set(ref, tokensByHash) + this.finalizationRegistry.register(ref, { hash, generation: this.cleanupGeneration }, token) } remove = (ref: HTMLElement, hash: string) => { - const stylesCounter = this.stylesCounter.get(hash) ?? new Set() + const token = this.finalizerTokens.get(ref)?.get(hash)?.pop() - stylesCounter.delete(ref) + if (token) { + this.finalizationRegistry?.unregister(token) + } - if (stylesCounter.size === 0) { - // Move this to the end of the event loop so the element is removed from the DOM - return Promise.resolve().then(() => { - // Check if element is still in the DOM - if (document.querySelector(`.${hash}`)) { - return false - } + return this.decrementAndMaybeCleanup(hash) + } + + private decrementAndMaybeCleanup = (hash: string): Promise => { + const next = (this.stylesCounter.get(hash) ?? 0) - 1 - this.css.remove(hash) - this.stylesCache.delete(hash) + if (next > 0) { + this.stylesCounter.set(hash, next) - return true - }) + return Promise.resolve(false) } - return Promise.resolve(false) + // Drop the empty bucket from the Map + this.stylesCounter.delete(hash) + + // Move this to the end of the event loop so the element is removed from the DOM + return Promise.resolve().then(() => { + // New connect raced in during the microtask + if ((this.stylesCounter.get(hash) ?? 0) > 0) { + return false + } + + // Check if element is still in the DOM + if (document.querySelector(`.${hash}`)) { + return false + } + + this.css.remove(hash) + this.stylesCache.delete(hash) + + return true + }) } add = (value: UnistylesValues, forChild?: boolean) => { @@ -126,7 +171,11 @@ export class UnistylesRegistry { } reset = () => { + // Invalidate pending FR callbacks from this generation + this.cleanupGeneration += 1 + this.disposeListenersMap.forEach((dispose) => dispose()) this.css.reset() + this.stylesheets.clear() this.stylesCache.clear() this.dependenciesMap.clear() this.disposeListenersMap.clear() diff --git a/node_modules/react-native-unistyles/android/build.gradle b/node_modules/react-native-unistyles/android/build.gradle index 195954b..c4ac28c 100644 --- a/node_modules/react-native-unistyles/android/build.gradle +++ b/node_modules/react-native-unistyles/android/build.gradle @@ -38,6 +38,7 @@ if (isNewArchitectureEnabled()) { android { compileSdkVersion safeExtGet('compileSdkVersion', 35) + ndkVersion safeExtGet('ndkVersion', '27.1.12297006') namespace "com.unistyles" defaultConfig { diff --git a/node_modules/react-native-unistyles/src/core/withUnistyles/withUnistyles.tsx b/node_modules/react-native-unistyles/src/core/withUnistyles/withUnistyles.tsx --- a/node_modules/react-native-unistyles/src/core/withUnistyles/withUnistyles.tsx +++ b/node_modules/react-native-unistyles/src/core/withUnistyles/withUnistyles.tsx @@ -28,13 +28,14 @@ return forwardRef, PropsWithUnistyles>((props, ref) => { const narrowedProps = props as PropsWithUnistyles & UnistyleStyles + const { uniProps, ...componentProps } = narrowedProps const styleClassNames = getClassName(narrowedProps.style, true) const contentContainerStyleClassNames = getClassName(narrowedProps.contentContainerStyle) const { proxifiedRuntime, proxifiedTheme } = useProxifiedUnistyles() const { key: mappingsKey, ...mappingsProps } = mappings ? mappings(proxifiedTheme, proxifiedRuntime) : {} - const { key: uniPropsKey, ...unistyleProps } = narrowedProps.uniProps - ? narrowedProps.uniProps(proxifiedTheme, proxifiedRuntime) + const { key: uniPropsKey, ...unistyleProps } = uniProps + ? uniProps(proxifiedTheme, proxifiedRuntime) : {} const emptyStyles = narrowedProps.style @@ -46,7 +47,7 @@ : undefined const combinedProps = { - ...deepMergeObjects(mappingsProps, unistyleProps, props), + ...deepMergeObjects(mappingsProps, unistyleProps, componentProps), ...(narrowedProps.style ? { // Override default component styles with undefined values to reset them diff --git a/node_modules/react-native-unistyles/src/core/withUnistyles/withUnistyles.native.tsx b/node_modules/react-native-unistyles/src/core/withUnistyles/withUnistyles.native.tsx --- a/node_modules/react-native-unistyles/src/core/withUnistyles/withUnistyles.native.tsx +++ b/node_modules/react-native-unistyles/src/core/withUnistyles/withUnistyles.native.tsx @@ -63,6 +63,7 @@ return forwardRef, PropsWithUnistyles>((props, ref) => { const narrowedProps = props as PropsWithUnistyles & UnistyleStyles + const { uniProps, ...componentProps } = narrowedProps const NativeComponent = Component as ComponentType // @ts-ignore we don't know the type of the component @@ -89,15 +90,15 @@ }, [narrowedProps.style, narrowedProps.contentContainerStyle]) const { key: mappingsKey, ...mappingsProps } = mappings ? mappings(proxifiedTheme, proxifiedRuntime) : {} - const { key: uniPropsKey, ...unistyleProps } = narrowedProps.uniProps - ? narrowedProps.uniProps(proxifiedTheme, proxifiedRuntime) + const { key: uniPropsKey, ...unistyleProps } = uniProps + ? uniProps(proxifiedTheme, proxifiedRuntime) : {} const styleSecrets = getSecrets(narrowedProps.style) const contentContainerStyleSecrets = getSecrets(narrowedProps.contentContainerStyle) const finalProps = { - ...deepMergeObjects(mappingsProps, unistyleProps, props), + ...deepMergeObjects(mappingsProps, unistyleProps, componentProps), ...(narrowedProps.style ? { style: styleSecrets.styles, diff --git a/node_modules/react-native-unistyles/lib/module/core/withUnistyles/withUnistyles.js b/node_modules/react-native-unistyles/lib/module/core/withUnistyles/withUnistyles.js --- a/node_modules/react-native-unistyles/lib/module/core/withUnistyles/withUnistyles.js +++ b/node_modules/react-native-unistyles/lib/module/core/withUnistyles/withUnistyles.js @@ -13,6 +13,7 @@ export const withUnistyles = (Component, mappings) => { return /*#__PURE__*/forwardRef((props, ref) => { const narrowedProps = props; + const { uniProps, ...componentProps } = narrowedProps; const styleClassNames = getClassName(narrowedProps.style, true); const contentContainerStyleClassNames = getClassName(narrowedProps.contentContainerStyle); const { @@ -26,10 +27,10 @@ const { key: uniPropsKey, ...unistyleProps - } = narrowedProps.uniProps ? narrowedProps.uniProps(proxifiedTheme, proxifiedRuntime) : {}; + } = uniProps ? uniProps(proxifiedTheme, proxifiedRuntime) : {}; const emptyStyles = narrowedProps.style ? Object.fromEntries(Object.entries(Object.getOwnPropertyDescriptors(narrowedProps.style)).filter(([key]) => !key.startsWith('unistyles') && !key.startsWith('_')).map(([key]) => [key, undefined])) : undefined; const combinedProps = { - ...deepMergeObjects(mappingsProps, unistyleProps, props), + ...deepMergeObjects(mappingsProps, unistyleProps, componentProps), ...(narrowedProps.style ? { // Override default component styles with undefined values to reset them style: emptyStyles diff --git a/node_modules/react-native-unistyles/lib/module/core/withUnistyles/withUnistyles.native.js b/node_modules/react-native-unistyles/lib/module/core/withUnistyles/withUnistyles.native.js --- a/node_modules/react-native-unistyles/lib/module/core/withUnistyles/withUnistyles.native.js +++ b/node_modules/react-native-unistyles/lib/module/core/withUnistyles/withUnistyles.native.js @@ -27,6 +27,7 @@ }; return /*#__PURE__*/forwardRef((props, ref) => { const narrowedProps = props; + const { uniProps, ...componentProps } = narrowedProps; const NativeComponent = Component; // @ts-ignore we don't know the type of the component @@ -57,11 +58,11 @@ const { key: uniPropsKey, ...unistyleProps - } = narrowedProps.uniProps ? narrowedProps.uniProps(proxifiedTheme, proxifiedRuntime) : {}; + } = uniProps ? uniProps(proxifiedTheme, proxifiedRuntime) : {}; const styleSecrets = getSecrets(narrowedProps.style); const contentContainerStyleSecrets = getSecrets(narrowedProps.contentContainerStyle); const finalProps = { - ...deepMergeObjects(mappingsProps, unistyleProps, props), + ...deepMergeObjects(mappingsProps, unistyleProps, componentProps), ...(narrowedProps.style ? { style: styleSecrets.styles } : {}), diff --git a/node_modules/react-native-unistyles/lib/commonjs/core/withUnistyles/withUnistyles.js b/node_modules/react-native-unistyles/lib/commonjs/core/withUnistyles/withUnistyles.js --- a/node_modules/react-native-unistyles/lib/commonjs/core/withUnistyles/withUnistyles.js +++ b/node_modules/react-native-unistyles/lib/commonjs/core/withUnistyles/withUnistyles.js @@ -18,6 +18,7 @@ const withUnistyles = (Component, mappings) => { return /*#__PURE__*/(0, _react.forwardRef)((props, ref) => { const narrowedProps = props; + const { uniProps, ...componentProps } = narrowedProps; const styleClassNames = (0, _getClassname.getClassName)(narrowedProps.style, true); const contentContainerStyleClassNames = (0, _getClassname.getClassName)(narrowedProps.contentContainerStyle); const { @@ -31,10 +32,10 @@ const { key: uniPropsKey, ...unistyleProps - } = narrowedProps.uniProps ? narrowedProps.uniProps(proxifiedTheme, proxifiedRuntime) : {}; + } = uniProps ? uniProps(proxifiedTheme, proxifiedRuntime) : {}; const emptyStyles = narrowedProps.style ? Object.fromEntries(Object.entries(Object.getOwnPropertyDescriptors(narrowedProps.style)).filter(([key]) => !key.startsWith('unistyles') && !key.startsWith('_')).map(([key]) => [key, undefined])) : undefined; const combinedProps = { - ...(0, _utils.deepMergeObjects)(mappingsProps, unistyleProps, props), + ...(0, _utils.deepMergeObjects)(mappingsProps, unistyleProps, componentProps), ...(narrowedProps.style ? { // Override default component styles with undefined values to reset them style: emptyStyles diff --git a/node_modules/react-native-unistyles/lib/commonjs/core/withUnistyles/withUnistyles.native.js b/node_modules/react-native-unistyles/lib/commonjs/core/withUnistyles/withUnistyles.native.js --- a/node_modules/react-native-unistyles/lib/commonjs/core/withUnistyles/withUnistyles.native.js +++ b/node_modules/react-native-unistyles/lib/commonjs/core/withUnistyles/withUnistyles.native.js @@ -32,6 +32,7 @@ }; return /*#__PURE__*/(0, _react.forwardRef)((props, ref) => { const narrowedProps = props; + const { uniProps, ...componentProps } = narrowedProps; const NativeComponent = Component; // @ts-ignore we don't know the type of the component @@ -62,11 +63,11 @@ const { key: uniPropsKey, ...unistyleProps - } = narrowedProps.uniProps ? narrowedProps.uniProps(proxifiedTheme, proxifiedRuntime) : {}; + } = uniProps ? uniProps(proxifiedTheme, proxifiedRuntime) : {}; const styleSecrets = getSecrets(narrowedProps.style); const contentContainerStyleSecrets = getSecrets(narrowedProps.contentContainerStyle); const finalProps = { - ...(0, _utils.deepMergeObjects)(mappingsProps, unistyleProps, props), + ...(0, _utils.deepMergeObjects)(mappingsProps, unistyleProps, componentProps), ...(narrowedProps.style ? { style: styleSecrets.styles } : {}), diff --git a/node_modules/react-native-unistyles/lib/commonjs/web/runtime.js b/node_modules/react-native-unistyles/lib/commonjs/web/runtime.js index 305fa49..5957aa5 100644 --- a/node_modules/react-native-unistyles/lib/commonjs/web/runtime.js +++ b/node_modules/react-native-unistyles/lib/commonjs/web/runtime.js @@ -35,10 +35,17 @@ class UnistylesRuntime { return this.darkMedia; } get colorScheme() { + // Read the scheme through fresh lists, never through lightMedia/darkMedia. Chromium defers + // MediaQueryList change events while the document is hidden and drops the pending event + // of any list whose `matches` was read before the document became visible again, which + // would silence the adaptive-theme listeners registered on those two lists. + if ((0, _utils.isServer)()) { + return _types.ColorScheme.Unspecified; + } switch (true) { - case this.getLightMedia()?.matches: + case window.matchMedia('(prefers-color-scheme: light)').matches: return _types.ColorScheme.Light; - case this.getDarkMedia()?.matches: + case window.matchMedia('(prefers-color-scheme: dark)').matches: return _types.ColorScheme.Dark; default: return _types.ColorScheme.Unspecified; diff --git a/node_modules/react-native-unistyles/lib/module/web/runtime.js b/node_modules/react-native-unistyles/lib/module/web/runtime.js index 7f13a06..c487bee 100644 --- a/node_modules/react-native-unistyles/lib/module/web/runtime.js +++ b/node_modules/react-native-unistyles/lib/module/web/runtime.js @@ -31,10 +31,17 @@ export class UnistylesRuntime { return this.darkMedia; } get colorScheme() { + // Read the scheme through fresh lists, never through lightMedia/darkMedia. Chromium defers + // MediaQueryList change events while the document is hidden and drops the pending event + // of any list whose `matches` was read before the document became visible again, which + // would silence the adaptive-theme listeners registered on those two lists. + if (isServer()) { + return ColorScheme.Unspecified; + } switch (true) { - case this.getLightMedia()?.matches: + case window.matchMedia('(prefers-color-scheme: light)').matches: return ColorScheme.Light; - case this.getDarkMedia()?.matches: + case window.matchMedia('(prefers-color-scheme: dark)').matches: return ColorScheme.Dark; default: return ColorScheme.Unspecified; diff --git a/node_modules/react-native-unistyles/src/web/runtime.ts b/node_modules/react-native-unistyles/src/web/runtime.ts index a16dac0..6519858 100644 --- a/node_modules/react-native-unistyles/src/web/runtime.ts +++ b/node_modules/react-native-unistyles/src/web/runtime.ts @@ -41,8 +41,16 @@ export class UnistylesRuntime { get colorScheme() { + // Read the scheme through fresh lists, never through lightMedia/darkMedia. Chromium defers + // MediaQueryList change events while the document is hidden and drops the pending event + // of any list whose `matches` was read before the document became visible again, which + // would silence the adaptive-theme listeners registered on those two lists. + if (isServer()) { + return ColorScheme.Unspecified + } + switch (true) { - case this.getLightMedia()?.matches: + case window.matchMedia('(prefers-color-scheme: light)').matches: return ColorScheme.Light - case this.getDarkMedia()?.matches: + case window.matchMedia('(prefers-color-scheme: dark)').matches: return ColorScheme.Dark default: return ColorScheme.Unspecified diff --git a/node_modules/react-native-unistyles/android/src/main/java/com/unistyles/NativePlatform+android.kt b/node_modules/react-native-unistyles/android/src/main/java/com/unistyles/NativePlatform+android.kt index 0f8d1f0..154a12a 100644 --- a/node_modules/react-native-unistyles/android/src/main/java/com/unistyles/NativePlatform+android.kt +++ b/node_modules/react-native-unistyles/android/src/main/java/com/unistyles/NativePlatform+android.kt @@ -3,11 +3,16 @@ package com.unistyles import android.os.Build import android.content.Context import android.content.res.Configuration +import android.content.res.Resources +import android.os.Handler +import android.os.Looper import android.util.DisplayMetrics import android.view.View import android.view.WindowManager import androidx.annotation.Keep +import androidx.core.content.OnConfigurationChangedProvider import androidx.core.text.TextUtilsCompat +import androidx.core.util.Consumer import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat @@ -22,6 +27,7 @@ import com.margelo.nitro.unistyles.Insets import com.margelo.nitro.unistyles.Orientation import com.margelo.nitro.unistyles.UnistyleDependency import com.margelo.nitro.unistyles.UnistylesNativeMiniRuntime +import java.lang.ref.WeakReference import java.util.Locale @Keep @@ -30,6 +36,14 @@ class NativePlatformAndroid(private val reactContext: ReactApplicationContext): private val _insets = NativePlatformInsets(reactContext, this::getMiniRuntime) { this.onConfigChange() } private var _miniRuntime: UnistylesNativeMiniRuntime = buildMiniRuntime() private val _listener = NativePlatformListener(reactContext, this::getMiniRuntime) { this.diffMiniRuntime() } + private var _configurationProvider: WeakReference? = null + private val _activityConfigurationListener = Consumer { + Handler(Looper.getMainLooper()).post { this.onConfigChange() } + } + + // Application resources can lag behind configuration changes + private val configurationResources: Resources + get() = reactContext.currentActivity?.resources ?: reactContext.resources init { checkEdgeToEdge() @@ -37,12 +51,16 @@ class NativePlatformAndroid(private val reactContext: ReactApplicationContext): } fun onDestroy() { + stopObservingActivityConfiguration() _listener.onDestroy() _insets.onDestroy() reactContext.removeLifecycleEventListener(this) } override fun onHostResume() { + observeActivityConfiguration() + // catch configuration changes missed while no Activity was attached + onConfigChange() _insets.getInitialInsets(false) _insets.startInsetsListener() } @@ -51,7 +69,26 @@ class NativePlatformAndroid(private val reactContext: ReactApplicationContext): _insets.stopInsetsListener() } - override fun onHostDestroy() {} + override fun onHostDestroy() { + stopObservingActivityConfiguration() + } + + private fun observeActivityConfiguration() { + val provider = reactContext.currentActivity as? OnConfigurationChangedProvider + + if (_configurationProvider?.get() === provider) { + return + } + + stopObservingActivityConfiguration() + provider?.addOnConfigurationChangedListener(_activityConfigurationListener) + _configurationProvider = provider?.let { WeakReference(it) } + } + + private fun stopObservingActivityConfiguration() { + _configurationProvider?.get()?.removeOnConfigurationChangedListener(_activityConfigurationListener) + _configurationProvider = null + } override val memorySize: Long get() = 0 @@ -61,7 +98,7 @@ class NativePlatformAndroid(private val reactContext: ReactApplicationContext): } override fun getColorScheme(): ColorScheme { - val uiMode = reactContext.resources.configuration.uiMode + val uiMode = configurationResources.configuration.uiMode val colorScheme = when (uiMode.and(Configuration.UI_MODE_NIGHT_MASK)) { Configuration.UI_MODE_NIGHT_YES -> ColorScheme.DARK @@ -73,15 +110,15 @@ class NativePlatformAndroid(private val reactContext: ReactApplicationContext): } override fun getFontScale(): Double { - return reactContext.resources.configuration.fontScale.toDouble() + return configurationResources.configuration.fontScale.toDouble() } override fun getPixelRatio(): Double { - return reactContext.resources.displayMetrics.density.toDouble() + return configurationResources.displayMetrics.density.toDouble() } override fun getOrientation(): Orientation { - val orientation = when (reactContext.resources.configuration.orientation) { + val orientation = when (configurationResources.configuration.orientation) { Configuration.ORIENTATION_PORTRAIT -> Orientation.PORTRAIT Configuration.ORIENTATION_LANDSCAPE -> Orientation.LANDSCAPE else -> Orientation.PORTRAIT @@ -122,7 +159,7 @@ class NativePlatformAndroid(private val reactContext: ReactApplicationContext): return Dimensions(screenWidth, screenHeight) } else -> { - val displayMetrics = reactContext.resources.displayMetrics + val displayMetrics = configurationResources.displayMetrics reactContext.currentActivity?.windowManager?.currentWindowMetrics?.bounds?.let { val boundsWidth = (it.width() / displayMetrics.density).toDouble() @@ -157,7 +194,7 @@ class NativePlatformAndroid(private val reactContext: ReactApplicationContext): "com.facebook.react.modules.i18nmanager.I18nUtil", Context.MODE_PRIVATE ) - val resourcesLocale = reactContext.resources.configuration.locales[0] + val resourcesLocale = configurationResources.configuration.locales[0] val hasForcedRtl = sharedPrefs.getBoolean("RCTI18nUtil_forceRTL", false) // user preferences val isRtl = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL diff --git a/node_modules/react-native-unistyles/android/src/main/java/com/unistyles/NativePlatform+insets.kt b/node_modules/react-native-unistyles/android/src/main/java/com/unistyles/NativePlatform+insets.kt index a9c1804..f5a3c20 100644 --- a/node_modules/react-native-unistyles/android/src/main/java/com/unistyles/NativePlatform+insets.kt +++ b/node_modules/react-native-unistyles/android/src/main/java/com/unistyles/NativePlatform+insets.kt @@ -38,7 +38,7 @@ class NativePlatformInsets( } fun getInsets(): Insets { - val density = reactContext.resources.displayMetrics.density + val density = (reactContext.currentActivity?.resources ?: reactContext.resources).displayMetrics.density return Insets( this._insets.top / density,