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模块。我一般这么干:
- 在项目根目录的
android/app/src/main/java/com/yourcompany/下新建一个包 - 创建一个Java或Kotlin类,继承自
ReactContextBaseJavaModule - 实现必要的方法,比如
getName() - 注册到
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 调试与测试:验证你的模块
模块写完了怎么测?我一般这么做:
- 在JS端写一个测试页面,调用所有暴露的方法
- 用console.log打印返回值
- 用Android Studio的Logcat看原生日志
- 测试边界情况:传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 |