React Native Android原生模块开发实战教程
前言
我一直想分享在React Native原生模块封装方面的经验和心得。在开发React Native应用时,经常需要使用原生模块,例如社会化分享、第三方登录、扫描、通信录和日历等功能。
React Native官方文档指出,当需要访问平台API或复用Java代码时,可以编写原生模块。原生模块允许你实现高性能、多线程的代码,如图片处理或数据库操作。本文将引导你开发一个从相册选择并裁切照片的项目,并以此为例详细讲解React Native Android原生模块的开发步骤。
开发Android原生模块的主要流程
构建React Native Android原生模块主要包括三个步骤:
- 编写原生模块的Java代码。
- 暴露接口并处理数据交互。
- 注册和导出React Native原生模块。
下面将详细介绍每个步骤。
原生模块开发实战
我们将通过一个从相册获取照片并裁切的项目来具体讲解React Native Android原生模块的开发。
编写原生模块的相关Java代码
首先,使用Android Studio打开React Native项目中的android目录。项目初始化后,可以开始编写Java代码。
我们首先定义一个Crop接口:
public interface Crop {
void selectWithCrop(int outputX, int outputY, Promise promise);
}
接下来,创建CropImpl.java类来实现从相册选择照片及裁切功能:
public class CropImpl implements ActivityEventListener, Crop {
private final int RC_PICK = 50081;
private final int RC_CROP = 50082;
private final String CODE_ERROR_PICK = "用户取消";
private final String CODE_ERROR_CROP = "裁切失败";
private Promise pickPromise;
private Uri outPutUri;
private int aspectX;
private int aspectY;
private Activity activity;
public static CropImpl of(Activity activity) {
return new CropImpl(activity);
}
private CropImpl(Activity activity) {
this.activity = activity;
}
public void updateActivity(Activity activity) {
this.activity = activity;
}
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
if (requestCode == RC_PICK) {
if (resultCode == Activity.RESULT_OK && data != null) {
outPutUri = Uri.fromFile(Utils.getPhotoCacheDir(System.currentTimeMillis() + ".jpg"));
onCrop(data.getData(), outPutUri);
} else {
pickPromise.reject(CODE_ERROR_PICK, "没有获取到结果");
}
} else if (requestCode == RC_CROP) {
if (resultCode == Activity.RESULT_OK) {
pickPromise.resolve(outPutUri.getPath());
} else {
pickPromise.reject(CODE_ERROR_CROP, "裁剪失败");
}
}
}
private void onCrop(Uri targetUri, Uri outputUri) {
this.activity.startActivityForResult(IntentUtils.getCropIntentWith(targetUri, outputUri, aspectX, aspectY), RC_CROP);
}
@Override
public void selectWithCrop(int outputX, int outputY, Promise promise) {
this.aspectX = outputX;
this.aspectY = outputY;
this.pickPromise = promise;
this.activity.startActivityForResult(IntentUtils.getPickIntentWithGallery(), RC_PICK);
}
}
暴露接口与数据交互
为了向React Native暴露接口并进行数据交互,需要创建一个继承自ReactContextBaseJavaModule的类。
创建一个ReactContextBaseJavaModule
创建ImageCropModule.java类:
public class ImageCropModule extends ReactContextBaseJavaModule implements Crop {
private CropImpl cropImpl;
public ImageCropModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "ImageCrop";
}
@Override
@ReactMethod
public void selectWithCrop(int aspectX, int aspectY, Promise promise) {
getCrop().selectWithCrop(aspectX, aspectY, promise);
}
private CropImpl getCrop() {
if (cropImpl == null) {
cropImpl = CropImpl.of(getCurrentActivity());
getReactApplicationContext().addActivityEventListener(cropImpl);
} else {
cropImpl.updateActivity(getCurrentActivity());
}
return cropImpl;
}
}
在ImageCropModule中,重写getName方法以暴露模块名称,并在selectWithCrop方法上添加@ReactMethod注解来暴露接口。这样,在JavaScript中可以通过ImageCrop.selectWithCrop调用该接口。
原生模块和JS进行数据交互
原生模块和JavaScript之间的通信是异步的。JavaScript通过调用暴露的接口传递数据,例如裁切照片的宽高比。原生模块完成操作后,通过Promise回调JavaScript。
被@ReactMethod标注的方法支持以下数据类型参数:
- Boolean -> Bool
- Integer -> Number
- Double -> Number
- Float -> Number
- String -> String
- Callback -> function
- ReadableMap -> Object
- ReadableArray -> Array
使用Promise进行回调
在selectWithCrop方法中,最后一个参数是Promise。当照片裁切完成后,原生模块通过Promise解析或拒绝来回调JavaScript。
在JavaScript中,可以这样调用:
ImageCrop.selectWithCrop(parseInt(x), parseInt(y))
.then(result => {
console.log(result);
})
.catch(e => {
console.log(e);
});
或者使用async/await语法:
async onSelectCrop() {
var result = await ImageCrop.selectWithCrop(parseInt(x), parseInt(y));
}
向JS发送事件
对于需要多次传递数据的情况,如二维码扫描,可以使用RCTDeviceEventEmitter向JavaScript发送事件。
在原生模块中发送事件:
private void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) {
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}
在JavaScript中监听事件:
componentDidMount() {
DeviceEventEmitter.addListener('onScanningResult', this.onScanningResult);
}
onScanningResult = (e) => {
console.log(e.result);
};
componentWillUnmount() {
DeviceEventEmitter.removeListener('onScanningResult', this.onScanningResult);
}
注册与导出React Native原生模块
为了向React Native注册原生模块,需要实现ReactPackage接口。
创建ImageCropReactPackage.java:
public class ImageCropReactPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new ImageCropModule(reactContext));
return modules;
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
然后在MainApplication.java中注册该Package:
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new ImageCropReactPackage()
);
}
最后,导出一个JavaScript模块以便使用。创建ImageCrop.js文件:
import { NativeModules } from 'react-native';
export default NativeModules.ImageCrop;
在其他地方导入并使用:
import ImageCrop from './ImageCrop';
onSelectCrop() {
let x = this.aspectX ? this.aspectX : ASPECT_X;
let y = this.aspectY ? this.aspectY : ASPECT_Y;
ImageCrop.selectWithCrop(parseInt(x), parseInt(y))
.then(result => {
this.setState({ result: result });
})
.catch(e => {
this.setState({ result: e });
});
}
关于线程
在React Native中,JavaScript运行在独立线程中。如果原生模块有耗时操作,如文件读写或网络请求,需要在新线程中执行,以避免阻塞JavaScript线程。在Android中,可以使用AsyncTask实现多线程。如果需要更新UI,必须在主线程中进行:
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
if (!activity.isFinishing()) {
// 更新UI代码
}
}
});
通过以上步骤,你可以成功开发并集成React Native Android原生模块,增强应用的功能和性能。