Reference version

This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.

Expo 棕地开发

用于将 Expo 集成到现有原生应用中的工具包和 API。

Android
iOS

expo-brownfield 是一个工具包,用于向现有的原生 Android 和 iOS 应用中添加 React Native 视图。它提供:

  • 内置 API,用于原生应用和 React Native 应用之间的双向通信与导航
  • 配置插件,用于在你的 Expo 项目中自动设置 brownfield 目标
  • CLI,用于将构建产物发布到 Maven 仓库(Android)和 XCFrameworks(iOS)。

安装

Terminal
npx expo install expo-brownfield

If you are installing this in an existing React Native app, make sure to install expo in your project.

使用

通信 API

通信 API 使原生(宿主)应用与 React Native 之间能够进行基于消息的双向通信。

从 React Native 向原生发送消息

import * as Brownfield from 'expo-brownfield'; Brownfield.sendMessage({ type: 'MyMessage', data: { language: 'TypeScript', expo: true, platforms: ['android', 'ios'], }, });

在 React Native 中接收来自原生的消息

import * as Brownfield, { type MessageEvent } from 'expo-brownfield'; import { useEffect } from 'react'; function MyComponent() { useEffect(() => { const handleMessage = (event: MessageEvent) => { console.log('收到消息:', event); }; Brownfield.addMessageListener(handleMessage); return () => { Brownfield.removeMessageListener(handleMessage); }; }, []); // ... }

从原生向 React Native 发送消息

import expo.modules.brownfield.BrownfieldMessaging BrownfieldMessaging.sendMessage(mapOf( "type" to "MyAndroidMessage", "timestamp" to System.currentTimeMillis(), "data" to mapOf( "platform" to "android" ) ))

在原生中接收来自 React Native 的消息

import expo.modules.brownfield.BrownfieldMessaging val listenerId = BrownfieldMessaging.addListener { event -> println("来自 React Native 的消息:$event") } // 稍后,移除监听器: BrownfieldMessaging.removeListener(listenerId)

在应用配置中进行配置

expo-brownfield 包提供了一个配置插件,可在使用连续原生生成(CNG)时用于配置 brownfield 集成。此插件允许你自定义 Expo 项目如何被打包并集成到现有的原生应用中。

Example app.json with config plugin

app.json
{ "expo": { "plugins": [ [ "expo-brownfield", { "ios": { "targetName": "MyBrownfieldTarget", "bundleIdentifier": "com.example.brownfield" }, "android": { "group": "com.example", "libraryName": "brownfield", "package": "com.example.brownfield", "version": "1.0.0" } } ] ] } }

Configurable properties

NameDefaultDescription
ios.targetName"<scheme>brownfield" or "<slug>brownfield"
Only for:
iOS

brownfield 集成的 Xcode 目标名称。这将用于在你的 Xcode 项目中为 React Native 代码创建一个单独的目标。

ios.bundleIdentifier"<ios.bundleIdentifier base>.<targetName>" or "com.example.<targetName>"
Only for:
iOS

brownfield 目标的包标识符。这应该是唯一的,并且不同于你的主应用包标识符。

ios.buildReactNativeFromSourcefalse
Only for:
iOS

从源码构建 React Native,而不是使用预构建框架。开启此项会显著增加构建时间。

android.group"<package without last segment>"
Only for:
Android

生成的 Android 库的 Maven group ID。发布该库到 Maven 仓库时会使用它。

android.libraryName"brownfield"
Only for:
Android

生成的 Android 库模块名称。

android.package"<android.package>.brownfield" or "com.example.brownfield"
Only for:
Android

生成的 Android 库代码的 Java/Kotlin 包名。

android.version"1.0.0"
Only for:
Android

生成的 Android 库的版本字符串。发布到 Maven 仓库时会使用它。

android.publishing[{ type: "localMaven" }]
Only for:
Android

生成的 Android 库的发布配置。支持 localMavenlocalDirectoryremotePublicremotePrivate 发布类型。每种类型都有不同的配置选项,用于指定库发布到哪里以及如何发布。

发布构件

expo-brownfield CLI 会将 Android 构件推送到一个或多个 Maven 仓库,这些仓库通过应用配置中的 android.publishing 声明。支持的仓库类型如下:

type描述
localMaven发布到 ~/.m2/repository。默认选项。适用于开发期间在本地主机应用中进行集成。
localDirectory发布到本地文件系统路径(path 字段)。适用于将构件提交到同级 git 仓库。
remotePublic发布到无需身份验证的公共 Maven 仓库。
remotePrivate发布到需要身份验证的远程 Maven 仓库。凭据可以是内联字符串,也可以是环境变量引用。

remotePrivate 上的 urlusernamepassword 接受普通字符串或 EnvValue 对象({ "variable": "SOME_ENV_VAR" }),并通过 Gradle 的 providers.environmentVariable(...) 在构建时解析值。请将密钥固定为环境变量,以免它们出现在提交的应用配置中。

发布到 GitHub Packages

GitHub Packages 托管了一个作用域限定于 GitHub 仓库的私有 Maven 仓库。身份验证使用 GITHUB_ACTOR(用户或机器人名称)以及具有 write:packagesread:packages 权限范围的个人访问令牌(PAT)。在 GitHub Actions 中,工作流的 GITHUB_TOKEN 已经拥有该工作流所属仓库所需的权限范围。

android.publishing 添加一个 remotePrivate 条目,并将其指向测试仓库的 GitHub Packages URL:

app.json
{ "expo": { "plugins": [ [ "expo-brownfield", { "android": { "group": "com.example", "libraryName": "brownfield", "publishing": [ { "type": "localMaven" }, { "type": "remotePrivate", "name": "GitHubPackages", "url": "https://maven.pkg.github.com/<owner>/<repo>", "username": { "variable": "GITHUB_ACTOR" }, "password": { "variable": "GITHUB_TOKEN" } } ] } } ] ] } }

name 字段同时控制 Gradle Maven 仓库名称和 --repo CLI 参数值。使用 "name": "GitHubPackages" 时,发布版本的任务会变为 publishBrownfieldReleasePublicationToGitHubPackagesRepository(调试版本对应的任务为 publishBrownfieldDebugPublicationToGitHubPackagesRepository)。默认调用会同时运行以下任务:

Terminal
npx expo-brownfield build:android --fused --repo GitHubPackages

仅发布融合后的构件

非融合发布流程会为每个自动链接的 Expo Android 模块发布一个 Maven 坐标(每次发布通常为 20 到 40 个构件)。如需改为发布一组精简且经过筛选的构件,请使用 --fused。融合模式会跳过发布插件按模块重新发布的循环,仅发布 fat AAR 兄弟构件。由于没有默认值,因此始终需要指定仓库(--repo)或显式任务(-t):

调用方式发布的坐标
--fused --release --repo <name><group>:<libraryName>-fused-release:<version>
--fused --debug --repo <name><group>:<libraryName>-fused-debug:<version>
--fused --all --repo <name>(使用 --fused 时的默认值)上述两者(底层执行两次 Gradle 调用,每个变体一次)

每次发布最多只会发布两个 Maven 坐标,与项目自动链接的 Expo 模块数量无关。其他所有内容(每个模块的 AAR、传递性 Maven 依赖)都会保留在远程仓库之外。基础库(AndroidX、RN 运行时、Kotlin stdlib、宿主已有的 Glide 替代方案)会继续作为外部依赖保留在 POM 中,而不会融合进 AAR。

融合模式

--fused 会通过两个额外的 Gradle 子项目发布内容,这两个子项目由 prebuild 始终生成 — :<libraryName>-fused-release:<libraryName>-fused-debug — 每个子项目都通过 AGP 的 Fused Library 插件 生成一个 fat AAR(该功能目前处于 Preview 阶段;CLI 会临时强制这些构建使用 AGP 8.13)。这些子项目在普通构建期间不会执行任何操作:只有当 CLI 传入 -Pbrownfield.fused=true 时,它们的构建脚本才会激活,因此 npx expo run:android 和 IDE 同步不会产生额外的配置开销。如果你不通过 CLI、而是直接调用 fused Gradle 任务,请自行传入 -Pbrownfield.fused=true

并非所有内容都会被融合进 AAR。构建会将以下三类依赖保留在外部,并在发布的 POM 和 Gradle Module Metadata 中将它们声明为普通 Maven 依赖,由宿主应用自行解析:

  • brownfield 宿主基线 — React Native 运行时(react-androidhermes-androidfbjnisoloaderyoga)、Kotlin 标准库,以及由宿主提供的 commons(Material Components、Guava、Fresco、OkHttp、Okio)。融合这些内容会与宿主已经携带的类重复。
  • androidx.* — AGP Fused Library 的类重写器无法解析其 styleable 中的 android: 框架属性,因此除非出于验证原因必须融合的链路(默认包括 androidx.cameraandroidx.media3),所有 AndroidX 都会保留在外部。
  • 自动检测到的 KMP 总模块 — 仅存在 pom 的坐标,其所有变体都会重定向到一个平台模块。

发布的元数据还会为 react-androidhermes-android 依赖边添加同级模块的构建类型,因此宿主应用始终会解析与 fused AAR 的原生库进行链接时相同的 React Native 变体,即使宿主应用以 debug 模式使用 release AAR。

当项目的依赖图有相应需求时,可以通过以下五个 Gradle 属性调整此行为:

属性作用
brownfield.fused.skip以逗号分隔的 Gradle 项目名称,这些项目将完全排除在 fat AAR 之外。
brownfield.fused.strip-packages以逗号分隔的包前缀,将从生成的 ExpoModulesPackageList 中移除。与 skip 配合使用,以避免启动时因被跳过的模块而出现 NoClassDefFoundError
brownfield.fused.androidx-fuse额外的 androidx.* 组前缀,将其融合进 AAR,而不是保留在外部。
brownfield.fused.exclude-transitive额外的依赖组,将其保留在外部(在 POM 中声明,而不是融合进 AAR)。
brownfield.fused.host-provided宿主应用已经提供的依赖组(Glide、Compose 等)。与 exclude-transitive 一样,这些依赖不会放入 AAR;但也不会在 POM 中声明,因此不会影响宿主自身使用的版本。

android 目录直接调用 fused 发布任务时,通过 -P Gradle 属性传入这些配置:

Terminal
./gradlew :brownfield-fused-release:publishBrownfieldReleasePublicationToMavenLocal \
-Pbrownfield.fused=true \ -Pbrownfield.fused.skip=expo-camera \ -Pbrownfield.fused.strip-packages=expo.modules.camera.

GitHub Actions 工作流示例

.github/workflows/publish-brownfield.yml
name: Publish brownfield fused AAR on: push: tags: - 'brownfield-v*' workflow_dispatch: permissions: contents: read packages: write jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 - run: npm ci - run: npx expo prebuild --platform android - run: npx expo-brownfield build:android --fused --release --repo GitHubPackages env: GITHUB_ACTOR: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

通过 git tag brownfield-v1.0.0 && git push --tags 触发(或通过 Actions 选项卡手动触发)。工作流内置的 GITHUB_TOKEN 仅限于工作流所属的仓库,因此若要发布到第三方仓库,则需要改用存储在仓库密钥中的 PAT。

消费方设置(宿主 Android 应用)

宿主应用的 settings.gradle.kts(或各模块的 build.gradle.kts)声明相同的 GitHub Packages URL 和凭据。宿主应用的 CI/本地构建必须设置 GITHUB_ACTORGITHUB_TOKEN 环境变量,或从 ~/.gradle/gradle.properties 中读取。

android/settings.gradle.kts
dependencyResolutionManagement { repositories { google() mavenCentral() maven { url = uri("https://maven.pkg.github.com/<owner>/<repo>") credentials { username = providers.environmentVariable("GITHUB_ACTOR").orNull ?: providers.gradleProperty("gpr.user").orNull password = providers.environmentVariable("GITHUB_TOKEN").orNull ?: providers.gradleProperty("gpr.token").orNull } } } }
android/app/build.gradle.kts
dependencies { releaseImplementation("com.example:brownfield-fused-release:1.0.0") debugImplementation("com.example:brownfield-fused-debug:1.0.0") }

releaseImplementation/debugImplementation 会自动配对各个变体。Gradle 会根据宿主构建类型选择匹配的同级变体。如果宿主应用只发布 release 构建,可以删除 debugImplementation 行,并使用 --fused --release 仅发布 release 同级变体。

宿主应用的一些要求和提示:

  • minSdk 必须至少为 24(React Native 的最低要求)。如果宿主应用面向更低的 API 级别,那么在使用 AAR 时会在清单合并阶段失败。
  • 权限会从融合模块合并进来。 例如,与媒体相关的 Expo 模块会声明存储权限。如果宿主应用强制执行权限允许列表,请在宿主清单中使用 tools:node="remove" 移除不需要的条目(或使用 tools:replace 解决属性冲突)。
  • AAR 会携带发布时启用的每个 ABI 的原生库。 如果不进行过滤,宿主 APK 的体积会增加相当于四种 ABI 的 React Native 库大小。可以在发布时限制 ABI(在 Expo 项目的 gradle.properties 中设置 reactNativeArchitectures=arm64-v8a),或在宿主应用中使用 ndk.abiFilters/APK 分包进行过滤。

CLI

expo-brownfield 库包含一个 CLI,用于构建并发布到 Maven 仓库(Android)和 XCFrameworks(iOS)。

Terminal
npx expo-brownfield [command] [options]

命令

build:android

构建并将 brownfield 库及其依赖项发布到 Maven 仓库。

Terminal
npx expo-brownfield build:android [options]
选项描述
-d, --debug以调试模式构建
-r, --release以发布模式构建
-a, --all同时以调试和发布模式构建(默认)
--fused通过 AGP Fused Library 为每个变体发布一个单独的 fat AAR。请参阅融合模式
-l, --library指定 brownfield 库名称
--repo, --repository指定要发布到的 Maven 仓库
-t, --task指定要运行的 Gradle 发布任务
--verbose包含子进程的所有日志

build:ios

构建 brownfield XCFramework,并将 Hermes XCFramework 复制到产物目录。

Terminal
npx expo-brownfield build:ios [options]
选项描述
-d, --debug以调试模式构建
-r, --release以发布模式构建(默认)
-a, --artifacts产物目录的路径(默认:./artifacts
-s, --scheme要构建的 Xcode scheme
-x, --xcworkspaceXcode workspace 路径
-p, --package将产物作为 Swift Package 交付(可选名称)
--verbose包含子进程的所有日志

tasks:android

列出所有可用的发布任务和 Maven 仓库。

Terminal
npx expo-brownfield tasks:android

API

import * as Brownfield from 'expo-brownfield';

Hooks

useSharedState(key, initialValue)

Android
iOS
ParameterTypeDescription
keystring

The key to get the value for.

initialValue(optional)T

The initial value to be used if the shared state is not set.


Hook to observe and set the value of shared state for a given key. Provides a synchronous API similar to useState.

Returns:
[T | undefined, (value: T | (prev: T | undefined) => T) => void]

A tuple containing the value and a function to set the value.

Methods

Brownfield.deleteSharedState(key)

Android
iOS
ParameterTypeDescription
keystring

The key to delete the shared state for.


Deletes the shared state for a given key.

Returns:
void

Brownfield.getMessageListenerCount()

Android
iOS

Gets the number of registered message listeners.

Returns:
number

The number of active message listeners.

Brownfield.getSharedStateValue(key)

Android
iOS
ParameterTypeDescription
keystring

The key to get the value for.


Gets the value of shared state for a given key.

Returns:
T | undefined

Brownfield.popToNative(animated)

Android
iOS
ParameterTypeDescription
animated(optional)boolean

Whether to animate the transition (iOS only). Defaults to false.

Default:false

Navigates back to the native part of the app, dismissing the React Native view.

Returns:
void

Brownfield.sendMessage(message)

Android
iOS
ParameterTypeDescription
messageRecord<string, any>

A dictionary containing the message payload to send to native.


Sends a message to the native side of the app. The message can be received by setting up a listener in the native code.

Returns:
void

Brownfield.setNativeBackEnabled(enabled)

Android
iOS
ParameterTypeDescription
enabledboolean

Whether to enable native back button handling.


Enables or disables the native back button behavior. When enabled, pressing the back button will navigate back to the native part of the app instead of performing the default React Navigation back action.

Returns:
void

Brownfield.setSharedStateValue(key, value)

Android
iOS
ParameterTypeDescription
keystring

The key to set the value for.

valueT

The value to be set.


Sets the value of shared state for a given key.

Returns:
void

Event subscriptions

Brownfield.addMessageListener(listener)

Android
iOS
ParameterTypeDescription
listenerListener<MessageEvent>

A callback function that receives message events from native.


Adds a listener for messages sent from the native side of the app.

Returns:
EventSubscription

A subscription object that can be used to remove the listener.

Example

const subscription = addMessageListener((event) => { console.log('Received message from native:', event); }); // Later, to remove the listener: subscription.remove();

Brownfield.addSharedStateListener(key, callback)

Android
iOS
ParameterTypeDescription
keystring

The key to add the listener for.

callback(event: SharedStateChangeEvent<T> | undefined) => void

The callback to be called when the shared state changes.


Adds a listener for changes to the shared state for a given key.

Returns:
EventSubscription

A subscription object that can be used to remove the listener.

Brownfield.removeAllMessageListeners()

Android
iOS

Removes all message listeners.

Returns:
void

Brownfield.removeMessageListener(listener)

Android
iOS
ParameterTypeDescription
listenerListener<MessageEvent>

The listener function to remove.


Removes a specific message listener.

Returns:
void

Interfaces

EventSubscription

Android
iOS

A subscription object that allows to conveniently remove an event listener from the emitter.

EventSubscription Methods

remove()

Android
iOS

Removes an event listener for which the subscription has been created. After calling this function, the listener will no longer receive any events from the emitter.

Returns:
void

Types

MessageEvent

Android
iOS

Type: Record<string, any>