From 0b159c61b5c6fa1809e84292794f03bfffb7df51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Sat, 9 Aug 2025 13:16:02 +0200 Subject: [PATCH] fix: Improve Android lifecycle compatibility detection for RN 0.76.x with targetSdk 35 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adopt react-native-screens' resolveReactNativeDirectory approach for consistency - Add automatic React Native version detection (gradle.properties then package.json) - Default to new lifecycle API (v26) for future compatibility - Use old API (v25) only when RN < 0.78 is detected - Provide user override via RNMapboxMapsLifecycleCompat flag - Fixes build failures with RN 0.76.x and targetSdkVersion 35 This resolves the 'Unresolved reference: setViewTreeLifecycleOwner' error reported in #3909 by properly detecting which lifecycle API to use based on the React Native version rather than just targetSdkVersion. Implementation uses the same approach as react-native-screens: https://github.com/software-mansion/react-native-screens/blob/main/android/build.gradle#L83-L127 This ensures consistency across the ecosystem and allows us to benefit from their battle-tested implementation for RN directory resolution. Users can override the automatic detection by setting: ext { RNMapboxMapsLifecycleCompat = 'v25' // or 'v26' // Or specify RN location: REACT_NATIVE_NODE_MODULES_DIR = '/path/to/react-native' } Fixes #3909 🤖 Generated with Claude Code Co-Authored-By: Claude --- android/build.gradle | 125 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 121 insertions(+), 4 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 288e3b6271..0be450b5a8 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -78,13 +78,130 @@ android { } java.srcDirs += 'src/main/rn-compat/rn75' + // Adapted from react-native-screens for consistency + // https://github.com/software-mansion/react-native-screens/blob/main/android/build.gradle + def resolveReactNativeDirectory = { + // 1. User-defined path + def userDefinedRnDirPath = safeExtGet("REACT_NATIVE_NODE_MODULES_DIR", null) + if (userDefinedRnDirPath != null) { + return file(userDefinedRnDirPath) + } + + // 2. Standard monorepo location + def standardRnDirFile = file("$rootDir/../node_modules/react-native/") + if (standardRnDirFile.exists()) { + return standardRnDirFile + } + + // 3. Legacy location + def legacyRnDirFile = file("$projectDir/../node_modules/react-native/") + if (legacyRnDirFile.exists()) { + return legacyRnDirFile + } + + // 4. Try Node resolution as fallback + try { + def maybeRnPackagePath = providers.exec { + workingDir(rootDir) + commandLine("node", "--print", "require.resolve('react-native/package.json')") + }.standardOutput.asText.get().trim() + + if (maybeRnPackagePath != null && !maybeRnPackagePath.isBlank()) { + def maybeRnPackageFile = file(maybeRnPackagePath) + if (maybeRnPackageFile.exists()) { + return maybeRnPackageFile.parentFile + } + } + } catch (Exception e) { + logger.debug("@rnmapbox/maps: Node resolution for react-native failed: ${e.message}") + } + + return null + } + + def detectReactNativeVersion = { + def rnDir = resolveReactNativeDirectory() + if (rnDir == null) { + return null + } + + // Try gradle.properties first (like react-native-screens) + def gradlePropertiesFile = file("$rnDir/ReactAndroid/gradle.properties") + if (gradlePropertiesFile.exists()) { + try { + def reactProperties = new Properties() + gradlePropertiesFile.withInputStream { reactProperties.load(it) } + def version = reactProperties.getProperty("VERSION_NAME") + if (version != null) { + return version + } + } catch (Exception e) { + logger.debug("@rnmapbox/maps: Failed to read gradle.properties: ${e.message}") + } + } + + // Fallback to package.json + def packageJsonFile = file("$rnDir/package.json") + if (packageJsonFile.exists()) { + try { + def packageJson = new groovy.json.JsonSlurper().parseText(packageJsonFile.text) + return packageJson.version + } catch (Exception e) { + logger.debug("@rnmapbox/maps: Failed to read package.json: ${e.message}") + } + } + + return null + } + // Add lifecycle compatibility source sets - // Apps targeting SDK 35+ typically use Lifecycle 2.6+ which changed from getLifecycle() to lifecycle property - def targetSdk = safeExtGet("targetSdkVersion", 28) - if (targetSdk >= 35) { + // Priority order: + // 1. User-defined override (RNMapboxMapsLifecycleCompat) + // 2. React Native version detection (0.78+ uses new API) + // 3. Conservative default (v25 for better compatibility) + + def lifecycleCompat = safeExtGet("RNMapboxMapsLifecycleCompat", null) + + if (lifecycleCompat == "v25" || lifecycleCompat == "old") { + logger.info("@rnmapbox/maps: Using v25 lifecycle compatibility (ViewTreeLifecycleOwner.set) - user override") + java.srcDirs += 'src/main/lifecycle-compat/v25' + } else if (lifecycleCompat == "v26" || lifecycleCompat == "new") { + logger.info("@rnmapbox/maps: Using v26 lifecycle compatibility (setViewTreeLifecycleOwner) - user override") java.srcDirs += 'src/main/lifecycle-compat/v26' } else { - java.srcDirs += 'src/main/lifecycle-compat/v25' + // Auto-detect based on React Native version + def rnVersion = null + try { + rnVersion = detectReactNativeVersion() + } catch (Exception e) { + logger.debug("@rnmapbox/maps: Failed to detect React Native version: ${e.message}") + } + + if (rnVersion != null) { + try { + def versionParts = rnVersion.split("\\.") + def majorVersion = versionParts[0].toInteger() + def minorVersion = versionParts[1].toInteger() + + // React Native < 0.78 needs the old lifecycle API + if (majorVersion == 0 && minorVersion < 78) { + logger.info("@rnmapbox/maps: Detected React Native ${rnVersion} - using v25 lifecycle compatibility (old API)") + java.srcDirs += 'src/main/lifecycle-compat/v25' + } else { + logger.info("@rnmapbox/maps: Detected React Native ${rnVersion} - using v26 lifecycle compatibility (new API)") + java.srcDirs += 'src/main/lifecycle-compat/v26' + } + } catch (Exception e) { + logger.warn("@rnmapbox/maps: Failed to parse React Native version '${rnVersion}', defaulting to v26 compatibility") + java.srcDirs += 'src/main/lifecycle-compat/v26' + } + } else { + // Cannot detect RN version, default to new API (v26) for future compatibility + def targetSdk = safeExtGet("targetSdkVersion", 28) + logger.info("@rnmapbox/maps: Unable to detect React Native version, using v26 lifecycle compatibility (new API, targetSdk=${targetSdk})") + logger.info("@rnmapbox/maps: If you encounter issues with RN < 0.78, set RNMapboxMapsLifecycleCompat='v25' in your app's android/build.gradle ext block") + java.srcDirs += 'src/main/lifecycle-compat/v26' + } } if (safeExtGet("RNMapboxMapsUseV11", false)) {