4. Android原生模块开发

好,咱们进入正题。这一章我打算聊聊Android原生模块开发。说实话,很多做React Native或Flutter的朋友,一听到「原生」两个字就头大。其实没那么可怕。你想想看,原生模块说白了就是给JS端开了一扇门,让JavaScript能调用Java或Kotlin写的代码。

我个人习惯用Kotlin写新项目,但老项目里Java也常见。所以这一章两种语言我都会提到。咱们从基础开始,一步步来。

4.1 Java/Kotlin基础回顾:够用就行

你不需要成为Java或Kotlin专家。只需要掌握几个核心概念:

  • 类与对象:万物皆对象,模块就是一个类
  • 方法定义:public、private、返回值类型
  • 注解:@ReactMethod这种就是给框架看的标记
  • 线程:原生方法默认跑在JS线程,别阻塞它

我记得刚转做跨平台时,有个同事问我:「Kotlin和Java到底选哪个?」我的建议是:如果你从零开始,直接上Kotlin。语法更简洁,空安全机制能帮你省掉不少NullPointerException的坑。但如果你维护老项目,Java也得会读。

核心原则:原生模块的代码量通常不大,几十行到几百行。你不需要成为语言专家,但得知道怎么写一个类、怎么暴露方法。

4.2 创建Android模块:从零搭架子

创建Android原生模块,说白了就是在你的RN或Flutter项目里,加一个Android Library模块。我一般这么干:

  1. 在项目根目录的 android/app/src/main/java/com/yourcompany/ 下新建一个包
  2. 创建一个Java或Kotlin类,继承自 ReactContextBaseJavaModule
  3. 实现必要的方法,比如 getName()
  4. 注册到 ReactPackage 里

举个例子,假设我们要做一个「设备信息」模块:

// DeviceInfoModule.java
package com.yourcompany.rnmodules;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;

public class DeviceInfoModule extends ReactContextBaseJavaModule {

    public DeviceInfoModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }

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

    @ReactMethod
    public void getDeviceName(Promise promise) {
        try {
            String deviceName = android.os.Build.MODEL;
            promise.resolve(deviceName);
        } catch (Exception e) {
            promise.reject("DEVICE_ERROR", e);
        }
    }
}

用Kotlin写更简洁:

// DeviceInfoModule.kt
package com.yourcompany.rnmodules

import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Promise

class DeviceInfoModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {

    override fun getName(): String = "DeviceInfoModule"

    @ReactMethod
    fun getDeviceName(promise: Promise) {
        try {
            val deviceName = android.os.Build.MODEL
            promise.resolve(deviceName)
        } catch (e: Exception) {
            promise.reject("DEVICE_ERROR", e)
        }
    }
}

小技巧:getName() 返回的字符串,就是你在JS端调用的模块名。比如这里叫 DeviceInfoModule,JS里就用 NativeModules.DeviceInfoModule 访问。命名要一致,大小写敏感。

4.3 实现原生方法:从JS到Java的桥梁

方法实现是核心。你需要理解几个关键点:

  • @ReactMethod:这个注解告诉RN框架,这个方法要暴露给JS
  • 参数类型:支持String、int、double、boolean、ReadableMap、ReadableArray
  • 回调方式:用Callback或Promise。我强烈推荐Promise,更现代、更可控
  • 线程模型:原生方法默认跑在JS线程。如果你要做耗时操作(比如网络请求、文件读写),必须自己开线程

我曾经在项目里踩过一个坑:直接在主线程里做图片压缩,结果UI卡死。后来改成用AsyncTask(现在用Coroutine或RxJava),问题就解决了。

来看一个带参数的方法示例:

@ReactMethod
public void saveData(String key, String value, Promise promise) {
    try {
        SharedPreferences prefs = getReactApplicationContext()
            .getSharedPreferences("MyApp", Context.MODE_PRIVATE);
        prefs.edit().putString(key, value).apply();
        promise.resolve("Saved successfully");
    } catch (Exception e) {
        promise.reject("SAVE_ERROR", e.getMessage());
    }
}

JS端调用:

import { NativeModules } from 'react-native';
NativeModules.DeviceInfoModule.saveData('username', '张三')
  .then(result => console.log(result))
  .catch(error => console.error(error));

注意:@ReactMethod 标记的方法必须是 public void 类型。返回值只能是void,因为结果通过Promise或Callback传递。别想着直接return一个值,那行不通。

4.4 注册模块:让RN认识你的模块

模块写好了,还得注册。这一步很多人会忘。你需要创建一个 ReactPackage 实现类:

// DeviceInfoPackage.java
package com.yourcompany.rnmodules;

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 DeviceInfoPackage implements ReactPackage {

    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();
        modules.add(new DeviceInfoModule(reactContext));
        return modules;
    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }
}

然后在 MainApplication.java 的 getPackages() 方法里加上:

packages.add(new DeviceInfoPackage());

避坑指南:我曾经在注册时忘了加 new DeviceInfoPackage(),结果JS端一直报 Native module cannot be null。排查了半天才发现是注册遗漏。所以,写完模块第一件事就是检查注册代码。

4.5 调试与测试:验证你的模块

模块写完了怎么测?我一般这么做:

  1. 在JS端写一个测试页面,调用所有暴露的方法
  2. 用console.log打印返回值
  3. 用Android Studio的Logcat看原生日志
  4. 测试边界情况:传null、传空字符串、传超大数字

举个例子,测试上面的 getDeviceName:

import { NativeModules } from 'react-native';

async function testModule() {
  try {
    const deviceName = await NativeModules.DeviceInfoModule.getDeviceName();
    console.log('Device name:', deviceName);
  } catch (error) {
    console.error('Error:', error);
  }
}

如果一切正常,你会看到类似 Device name: Pixel 6 Pro 的输出。如果报错,检查Logcat里的异常堆栈。

个人经验:调试原生模块时,我习惯在Android Studio里打断点。JS端调用时,代码会停在断点处,你可以一步步看变量值。比纯日志高效多了。

4.6 常见问题与解决方案

问题 原因 解决方案
Native module cannot be null 模块未注册或包名错误 检查ReactPackage注册和getName()返回值
Method not found @ReactMethod注解遗漏或方法名拼写错误 确认注解存在,JS端方法名与原生一致
UI卡顿 耗时操作跑在主线程 使用AsyncTask、Coroutine或Handler开子线程
Promise未调用 原生方法中忘记resolve或reject 确保每个分支都调用了promise.resolve或reject