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 原生模块加载与调用链路 JS 侧调用 NativeModules.DeviceInfo 桥接层 Catalyst / JSI 转发 原生模块实例 DeviceInfoModule ReactPackage createNativeModules() Application getPackages() 注册 RN 工程 MainApplication.kt 注册顺序:模块类 → Package → Application.getPackages() → 启动时实例化 → JS 可调用 任一环节缺失,NativeModules 中对应模块为 undefined

二、创建原生模块类

在 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 为 undefinedPackage 未注册或未重新构建检查 getPackages(),执行重新编译安装
方法调用报 “is not a function”方法缺少 @ReactMethod 注解补上注解并确认方法为 public
Promise 一直 pending未调用 resolve/reject检查所有分支是否都有回传
编译报类型不匹配参数或返回值不是桥接支持类型改用 Promise/Callback 或基础类型

完成以上步骤后,你就拥有了一个可运行的最小原生模块。它的价值不只是返回机型和电量,而是建立了一条从 JS 到 Android 原生代码的稳定通道。后续更复杂的原生能力,如文件读写、传感器、蓝牙、相机,都可以沿用同一套“模块类 + Package + 注册 + JS 调用”的结构逐步扩展。