跳至主内容

Android 原生模块

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

信息

原生模块(Native Module)和原生组件(Native Components)是旧架构中使用的稳定技术。它们将在新架构稳定后被弃用。新架构采用 Turbo 原生模块Fabric 原生组件 来实现类似功能。

欢迎来到 Android 原生模块指南。请先阅读原生模块介绍了解原生模块的基本概念。

创建日历原生模块

在本指南中,您将创建一个名为 CalendarModule 的原生模块,用于在 JavaScript 中访问 Android 日历 API。最终,您将能够通过 JavaScript 调用 CalendarModule.createCalendarEvent('Dinner Party', 'My House'); 来触发创建日历事件的 Java/Kotlin 方法。

环境准备

首先在 Android Studio 中打开您 React Native 应用内的 Android 工程。React Native 应用中的 Android 工程位于以下路径:

Image of opening up an Android project within a React Native app inside of Android Studio.
Image of where you can find your Android project

我们推荐使用 Android Studio 来编写原生代码。Android Studio 是专为 Android 开发构建的 IDE,使用它可以帮助你快速解决代码语法错误等小问题。

我们还建议启用 Gradle Daemon 以加速 Java/Kotlin 代码的构建过程。

创建自定义原生模块文件

第一步是在 android/app/src/main/java/com/your-app-name/ 目录下创建 Java/Kotlin 文件(CalendarModule.javaCalendarModule.kt)(Kotlin 和 Java 的目录结构相同)。该文件将包含您的原生模块类。

Image of adding a class called CalendarModule.java within the Android Studio.
Image of how to add the CalendarModuleClass

然后添加以下内容:

java
package com.your-apps-package-name; // replace your-apps-package-name with your app’s package name
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.util.Map;
import java.util.HashMap;

public class CalendarModule extends ReactContextBaseJavaModule {
CalendarModule(ReactApplicationContext context) {
super(context);
}
}

如您所见,CalendarModule 类继承自 ReactContextBaseJavaModule。在 Android 中,Java/Kotlin 原生模块通过扩展 ReactContextBaseJavaModule 并实现 JavaScript 所需功能来实现。

备注

需要注意的是,从技术上讲,Java/Kotlin 类只需继承 BaseJavaModule 类或实现 NativeModule 接口即可被 React Native 视为原生模块。

但我们推荐使用如上所示的 ReactContextBaseJavaModuleReactContextBaseJavaModule 提供了对 ReactApplicationContext(RAC)的访问权限,这对于需要挂钩 Activity 生命周期方法的原生模块非常有用。使用 ReactContextBaseJavaModule 还能让您的原生模块在未来更易实现类型安全。在即将发布的版本中,为了实现原生模块类型安全,React Native 会检查每个原生模块的 JavaScript 规范,并生成继承自 ReactContextBaseJavaModule 的抽象基类。

模块名称

所有 Android 的 Java/Kotlin 原生模块都必须实现 getName() 方法。该方法返回表示原生模块名称的字符串,JavaScript 可通过此名称访问该模块。例如在以下代码片段中,getName() 返回 "CalendarModule"

java
// add to CalendarModule.java
@Override
public String getName() {
return "CalendarModule";
}

这样 JavaScript 中就可以这样访问原生模块:

tsx
const {CalendarModule} = ReactNative.NativeModules;

向 JavaScript 暴露原生方法

接下来您需要添加一个用于创建日历事件的方法,并使其可在 JavaScript 中调用。所有需要从 JavaScript 调用的原生模块方法都必须使用 @ReactMethod 注解。

CalendarModule 设置 createCalendarEvent() 方法,使其可通过 CalendarModule.createCalendarEvent() 在 JS 中调用。目前该方法将接收名称和位置两个字符串参数,参数类型选项将在稍后介绍。

java
@ReactMethod
public void createCalendarEvent(String name, String location) {
}

在方法中添加调试日志,用于确认从应用调用时是否触发。以下是导入和使用 Android util 包中 Log 类的示例:

java
import android.util.Log;

@ReactMethod
public void createCalendarEvent(String name, String location) {
Log.d("CalendarModule", "Create event called with name: " + name
+ " and location: " + location);
}

完成原生模块实现并在 JavaScript 中连接后,您可以按照这些步骤查看应用日志。

同步方法

你可以给原生方法传递 isBlockingSynchronousMethod = true 参数,将其标记为同步方法。

java
@ReactMethod(isBlockingSynchronousMethod = true)

目前我们不推荐这种做法,因为同步调用方法可能导致严重的性能损失,并给原生模块引入线程相关的错误。另外请注意,如果启用 isBlockingSynchronousMethod,应用将无法再使用 Google Chrome 调试器。这是因为同步方法要求 JS 虚拟机与应用共享内存,而在 Google Chrome 调试器中,React Native 实际运行在 Chrome 的 JS 虚拟机里,通过 WebSockets 与移动设备进行异步通信。

注册模块(Android 专有)

原生模块编写完成后,需要向 React Native 注册。你需要将原生模块添加到 ReactPackage 中,然后将该 ReactPackage 注册到 React Native。初始化时,React Native 会遍历所有包,并为每个 ReactPackage 注册其中的原生模块。

React Native 通过调用 ReactPackagecreateNativeModules() 方法获取需要注册的原生模块列表。在 Android 平台上,如果模块未在 createNativeModules 中实例化并返回,JavaScript 将无法使用该模块。

要将原生模块添加到 ReactPackage,首先在 android/app/src/main/java/com/your-app-name/ 目录下创建实现 ReactPackage 的 Java/Kotlin 类(命名为 MyAppPackage.javaMyAppPackage.kt):

然后添加以下内容:

java
package com.your-app-name; // replace your-app-name with your app’s name
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 MyAppPackage 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 CalendarModule(reactContext));

return modules;
}

}

该文件导入了你创建的 CalendarModule 原生模块,在 createNativeModules() 函数中实例化 CalendarModule,并将其作为 NativeModules 列表返回注册。后续添加更多原生模块时,也可在此实例化并加入返回列表。

备注

需要注意的是,这种注册方式会在应用启动时立即初始化所有原生模块,从而增加应用的启动时间。您可以使用 TurboReactPackage 作为替代方案。与返回已实例化原生模块对象列表的 createNativeModules 不同,TurboReactPackage 实现了按需创建原生模块对象的 getModule(String name, ReactApplicationContext rac) 方法。目前 TurboReactPackage 的实现稍显复杂:除了实现 getModule() 方法,您还必须实现 getReactModuleInfoProvider() 方法,该方法返回包可实例化的所有原生模块列表及其实例化函数(示例参见此处)。再次强调,使用 TurboReactPackage 能加快应用启动速度,但目前编写较为繁琐。因此若选择使用 TurboReactPackage,请谨慎行事。

要注册 CalendarModule 包,需将 MyAppPackage 添加到 ReactNativeHost 的 getPackages() 方法返回列表中。打开位于 android/app/src/main/java/com/your-app-name/MainApplication.javaMainApplication.kt 文件:

找到 ReactNativeHost 的 getPackages() 方法,将你的包添加到 getPackages() 返回的包列表中:

java
@Override
protected List<ReactPackage> getPackages() {
List<ReactPackage> packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
packages.add(new MyAppPackage());
return packages;
}

现在你已成功为 Android 注册了原生模块!

测试构建结果

至此你已完成 Android 原生模块的基础搭建。现在通过 JavaScript 访问原生模块并调用其导出方法进行测试。

在应用中选定调用原生模块 createCalendarEvent() 的位置。以下示例组件 NewModuleButton 可添加到应用中,在 NewModuleButtononPress() 函数内调用原生模块:

tsx
import React from 'react';
import {NativeModules, Button} from 'react-native';

const NewModuleButton = () => {
const onPress = () => {
console.log('We will invoke the native module here!');
};

return (
<Button
title="Click to invoke your native module!"
color="#841584"
onPress={onPress}
/>
);
};

export default NewModuleButton;

要从 JavaScript 访问原生模块,首先需要从 React Native 导入 NativeModules

tsx
import {NativeModules} from 'react-native';

然后即可通过 NativeModules 访问 CalendarModule 原生模块。

tsx
const {CalendarModule} = NativeModules;

现在 CalendarModule 原生模块已可用,您可以直接调用原生方法 createCalendarEvent()。以下示例将其添加到 NewModuleButtononPress() 方法中:

tsx
const onPress = () => {
CalendarModule.createCalendarEvent('testName', 'testLocation');
};

最后一步是重新构建 React Native 应用,以获取包含新原生模块的最新原生代码。在 React Native 应用所在命令行中执行:

shell
npm run android

迭代开发时的构建

在按照指南开发并迭代原生模块时,您需要通过原生构建来获取 JavaScript 的最新变更。因为您编写的代码属于应用的原生部分。虽然 React Native 的 Metro 打包器可以监听 JavaScript 变更并实时重建,但这不适用于原生代码。因此要测试最新的原生变更,需要使用上述命令重新构建。

回顾 ✨

现在您应该能在应用中通过原生模块调用 createCalendarEvent() 方法了(本示例中通过点击 NewModuleButton 触发)。可以通过查看您在 createCalendarEvent() 方法中设置的日志来确认调用成功。请遵循 这些步骤 查看 ADB 日志。随后搜索 Log.d 消息(本示例中为"Create event called with name: testName and location: testLocation"),每次调用原生方法时都应看到该日志。

Image of logs.
Image of ADB logs in Android Studio

至此您已完成 Android 原生模块的创建,并在 React Native 应用中通过 JavaScript 调用其原生方法。接下来可继续了解原生模块方法的参数类型、回调函数和 Promise 配置等进阶内容。

日历模块的进阶应用

优化模块导出方式

通过 NativeModules 导入原生模块的方式略显繁琐。

为避免每次使用都需重复此操作,可为模块创建 JavaScript 封装层。新建名为 CalendarModule.js 的 JavaScript 文件并添加以下内容:

tsx
/**
* This exposes the native CalendarModule module as a JS module. This has a
* function 'createCalendarEvent' which takes the following parameters:

* 1. String name: A string representing the name of the event
* 2. String location: A string representing the location of the event
*/
import {NativeModules} from 'react-native';
const {CalendarModule} = NativeModules;
export default CalendarModule;

该 JavaScript 文件也是添加 JavaScript 端功能的理想位置。例如使用 TypeScript 等类型系统时,可在此处添加类型注解。虽然 React Native 暂不支持原生到 JS 的类型安全,但所有 JS 代码均可获得类型保障。这也为将来迁移到类型安全的原生模块奠定基础。以下是添加类型安全的示例:

tsx
/**
* This exposes the native CalendarModule module as a JS module. This has a
* function 'createCalendarEvent' which takes the following parameters:
*
* 1. String name: A string representing the name of the event
* 2. String location: A string representing the location of the event
*/
import {NativeModules} from 'react-native';
const {CalendarModule} = NativeModules;
interface CalendarInterface {
createCalendarEvent(name: string, location: string): void;
}
export default CalendarModule as CalendarInterface;

在其他 JavaScript 文件中可如此访问原生模块并调用其方法:

tsx
import CalendarModule from './CalendarModule';
CalendarModule.createCalendarEvent('foo', 'bar');
备注

此说明假设您导入 CalendarModule 的位置与 CalendarModule.js 处于相同层级,请根据实际情况调整相对导入路径。

参数类型

当在 JavaScript 中调用原生模块方法时,React Native 会将参数从 JS 对象转换为对应的 Java/Kotlin 对象。例如若 Java 原生方法接收 double 类型,在 JS 中调用时需传入 number 类型。React Native 会自动处理类型转换。下表列出原生模块方法支持的参数类型及其对应的 JavaScript 类型:

JavaKotlinJavaScript
BooleanBoolean?boolean
booleanboolean
DoubleDouble?number
doublenumber
StringStringstring
CallbackCallbackFunction
PromisePromisePromise
ReadableMapReadableMapObject
ReadableArrayReadableArrayArray
信息

以下类型当前受支持,但在 TurboModules 中将不再支持,请避免使用:

  • Integer Java/Kotlin -> ?number
  • Float Java/Kotlin -> ?number
  • int Java -> number
  • float Java -> number

对于上表未列出的参数类型,需自行处理转换。例如 Android 中 Date 类型不直接支持转换,可在原生方法内手动处理转换到 Date 类型:

java
    String dateFormat = "yyyy-MM-dd";
SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
Calendar eStartDate = Calendar.getInstance();
try {
eStartDate.setTime(sdf.parse(startDate));
}

导出常量

原生模块可以通过实现 getConstants() 原生方法来导出常量,这些常量在 JavaScript 中可用。下面将实现 getConstants() 并返回一个包含 DEFAULT_EVENT_NAME 常量的 Map,该常量可在 JavaScript 中访问:

java
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();
constants.put("DEFAULT_EVENT_NAME", "New Event");
return constants;
}

然后在 JS 中可通过调用原生模块的 getConstants 方法访问该常量:

tsx
const {DEFAULT_EVENT_NAME} = CalendarModule.getConstants();
console.log(DEFAULT_EVENT_NAME);

从技术上讲,目前可以直接通过原生模块对象访问 getConstants() 导出的常量。但在 TurboModules 中不再支持此方式,因此我们建议开发者采用上述方式以避免后续不必要的迁移。

备注

请注意:常量仅在初始化阶段导出,因此运行时修改 getConstants 返回值不会影响 JavaScript 环境。此行为将在 Turbomodules 中改变——届时 getConstants() 将成为常规原生模块方法,每次调用都会访问原生端。

回调函数

原生模块还支持一种特殊的参数:回调函数。回调用于在异步方法中将数据从 Java/Kotlin 传递到 JavaScript,也可用于从原生端异步执行 JavaScript。

要创建带回调的原生模块方法,首先导入 Callback 接口,然后在原生模块方法中添加类型为 Callback 的新参数。回调参数有两个需要注意的特性(这些特性将在 TurboModules 中调整):首先,函数参数中最多只能有两个回调——successCallback 和 failureCallback;其次,如果原生模块方法调用的最后一个参数是函数,则视为成功回调,倒数第二个参数如果是函数则视为失败回调。

java
import com.facebook.react.bridge.Callback;

@ReactMethod
public void createCalendarEvent(String name, String location, Callback callBack) {
}

您可以在 Java/Kotlin 方法中调用回调,传递任何想要发送到 JavaScript 的数据。请注意,只能传递可序列化数据。如需传递原生对象可使用 WriteableMaps,集合数据可使用 WritableArrays。重点强调:回调不会在原生函数完成后立即调用。下面将之前调用创建的事件ID传递给回调。

java
  @ReactMethod
public void createCalendarEvent(String name, String location, Callback callBack) {
Integer eventId = ...
callBack.invoke(eventId);
}

然后在 JavaScript 中通过以下方式调用该方法:

tsx
const onPress = () => {
CalendarModule.createCalendarEvent(
'Party',
'My House',
eventId => {
console.log(`Created a new event with id ${eventId}`);
},
);
};

另一个重要细节:原生模块方法只能调用一次回调(无论是成功还是失败回调),且每个回调最多只能调用一次。但原生模块可以存储回调并在稍后调用。

回调的错误处理有两种常用方式。第一种遵循 Node 规范,将回调的第一个参数视为错误对象:

java
  @ReactMethod
public void createCalendarEvent(String name, String location, Callback callBack) {
Integer eventId = ...
callBack.invoke(null, eventId);
}

在 JavaScript 中可检查第一个参数判断是否传递了错误:

tsx
const onPress = () => {
CalendarModule.createCalendarEvent(
'testName',
'testLocation',
(error, eventId) => {
if (error) {
console.error(`Error found! ${error}`);
}
console.log(`event id ${eventId} returned`);
},
);
};

另一种方式是使用独立的 onSuccess 和 onFailure 回调:

java
@ReactMethod
public void createCalendarEvent(String name, String location, Callback myFailureCallback, Callback mySuccessCallback) {
}

然后在 JavaScript 中为错误和成功响应分别添加回调:

tsx
const onPress = () => {
CalendarModule.createCalendarEvent(
'testName',
'testLocation',
error => {
console.error(`Error found! ${error}`);
},
eventId => {
console.log(`event id ${eventId} returned`);
},
);
};

Promise

原生模块也可以返回 Promise,这能简化 JavaScript 代码,特别是在使用 ES2016 的 async/await 语法时。当 Java/Kotlin 方法的最后一个参数是 Promise 时,其对应的 JS 方法将返回 JS Promise 对象。

将上述代码从回调重构为使用 Promise 的示例如下:

java
import com.facebook.react.bridge.Promise;

@ReactMethod
public void createCalendarEvent(String name, String location, Promise promise) {
try {
Integer eventId = ...
promise.resolve(eventId);
} catch(Exception e) {
promise.reject("Create Event Error", e);
}
}
备注

与回调类似,原生模块方法可以拒绝 (reject) 或解决 (resolve) Promise(但不能同时进行),且最多执行一次。这意味着您只能调用成功回调或失败回调之一(不可同时调用),且每个回调最多触发一次。但原生模块可以存储回调并在稍后调用。

该方法的 JavaScript 对应版本会返回一个 Promise。这意味着你可以在异步函数中使用 await 关键字调用它并等待结果:

tsx
const onSubmit = async () => {
try {
const eventId = await CalendarModule.createCalendarEvent(
'Party',
'My House',
);
console.log(`Created a new event with id ${eventId}`);
} catch (e) {
console.error(e);
}
};

reject 方法接受以下参数的任意组合:

java
String code, String message, WritableMap userInfo, Throwable throwable

更多细节请参见 Promise.java 接口。如果未提供 userInfo,React Native 会将其设为 null。其余参数 React Native 将使用默认值。message 参数提供错误调用堆栈顶部显示的错误 message。以下是 Java/Kotlin 中 reject 调用在 JavaScript 端显示的错误信息示例。

Java/Kotlin 的 reject 调用:

java
promise.reject("Create Event error", "Error parsing date", e);

Promise 被拒绝时在 React Native 应用中显示的错误信息:

Image of error message in React Native app.
Image of error message

向 JavaScript 发送事件

原生模块无需被直接调用即可向 JavaScript 发出事件信号。例如,你可能希望向 JavaScript 发出提醒:原生 Android 日历应用中的某个日历事件即将发生。最简单的方式是使用 RCTDeviceEventEmitter,可通过 ReactContext 获取,如下所示:

java
...
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.Arguments;
...
private void sendEvent(ReactContext reactContext,
String eventName,
@Nullable WritableMap params) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}

private int listenerCount = 0;

@ReactMethod
public void addListener(String eventName) {
if (listenerCount == 0) {
// Set up any upstream listeners or background tasks as necessary
}

listenerCount += 1;
}

@ReactMethod
public void removeListeners(Integer count) {
listenerCount -= count;
if (listenerCount == 0) {
// Remove upstream listeners, stop unnecessary background tasks
}
}
...
WritableMap params = Arguments.createMap();
params.putString("eventProperty", "someValue");
...
sendEvent(reactContext, "EventReminder", params);

JavaScript 模块随后可以通过 NativeEventEmitter 类的 addListener 方法注册接收事件:

tsx
import {NativeEventEmitter, NativeModules} from 'react-native';
...
useEffect(() => {
const eventEmitter = new NativeEventEmitter(NativeModules.ToastExample);
let eventListener = eventEmitter.addListener('EventReminder', event => {
console.log(event.eventProperty) // "someValue"
});

// Removes the listener once unmounted
return () => {
eventListener.remove();
};
}, []);

从 startActivityForResult 获取 Activity 结果

若要从通过 startActivityForResult 启动的 Activity 获取结果,需要监听 onActivityResult。为此,必须扩展 BaseActivityEventListener 或实现 ActivityEventListener。建议选择前者,因其更能适应 API 变更。接着在模块构造函数中注册监听器:

java
reactContext.addActivityEventListener(mActivityResultListener);

现在可以通过实现以下方法监听 onActivityResult

java
@Override
public void onActivityResult(
final Activity activity,
final int requestCode,
final int resultCode,
final Intent intent) {
// Your logic here
}

让我们实现一个基础图片选择器来演示此功能。该选择器将向 JavaScript 暴露 pickImage 方法,调用时会返回图片路径:

kotlin
public class ImagePickerModule extends ReactContextBaseJavaModule {

private static final int IMAGE_PICKER_REQUEST = 1;
private static final String E_ACTIVITY_DOES_NOT_EXIST = "E_ACTIVITY_DOES_NOT_EXIST";
private static final String E_PICKER_CANCELLED = "E_PICKER_CANCELLED";
private static final String E_FAILED_TO_SHOW_PICKER = "E_FAILED_TO_SHOW_PICKER";
private static final String E_NO_IMAGE_DATA_FOUND = "E_NO_IMAGE_DATA_FOUND";

private Promise mPickerPromise;

private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() {

@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent intent) {
if (requestCode == IMAGE_PICKER_REQUEST) {
if (mPickerPromise != null) {
if (resultCode == Activity.RESULT_CANCELED) {
mPickerPromise.reject(E_PICKER_CANCELLED, "Image picker was cancelled");
} else if (resultCode == Activity.RESULT_OK) {
Uri uri = intent.getData();

if (uri == null) {
mPickerPromise.reject(E_NO_IMAGE_DATA_FOUND, "No image data found");
} else {
mPickerPromise.resolve(uri.toString());
}
}

mPickerPromise = null;
}
}
}
};

ImagePickerModule(ReactApplicationContext reactContext) {
super(reactContext);

// Add the listener for `onActivityResult`
reactContext.addActivityEventListener(mActivityEventListener);
}

@Override
public String getName() {
return "ImagePickerModule";
}

@ReactMethod
public void pickImage(final Promise promise) {
Activity currentActivity = getCurrentActivity();

if (currentActivity == null) {
promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Activity doesn't exist");
return;
}

// Store the promise to resolve/reject when picker returns data
mPickerPromise = promise;

try {
final Intent galleryIntent = new Intent(Intent.ACTION_PICK);

galleryIntent.setType("image/*");

final Intent chooserIntent = Intent.createChooser(galleryIntent, "Pick an image");

currentActivity.startActivityForResult(chooserIntent, IMAGE_PICKER_REQUEST);
} catch (Exception e) {
mPickerPromise.reject(E_FAILED_TO_SHOW_PICKER, e);
mPickerPromise = null;
}
}
}

监听生命周期事件

监听 Activity 的生命周期事件(如 onResume, onPause 等)与实现 ActivityEventListener 类似。模块必须实现 LifecycleEventListener,然后在模块构造函数中注册监听器:

java
reactContext.addLifecycleEventListener(this);

现在可以通过实现以下方法监听 Activity 的生命周期事件:

java
@Override
public void onHostResume() {
// Activity `onResume`
}
@Override
public void onHostPause() {
// Activity `onPause`
}
@Override
public void onHostDestroy() {
// Activity `onDestroy`
}

线程处理

目前,在 Android 上所有原生模块的异步方法都在单一线程执行。原生模块不应预设其被调用的线程类型,因为当前分配方案未来可能变更。若需执行阻塞调用,应将繁重任务分派到内部管理的工作线程,并从该线程分发回调。