3. 搭建第一个Android原生模块
本节的目标非常明确:在一个已经初始化的 React Native 工程中,亲手创建一个 Android 原生模块,把 Java/Kotlin 方法通过 ReactContextBaseJavaModule 与 @ReactMethod 暴露给 JavaScript,并在 JS 侧完成一次真实调用。整个过程涉及四个关键对象:RN 工程、原生模块类、Package 注册入口、JS 调用方。它们之间的连接关系决定了模块能否被正确加载。
一、先理解原生模块的加载链路
很多初学者写完模块后调用报错,问题往往不在方法本身,而在注册链路断了。React Native 在 Android 侧通过 ReactPackage 收集所有原生模块,再由 ReactInstanceManager 在启动时实例化并注册到 Catalyst 实例中。JS 侧调用 NativeModules.模块名.方法名 时,实际是通过桥接层找到对应模块实例并触发方法。因此,模块类、Package、Application 注册三者缺一不可。
二、创建原生模块类
在 android/app/src/main/java/com/你的包名/ 下新建 DeviceInfoModule.kt。模块名通过 getName() 返回,这个名字就是 JS 侧访问的键名。下面示例暴露两个方法:getDeviceModel 同步返回设备型号,getBatteryLevel 通过 Promise 异步返回电量百分比。
package com.example.rnmodule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Promise
import android.os.Build
import android.os.BatteryManager
import android.content.Context
class DeviceInfoModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {
override fun getName(): String = "DeviceInfo"
@ReactMethod
fun getDeviceModel(promise: Promise) {
try {
promise.resolve(Build.MODEL)
} catch (e: Exception) {
promise.reject("ERR_MODEL", e)
}
}
@ReactMethod
fun getBatteryLevel(promise: Promise) {
val bm = reactApplicationContext
.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
val level = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
promise.resolve(level)
}
}
这里有两个必须注意的点。第一,@ReactMethod 标注的方法返回值必须是 void,不能直接返回 String 或 Int,所有结果通过 Promise 或 Callback 回传。第二,方法参数类型必须是 React Native 桥接支持的类型,如 String、Int、Boolean、ReadableMap、Promise 等,自定义对象需要先转换。
三、编写并注册 ReactPackage
模块类本身不会自动被 RN 发现,需要放进一个 ReactPackage 的 createNativeModules 列表中。新建 DeviceInfoPackage.kt:
package com.example.rnmodule
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
class DeviceInfoPackage : ReactPackage {
override fun createNativeModules(
reactContext: ReactApplicationContext
): List<NativeModule> = listOf(DeviceInfoModule(reactContext))
override fun createViewManagers(
reactContext: ReactApplicationContext
): List<ViewManager<*, *>> = emptyList()
}
然后在 MainApplication.kt 的 getPackages() 中追加这个 Package。注意 Kotlin 中 packages.add() 与 apply 块的写法:
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
add(DeviceInfoPackage())
}
如果使用 Java 模板,则是在 packages.add(new DeviceInfoPackage()); 处添加。注册完成后必须重新编译安装,热重载不会重新执行 Package 注册逻辑。
四、JS 侧调用与结果判断
在 JS 文件中导入并调用。由于两个方法都返回 Promise,推荐使用 async/await:
import { NativeModules } from 'react-native';
const { DeviceInfo } = NativeModules;
async function loadDeviceInfo() {
if (!DeviceInfo) {
console.warn('DeviceInfo 模块未注册');
return;
}
const model = await DeviceInfo.getDeviceModel();
const battery = await DeviceInfo.getBatteryLevel();
console.log('型号:', model, '电量:', battery + '%');
}
判断是否成功有三个观察点:NativeModules.DeviceInfo 不为 undefined,说明注册链路通了;getDeviceModel() 返回真实机型字符串,说明同步桥接正常;getBatteryLevel() 返回 0–100 的整数,说明异步 Promise 回传正常。若模块为 undefined,优先检查 Package 是否加入 getPackages() 并重新构建;若方法调用后 Promise 一直 pending,检查是否在 @ReactMethod 中忘记 resolve 或 reject。
Log.d("DeviceInfo", "called"),用 adb logcat | grep DeviceInfo 观察原生侧是否被触发,可快速区分是 JS 没调到还是原生没回传。五、常见错误对照
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| NativeModules.DeviceInfo 为 undefined | Package 未注册或未重新构建 | 检查 getPackages(),执行重新编译安装 |
| 方法调用报 “is not a function” | 方法缺少 @ReactMethod 注解 | 补上注解并确认方法为 public |
| Promise 一直 pending | 未调用 resolve/reject | 检查所有分支是否都有回传 |
| 编译报类型不匹配 | 参数或返回值不是桥接支持类型 | 改用 Promise/Callback 或基础类型 |
完成以上步骤后,你就拥有了一个可运行的最小原生模块。它的价值不只是返回机型和电量,而是建立了一条从 JS 到 Android 原生代码的稳定通道。后续更复杂的原生能力,如文件读写、传感器、蓝牙、相机,都可以沿用同一套“模块类 + Package + 注册 + JS 调用”的结构逐步扩展。