Expo Widgets 深度指南:用 Expo UI 组件构建 iOS 主屏小组件与 Live Activities
【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo
expo-widgets是 Expo 官方提供的模块,让你直接使用 Expo UI 的 React 组件(而非系统原生 API)来编写 iOS 主屏小组件(WidgetKit)与 Live Activities(ActivityKit),并通过统一的 JavaScript API 管理时间线(timeline)、刷新与推送更新。本篇基于 Expo 仓库中 packages/expo-widgets/README.md 的说明展开,结合模块的 JavaScript 实现、配置插件(config plugin)与原生端源码,梳理其安装方式、核心 API、事件模型与平台差异,帮助你在 managed 与 bare React Native 项目中正确接入并理解其底层机制。
一、模块定位:用 Expo UI 组件写小组件
README 开篇即点明该模块的核心价值:Build iOS home screen widgets and Live Activities using Expo UI components——小组件的 UI 层完全复用 Expo UI 组件体系,而刷新、时间线、推送等生命周期能力由原生端(iOS 的 WidgetKit/ActivityKit,Android 的 AppWidgetProvider)承载。
从仓库结构可以看到模块由四部分构成:
- JavaScript API 层:src/Widgets.ts、src/Widgets.types.ts 等,导出
createWidget、createLiveActivity等函数; - 原生实现层:ios/ 下的 Swift 代码(如 WidgetsModule.swift、TimelineProvider.swift、WidgetLiveActivity.swift),以及 android/src/main/java/expo/modules/widgets/ 下的 Kotlin 代码(如 ExpoWidgetsAppWidgetProvider.kt、WidgetsUpdater.kt);
- 配置插件层:plugin/src/withWidgets.ts 等 config plugin,负责在 prebuild 时把 Widget Target、App Group、推送能力等注入到原生工程;
- 独立 Bundle 层:bundle/ 目录,用于把小组件布局编译成可独立运行的 JS bundle(由原生端的独立 Hermes Runtime 执行,参见 android/src/main/cpp/WidgetsHermesRuntime.cpp 与 ios/Widgets/WidgetsJSRuntime.swift)。
当前仓库中该包版本为57.0.7(见 package.json),依赖@expo/ui与@expo/plist,peer dependencies 为expo、react、react-native。
二、安装方式
README 将安装分为两类项目:
Managed Expo 项目:遵循官方 API 文档中针对最新稳定版的安装说明(README 中给出的 docs.expo.dev 链接,属外部文档,此处不重复输出)。
Bare React Native 项目:必须先安装并配置好expo包,再执行:
npx expo install expo-widgets安装后,若使用 Expo 的 prebuild 流程,还需要在app.json中通过 config plugin 声明 widget。README 虽未给出完整配置示例,但可以从插件源码 plugin/src/withWidgets.ts 得到完整、可复制的配置参数:
{ "plugins": [ [ "expo-widgets", { "bundleIdentifier": "<主App的BundleID>.ExpoWidgetsTarget", "groupIdentifier": "group.<主App的BundleID>", "enablePushNotifications": true, "frequentUpdates": false, "enableAndroid": false, "widgets": [] } ] ] }各参数的语义直接来自 withWidgets.ts 中的类型定义ExpoWidgetsConfigPluginProps:
| 参数 | 默认值 | 说明 |
|---|---|---|
bundleIdentifier | <主App BundleID>.ExpoWidgetsTarget | Widget 独立 Target 的 bundle identifier |
groupIdentifier | group.<主App BundleID> | 主 App 与 Widget 之间通信所用的 App Group 标识 |
enablePushNotifications | false | 是否为 Widget 启用推送通知(Live Activity 远程更新依赖 APNs 推送 token) |
frequentUpdates | false | 是否启用更频繁的时间线更新 |
enableAndroid | false | 是否启用 Android 配置插件;源码注释指出该选项未来会移除、Android widget 将默认启用 |
widgets | [] | WidgetConfig[],声明具体 widget 配置 |
插件执行逻辑也很清晰:先按enableAndroid决定是否运行 Android 插件链(android/withAndroidWidgets.ts),再统一运行 iOS 插件链(ios/withIosWidgets.ts)。iOS 插件链内部还拆分为多个步骤文件,如 withAppGroupEntitlements.ts(写入 App Group 权限)、withPushNotifications.ts(配置推送)、withPodsLinking.ts(CocoaPods 链接)与 Xcode 工程操作工具(xcode/withTargetXcodeProject.ts 等),最终由 app.plugin.js 以expo-widgets插件名对外暴露(插件工厂见 plugin/src/index.ts,使用createRunOncePlugin保证同一插件只执行一次)。
三、JavaScript API:Widget、LiveActivity 与事件
expo-widgets的所有运行时导出集中在 src/index.ts:三类值导出(Widgets.ts中的函数与类)加上 Widgets.types.ts 中的类型定义。
3.1 创建与更新 Widget
核心入口是createWidget(Widgets.ts):
export function createWidget( name: string, // 必须与 app config 中 widget 配置的 'name' 字段一致 widget: (props, context) => React.JSX.Element, // 带 'widget' 指令的布局组件 initialProps?: object ): Widget<PropsType, ConfigurationType>Widget类(Widgets.ts)提供了时间线管理能力:
reload():强制刷新 widget,触发其内容与时间线重载;updateTimeline(entries):按[{ date, props }]批量排程时间线条目。注意平台差异:源码中明确if (Platform.OS === 'android') return;——即该方法仅在 iOS 生效;updateSnapshot(props):立即替换内容而不排程时间线。此处也体现了双端差异:Android 走nativeWidgetObject.updateSnapshot,iOS 则退化为一条timestamp: Date.now()的时间线条目;getTimeline():异步返回当前包含过去与未来条目在内的完整时间线;setConfigurationParameterEnum(parameterName, options):为“动态枚举配置参数”(App Intents 驱动的可选配置)在运行时替换选项,app config 中的值仍作为回退。
3.2 Live Activities:LiveActivityFactory 与 LiveActivity
Live Activity 通过createLiveActivity(name, layout)创建工厂(Widgets.ts),同样要求name与 app config 中 widget 配置的name字段匹配。工厂类LiveActivityFactory(Widgets.ts)提供:
start(props, url?, staleDate?):启动一个新的 Live Activity,url用于深链,staleDate让系统在内容长时间未刷新时降低其视觉强调;getInstances():获取该类型当前所有活动实例。
每个LiveActivity实例(Widgets.ts)提供:
getId():ActivityKit 的稳定标识符;update(props, staleDate?):更新内容,UI 立即反映;end(dismissalPolicy?, props?, contentDate?):结束活动。dismissalPolicy支持'default'、'immediate'或after(date)——after()辅助函数(Widgets.ts)构造一个在指定时间点(4 小时窗口内)从锁屏移除的策略对象,源码中end()会将其展开为'after'+ 时间戳传给原生端;getPushToken():返回用于经 APNs 推送内容更新的推送 token;addPushTokenListener(listener):监听 token 更新事件(底层事件名onExpoWidgetsTokenReceived)。
此外还有两个全局监听函数:addUserInteractionListener(监听按钮点击等交互事件,事件名onExpoWidgetsUserInteraction)与addPushToStartTokenListener(监听可用于远程启动 Live Activity 的 push-to-start token,事件名onExpoWidgetsPushToStartTokenReceived),见 Widgets.ts。最后一个值得注意的导出是widgetsDirectory——一个主 App 与 widget 都可访问的共享目录,常用于存放共享图片。
3.3 环境对象与类型
组件渲染时收到的第二个参数WidgetEnvironment(Widgets.types.ts)暴露了系统环境信息,对锁屏/主屏适配非常有用:
widgetFamily:组件族尺寸,systemSmall(2x2)、systemMedium(4x2)、systemLarge(4x4)、systemExtraLarge(仅 iPad,6x4),以及锁屏专用accessoryCircular、accessoryRectangular、accessoryInline;colorScheme、isLuminanceReduced(iOS 16+,提示你降低亮度渲染)、widgetRenderingMode(fullColor主屏 /accentediOS 18+ 着色 widget /vibrant锁屏)、showsWidgetLabel;widgetContentMargins(iOS 17+ 的内容边距)与levelOfDetail(iOS 26+,simplified/default,系统根据用户距离等推荐简化视图);configuration:widget 配置参数,由 App Intents 驱动(iOS 17+)。
Live Activity 侧有对应的LiveActivityEnvironment,并新增isActivityFullscreen(iOS 16.1+)等字段;ActivityFamily(small/medium)说明同一 Activity 在不同设备上可能渲染为不同族尺寸(iOS 18+)。
四、平台差异与 Web 回退
从源码结构看,该模块的主战场是 iOS:
- iOS 端完整实现了 WidgetKit/ActivityKit 桥接,ios/ 目录包含 20 余个 Swift 文件(如 TimelineEntry.swift、LiveActivityFactory.swift、WidgetsStorage.swift);
- Android 端提供
ExpoWidgetsAppWidgetProvider、WidgetsUpdater、WidgetsJSRuntime等实现(android/src/main/java/expo/modules/widgets/),但需要enableAndroid: true显式开启配置插件,且 JS 侧的部分 API 做了平台区分(如updateTimeline在 Android 上直接返回); - Web/非原生环境下,src/ExpoWidgets.native.ts 的加载机制会回退到 stub 实现(见 ExpoWidgets.ts 中的
WidgetStub、LiveActivityStub等空实现类),保证代码在 Web 平台可以安全 import 而不报错。
这一分层设计(native 模块 + stub 回退)是 Expo 模块的标准模式,意味着你的小组件代码可以无差别地在多平台构建流程中参与编译。
五、原生端如何渲染你的 JSX
一个常被问到的底层问题是:小组件进程独立于主 App,JS 代码如何执行?从仓库结构可以推断出其工作方式:
- 插件与构建脚本把 widget 布局编译为独立 bundle——bundle/ 目录(含 decorator.ts、index.ts 及一系列 stub 文件)配合 layout-registry.metro.config.js 完成布局注册;
- 原生端为 widget 启动独立的 JS Runtime:iOS 见 ios/Widgets/WidgetsJSRuntime.swift,Android 见 android/src/main/cpp/WidgetsHermesRuntime.cpp 及其 JNI 封装 jni/WidgetsHermesRuntime.kt;
- 主 App 与 widget 进程之间通过 App Group 共享存储交换时间线数据:iOS 侧有 WidgetsStorage.swift,Android 侧有 WidgetsStorage.kt,这也解释了为何插件默认会写入
group.<主App BundleID>的 App Group 权限。
六、贡献与延伸阅读
README 末尾指出该模块欢迎贡献,规范遵循 Expo 仓库的贡献指南(对应仓库根目录的 CONTRIBUTING.md)。模块自带 Jest 测试(jest.config.js),bundle 层测试包括 decorator.test.ts 与 jsx-runtime.test.ts,配置插件测试见 plugin/src/tests/。完整的 API 级文档请以官方 SDK 文档(latest stable 与 main 分支两个版本)为准,README 中已给出对应入口;本文则以仓库中 packages/expo-widgets/ 的实际源码为准绳,覆盖其安装、配置、JS API 与平台机制,适合作为在 Expo 项目中落地小组件与 Live Activities 的参考。
【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考