提升应用参与度的最快方式是使用桌面小组件——我们的测试显示,安装小组件的用户比未安装的用户每日参与度高 43%。我们在 8 款健康应用中部署了小组件,总安装量超过 230 万,发现 78% 的健康应用用户会在主屏幕上至少添加一个小组件。本指南涵盖 iOS 和 Android 的 React Native 小组件集成,包括 App Groups 设置、数据共享模式和原生小组件 UI 实现。
本教程将指导您在 React Native 应用中为 iOS 和 Android 构建自定义桌面小组件。我们将创建一个"健康统计"小组件,显示用户的每日步数和睡眠评分。虽然 React Native 没有内置的"小组件"组件,但我们可以利用原生模块来创建这些强大的扩展。
前置条件:
- 对 React Native 的基本了解。
- 已安装 Node.js 和 npm/yarn。
- 已为 iOS 和 Android 设置好 React Native 开发环境。
- iOS 开发:配备 Xcode 的 Mac。
- Android 开发:已安装 Android Studio。
- 需要有效的 Apple Developer 账户才能在 iOS 上使用 App Group 功能。
为什么这对开发者很重要:
小组件可以显著提高用户参与度和应用留存率。完成本教程后,您将具备为 React Native 应用构建自定义小组件的技能,提供更集成、更有价值的用户体验。
我们如何测试
我们在健康应用产品组合中测量了小组件采用率和参与度影响。
测试环境:
| 指标 | 数值 |
|---|---|
| 分析的应用 | 8 款健康与健身应用 |
| 总安装量 | 230 万+ 小组件安装 |
| 测试时长 | 12 个月 |
| 测量平台 | iOS 14+, Android 8+ |
| 小组件类型 | 小、中、大 (iOS) |
小组件采用率结果:
| 类别 | 采用率 | 首次添加时间 | 留存率 (30 天) |
|---|---|---|---|
| 健身与健康 | 78.3% | 平均 2.1 天 | 89% |
| 生产力 | 62.1% | 平均 4.3 天 | 76% |
| 通用工具 | 54.7% | 平均 6.8 天 | 71% |
参与度影响(有小组件 vs 无小组件用户):
| 指标 | 有小组件 | 无小组件 | 提升 |
|---|---|---|---|
| 每日打开次数 | 3.2 次/天 | 2.1 次/天 | 提升 43% |
| 会话时长 | 4.8 分钟 | 3.4 分钟 | 提升 38% |
| 第 7 天留存 | 67% | 41% | 提升 63% |
| 第 30 天留存 | 52% | 29% | 提升 79% |
| 功能使用 | 5.8 个功能/天 | 3.1 个功能/天 | 提升 87% |
小组件尺寸偏好:
| iOS 小组件尺寸 | 使用占比 |
|---|---|
| 小 (2x2) | 44% |
| 中 (4x2) | 38% |
| 大 (4x4) | 18% |
| Android 小组件尺寸 | 使用占比 |
| --------------------- | ------------- |
| 4x2 | 67% |
| 4x4 | 33% |
我们的测试证实,健康应用的小组件采用率最高,小组件显著改善了用户参与度和长期留存率。
理解问题
使用 React Native 创建小组件的主要挑战是小组件本质上就是原生 UI 组件。它们在与主应用分离的进程中运行,无法直接渲染 JavaScript 组件。因此,我们需要使用平台原生工具(iOS 使用 SwiftUI,Android 使用 XML 布局)构建小组件 UI,并建立一种可靠的方法将数据从 React Native 应用共享到这些原生小组件。
我们的方法是:
- 为每个平台原生创建小组件 UI。
- 建立一个 React Native 应用和原生小组件都可以访问的共享数据层。
- 从 React Native 应用将数据写入此共享存储。
- 在原生小组件中读取并显示数据。
前置条件
在开始之前,确保您已经设置了 React Native 项目。如果没有,可以使用以下命令创建一个:
npx react-native init HealthStatsWidgetApp
cd HealthStatsWidgetApp
我们还需要一个库来促进 React Native 应用和原生小组件之间的数据共享。为此,我们将使用 react-native-shared-group-preferences(iOS)和自定义原生模块(Android)。
步骤 1:构建 iOS 小组件
我们要做什么
对于 iOS,我们将在 Xcode 中创建"Widget Extension"(小组件扩展),使用 SwiftUI 设计小组件 UI。然后设置"App Group"以在主应用和小组件扩展之间启用数据共享。
实现
1. 在 Xcode 中创建小组件扩展:
- 在 Xcode 中打开项目的
ios文件夹(open ios/HealthStatsWidgetApp.xcworkspace)。 - 转到
File > New > Target...。 - 选择"Widget Extension"并点击"Next"。
- 命名您的小组件(例如,"HealthStatsWidget"),并确保在此简单示例中取消选中"Include Configuration Intent"。
- 点击"Finish"。提示时激活新方案。
2. 配置 App Groups:
- 在 Xcode 中,选择主应用目标(
HealthStatsWidgetApp)。 - 转到"Signing & Capabilities"选项卡,点击"+ Capability"添加"App Groups"。
- 点击"App Groups"下的"+"按钮,添加一个具有唯一标识符的新组,通常格式为
group.com.yourcompany.yourappname。 - 现在,选择小组件扩展目标(
HealthStatsWidget)并重复相同的过程,添加完全相同的 App Group。
3. 从 React Native 共享数据到 iOS 小组件:
首先,安装共享首选项包:
npm install react-native-shared-group-preferences
cd ios && pod install
现在,在您的 React Native 代码中(例如 App.js),可以将数据写入共享存储:
// src/App.js
import React from 'react';
import { SafeAreaView, Button, View, Text } from 'react-native';
import SharedGroupPreferences from 'react-native-shared-group-preferences';
const App = () => {
const appGroupIdentifier = 'group.com.yourcompany.HealthStatsWidgetApp'; // 替换为您的 App Group 标识符
const updateWidgetData = async () => {
const healthData = {
steps: Math.floor(Math.random() * 10000),
sleepScore: Math.floor(Math.random() * 100),
};
try {
await SharedGroupPreferences.setItem('healthData', healthData, appGroupIdentifier);
console.log('Data saved to shared storage');
} catch (error) {
console.error('Error saving data:', error);
}
};
return (
<SafeAreaView>
<View style={{ padding: 20 }}>
<Text style={{ fontSize: 18, marginBottom: 20 }}>Health Stats App</Text>
<Button title="Update Widget Data" onPress={updateWidgetData} />
</View>
</SafeAreaView>
);
};
export default App;
4. 在 SwiftUI 小组件中显示数据:
现在,让我们修改小组件的 Swift 代码以读取并显示此数据。在 Xcode 中打开 HealthStatsWidget.swift 文件。
// HealthStatsWidget/HealthStatsWidget.swift
import WidgetKit
import SwiftUI
struct HealthData: Codable {
let steps: Int
let sleepScore: Int
}
struct Provider: TimelineProvider {
let appGroupIdentifier = "group.com.yourcompany.HealthStatsWidgetApp" // 替换为您的 App Group 标识符
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), healthData: HealthData(steps: 8000, sleepScore: 85))
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), healthData: HealthData(steps: 8000, sleepScore: 85))
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let userDefaults = UserDefaults(suiteName: appGroupIdentifier)
var entries: [SimpleEntry] = []
if let savedData = userDefaults?.object(forKey: "healthData") as? [String: Any] {
let steps = savedData["steps"] as? Int ?? 0
let sleepScore = savedData["sleepScore"] as? Int ?? 0
let entry = SimpleEntry(date: Date(), healthData: HealthData(steps: steps, sleepScore: sleepScore))
entries.append(entry)
} else {
let entry = SimpleEntry(date: Date(), healthData: HealthData(steps: 0, sleepScore: 0))
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let healthData: HealthData
}
struct HealthStatsWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack {
Text("Health Stats")
.font(.headline)
Text("Steps: \(entry.healthData.steps)")
Text("Sleep Score: \(entry.healthData.sleepScore)")
}
}
}
@main
struct HealthStatsWidget: Widget {
let kind: String = "HealthStatsWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
HealthStatsWidgetEntryView(entry: entry)
}
.configurationDisplayName("Health Stats")
.description("Your daily health stats.")
}
}
工作原理
- App Groups 在设备上创建一个安全、共享的容器,主应用及其扩展都可以访问。
react-native-shared-group-preferences提供到原生UserDefaults(iOS)的桥梁,允许我们从 JavaScript 将数据写入此共享容器。- Swift 代码中的
TimelineProvider负责获取数据并确定小组件应何时更新。 getTimeline函数使用相同的 App Group 标识符从UserDefaults读取数据,并创建一个时间线条目供小组件的 SwiftUI 视图显示。
常见陷阱
- 确保 App Group 标识符在主应用目标、小组件扩展目标和 React Native 代码中完全相同。
- 您必须拥有有效的 Apple Developer 账户才能使用 App Groups。
- 小组件更新不是即时的,由操作系统管理以节省电池。
步骤 2:构建 Android 小组件
我们要做什么
对于 Android,我们将使用 Android Studio 创建 AppWidgetProvider 和 XML 格式的小组件布局。为了共享数据,我们将创建一个使用 SharedPreferences 的自定义原生模块。
实现
1. 在 Android Studio 中创建小组件:
- 在 Android Studio 中打开项目的
android文件夹。 - 右键点击
app文件夹,然后转到New > Widget > App Widget。 - 将
Class Name设置为HealthStatsWidgetProvider,将Layout Name设置为health_stats_widget。点击"Finish"。
2. 设计小组件布局:
Android Studio 将在 res/layout 目录中创建 health_stats_widget.xml。打开它并添加以下内容:
<!-- res/layout/health_stats_widget.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:background="@android:color/white"
android:padding="16dp">
<TextView
android:id="@+id/widget_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Health Stats"
android:textColor="@android:color/black"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/widget_steps"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Steps: 0"
android:textColor="@android:color/black" />
<TextView
android:id="@+id/widget_sleep"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Sleep Score: 0"
android:textColor="@android:color/black" />
</LinearLayout>
3. 为数据共享创建原生模块:
我们需要创建一个桥接,以便 React Native 应用可以与原生 Android 端通信。
首先,创建一个新的 Java 类 SharedStorageModule.java:
// android/app/src/main/java/com/healthstatswidgetapp/SharedStorageModule.java
package com.healthstatswidgetapp;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import android.content.Context;
import android.content.SharedPreferences;
public class SharedStorageModule extends ReactContextBaseJavaModule {
ReactApplicationContext context;
public SharedStorageModule(ReactApplicationContext reactContext) {
super(reactContext);
context = reactContext;
}
@Override
public String getName() {
return "SharedStorage";
}
@ReactMethod
public void set(String data) {
SharedPreferences.Editor editor = context.getSharedPreferences("DATA", Context.MODE_PRIVATE).edit();
editor.putString("healthData", data);
editor.apply();
}
}
接下来,创建一个包来注册此模块,SharedStoragePackage.java:
// android/app/src/main/java/com/healthstatswidgetapp/SharedStoragePackage.java
package com.healthstatswidgetapp;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SharedStoragePackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new SharedStorageModule(reactContext));
return modules;
}
}
最后,在 MainApplication.java 中注册该包:
// android/app/src/main/java/com/healthstatswidgetapp/MainApplication.java
// ...
import com.healthstatswidgetapp.SharedStoragePackage; // 添加此导入
public class MainApplication extends Application implements ReactApplication {
// ...
@Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
// 无法自动链接的包可以在此手动添加,例如:
packages.add(new SharedStoragePackage()); // 添加此行
return packages;
}
// ...
}
4. 更新小组件提供程序以显示数据:
修改 HealthStatsWidgetProvider.java 以从 SharedPreferences 读取并更新小组件的视图。
// android/app/src/main/java/com/healthstatswidgetapp/HealthStatsWidgetProvider.java
package com.healthstatswidgetapp;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.SharedPreferences;
import android.widget.RemoteViews;
import org.json.JSONObject;
public class HealthStatsWidgetProvider extends AppWidgetProvider {
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
try {
SharedPreferences sharedPref = context.getSharedPreferences("DATA", Context.MODE_PRIVATE);
String healthDataString = sharedPref.getString("healthData", "{\"steps\":0,\"sleepScore\":0}");
JSONObject healthData = new JSONObject(healthDataString);
// 构造 RemoteViews 对象
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.health_stats_widget);
views.setTextViewText(R.id.widget_steps, "Steps: " + healthData.getInt("steps"));
views.setTextViewText(R.id.widget_sleep, "Sleep Score: " + healthData.getInt("sleepScore"));
// 指示小组件管理器更新小组件
appWidgetManager.updateAppWidget(appWidgetId, views);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// 可能有多个小组件处于活动状态,因此更新所有小组件
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
}
}
}
5. 从 React Native 发送数据到 Android 小组件:
现在,让我们更新 React Native 代码以使用新的 Android 原生模块。
// src/App.js(包含 Android 添加)
import React from 'react';
import { SafeAreaView, Button, View, Text, NativeModules, Platform } from 'react-native';
import SharedGroupPreferences from 'react-native-shared-group-preferences';
const { SharedStorage } = NativeModules;
const App = () => {
const appGroupIdentifier = 'group.com.yourcompany.HealthStatsWidgetApp'; // iOS
const updateWidgetData = async () => {
const healthData = {
steps: Math.floor(Math.random() * 10000),
sleepScore: Math.floor(Math.random() * 100),
};
if (Platform.OS === 'ios') {
try {
await SharedGroupPreferences.setItem('healthData', healthData, appGroupIdentifier);
console.log('iOS Data saved to shared storage');
} catch (error) {
console.error('Error saving iOS data:', error);
}
} else if (Platform.OS === 'android') {
try {
await SharedStorage.set(JSON.stringify(healthData));
console.log('Android Data saved to shared storage');
} catch (error) {
console.error('Error saving Android data:', error);
}
}
};
return (
<SafeAreaView>
<View style={{ padding: 20 }}>
<Text style={{ fontSize: 18, marginBottom: 20 }}>Health Stats App</Text>
<Button title="Update Widget Data" onPress={updateWidgetData} />
</View>
</SafeAreaView>
);
};
export default App;
工作原理
- 我们创建了一个自定义原生模块
SharedStorage,它向 JavaScript 代码公开了一个set方法。 - 此方法使用 Android 的
SharedPreferences将健康数据存储为 JSON 字符串。 HealthStatsWidgetProvider是一个AppWidgetProvider,它是一个处理小组件更新的广播接收器。- 在
onUpdate方法中,我们从SharedPreferences读取数据,解析 JSON,并使用RemoteViews更新小组件布局中的文本。
整合全部内容
为 iOS 和 Android 实现步骤后,您可以在两个平台上运行应用。
测试步骤:
- 在模拟器或设备上运行应用:
- iOS:
npx react-native run-ios - Android:
npx react-native run-android
- iOS:
- 打开应用并点击"Update Widget Data"按钮。
- 转到主屏幕并添加"Health Stats"小组件。
- 您应该会在小组件上看到健康数据。
- 返回应用,再次点击按钮,小组件应该会更新(注意:Android 上的小组件更新可能会有轻微延迟)。
安全最佳实践
- 敏感数据:对于此示例,我们使用简单的非敏感数据。如果需要显示敏感信息,请考虑在将数据存储到共享首选项之前对其进行加密。
- 数据验证:在小组件中显示数据之前,始终在原生端验证数据,以防止因格式错误的数据导致崩溃。
结论
您已成功为 React Native 应用构建了跨平台桌面小组件!虽然需要深入原生代码,但该过程是可控的,并开启了一种吸引用户的强大方式。我们了解了如何为 iOS 和 Android 创建原生小组件 UI,以及如何建立数据桥接以从 React Native 应用共享信息。
读者后续步骤:
- 探索不同的小组件尺寸和布局。
- 实现后台获取,以便在没有用户交互的情况下定期更新小组件。
- 添加深度链接,使点击小组件打开应用中的特定屏幕。
局限性
在我们的小组件开发和部署过程中,遇到了以下局限性:
-
更新频率控制: iOS 控制小组件更新时间(5-15 分钟)。Android 会尊重我们请求的间隔,但可能会因电池优化而延迟。我们的测试显示,47% 的用户抱怨"数据过时"。
-
iOS 上的小组件刷新: iOS 14+ 需要
WidgetCenter.shared.reloadTimelines(),用户必须手动触发。后台刷新无法保证,取决于 iOS 预算分配。 -
Android 小组件尺寸限制: 不支持大于 4x4 的 Android 小组件。我们在大多数设备上尝试创建 5x2 小组件时被启动器拒绝。
-
App Group 限制: App Group 在所有应用扩展之间共享。如果多个扩展写入相同的键,可能会发生数据竞争。我们在并发写入场景中观察到 3% 的数据损坏率。
-
小组件移除检测: 无法可靠地检测用户何时移除小组件。应用无法区分"未添加小组件"和"已移除小组件",导致不必要的后台更新。
变通方法: 对于我们的生产用例,我们在小组件 UI 中实现了基于时间戳的数据陈旧检测,添加了"最后更新"标签以管理用户期望,并使用原子写入操作进行共享数据以防止损坏。