React Native Android原生模块开发实战|教程|心得|如何创建React
前言一直想写一下我在React Native原生模块封装方面的一些经验和心得,来分享给大家,但实在抽不开身,今天看了一下日历发现马上就春节了,所以就赶在春节之前将这篇博文写好并发布(其实是两篇: 我平时在用React Native开发App时会用到一些原生模块,比如:在做社会化分享、第三方登录、扫描、通信录,日历等等,想必大家也是一样。 关于在React Native中使用原生模块,在这里引用React Native官方文档的一段话:
上面是我翻译React Native官方文档上的一段话,大家如果想看英文版可以点这里:Native Modules
首先,让我们先看一下,开发Android原生模块的主要流程。 开发Android原生模块的主要流程在这里我把构建React Native Android原生模块的流程概括为以下三大步:
接下来让我们一起来看一下每一步所需要做的一些事情。 原生模块开发实战在这里我们就以开发一个从相册获取照片并裁切照片的实战项目,来具体讲解一下如何开发React Native Android原生模块的。 编写原生模块的相关Java代码这一步我们需要用到AndroidStudio。 用AndroidStudio第一次打开这个Android项目的时候,AndroidStudio会下载一些此项目所需要的依赖,比如项目所依赖的Gradle版本等。这些依赖下载完成之后呢,AndroidStudio会对项目进行初始化,初始化成功之后在AndroidStudio的工具栏中可以看到一个名为“app”的一个可运行的模块,如图: 接下来呢,我们就可以编写Java代码了。 首先呢,我们先来实现一个Crop接口: public interface Crop {
/** * 选择并裁切照片 * @param outputX * @param outputY * @param promise */
void selectWithCrop(int outputX,int outputY,Promise promise);
}
我们创建一个CropImpl.java,在这个类中呢,我们实现了从相册选择照片以及裁切照片的功能: /** * React Native Android原生模块开发 * Author: CrazyCodeBoy * 技术博文:http://www.devio.org * GitHub:https://github.com/crazycodeboy * Email:crazycodeboy@gmail.com */
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,"裁剪失败");
}
}
}
@Override
public void onNewIntent(Intent intent) {}
@Override
public void selectWithCrop(int aspectX,int aspectY,Promise promise) {
this.pickPromise=promise;
this.aspectX=aspectX;
this.aspectY=aspectY;
this.activity.startActivityForResult(IntentUtils.getPickIntentWithGallery(),RC_PICK);
}
private void onCrop(Uri targetUri,Uri outputUri){
this.activity.startActivityForResult(IntentUtils.getCropIntentWith(targetUri,outputUri,aspectX,aspectY),RC_CROP);
}
}
查看源码
实现了从相册选择照片以及裁切照片的功能之后呢,接下来我们需要将 暴露接口与数据交互接下了我们就向React Native暴露接口以及做一些数据交互部分的操作。为了暴露接口以及进行数据交互我们需要借助React Native的 创建一个ReactContextBaseJavaModule/** * React Native Android原生模块开发 * Author: CrazyCodeBoy * 技术博文:http://www.devio.org * GitHub:https://github.com/crazycodeboy * Email:crazycodeboy@gmail.com */
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,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;
}
}
查看源码 在 接下来呢,我们来看一下原生模块和js模块是如何进行数据交互的? 原生模块和JS进行数据交互在我们要实现的从相册选择照片并裁切的项目中,js模块需要告诉原生模块照片裁切的比例,等照片裁切完成后,原生模块需要对js模块进行回调来告诉js模块照片裁切的结果,在这里我们需要将照片裁切后生成的图片的路径告诉js模块。
接下来我们就来看下一JS是如何向原生模块传递数据的?
为了实现JS向原生模块进行传递数据,我们可以直接通过调用原生模块所暴露出来的接口,来为接口方法设置参数。这样以来我们就可以将数据通过接口参数传递到原生模块中,如: /** * 选择并裁切照片 * @param outputX * @param outputY * @param promise */
void selectWithCrop(int outputX,Promise promise);
通过上述代码我们可以看出,js模块可以通过 既然是js和Java进行数据传递,那么他们两者之间是如何进行类型转换的呢:
Boolean -> Bool
Integer -> Number
Double -> Number
Float -> Number
String -> String
Callback -> function
ReadableMap -> Object
ReadableArray -> Array
原生模块向JS传递数据我们可以借助Callbacks与Promises,接下来就讲一下如何通过他们两个进行数据传递的。 Callbacks 原生模块支持一个特殊类型的参数-Callbacks,我们可以通过它来对js进行回调,以告诉js调用原生模块方法的结果。 @Override
public void selectWithCrop(int aspectX,Callback errorCallback,Callback successCallback) {
this.errorCallback=errorCallback;
this.successCallback=successCallback;
this.aspectX=aspectX;
this.aspectY=aspectY;
this.activity.startActivityForResult(IntentUtils.getPickIntentWithGallery(),RC_PICK);
}
在回调的时候,我们就可以这样写: if (resultCode == Activity.RESULT_OK) {
successCallback.invoke(outPutUri.getPath());
}else {
errorCallback.invoke(CODE_ERROR_CROP,"裁剪失败");
}
在上述代码中我们通过 public interface Callback {
/** * Schedule javascript function execution represented by this {@link Callback} instance * * @param args arguments passed to javascript callback method via bridge */
public void invoke(Object... args);
}
从 接下来呢,我们在js中就可以这样来调用我们所暴露的接口: ImageCrop.selectWithCrop(parseInt(x),parseInt(y),(error)=>{
console.log(error);
},(result)=>{
console.log(result);
})
Promises 除了上文所讲的 @Override @ReactMethod
public void selectWithCrop(int aspectX,Promise promise) {
getCrop().selectWithCrop(aspectX,promise);
}
那么当js调用它的时候将会返回一个Promsie: ImageCrop.selectWithCrop(parseInt(x),parseInt(y)).then(result=> {
this.setState({
result: result
})
}).catch(e=> {
this.setState({
result: e
})
});
另外,我们也可以使用ES2016的 async onSelectCrop() {
var result=await ImageCrop.selectWithCrop(parseInt(x),parseInt(y));
}
这样以来代码就简化了很多。 因为,基于回调的数据传递无论是Callback还是Promise,都只能调用一次。但,在实际项目开发中我们有时会向js多次传递数据,比如二维码扫描原生模块,针对这种多次数据传递的情况我们该怎么实现呢? 接下来我就为大家介绍一种原生模块可以向js多次传递数据的方式: 向js发送事件在原生模块中我们可以向js发送多次事件,即使原生模块没有被直接的调用。为了向js传递事件我们需要用到RCTDeviceEventEmitter,它是原生模块和js之间的一个事件发射器。 private void sendEvent(ReactContext reactContext,String eventName,@Nullable WritableMap params) {
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName,params);
}
在上述方法中我们可以向js模块发送任意次数的事件,其中 componentDidMount() {
//注册扫描监听
DeviceEventEmitter.addListener('onScanningResult',this.onScanningResult);
}
onScanningResult = (e)=> {
this.setState({
scanningResult: e.result,});
}
另外,不要忘记在组件被卸载的时候移除监听: componentWillUnmount(){
DeviceEventEmitter.removeListener('onScanningResult',this.onScanningResult);//移除扫描监听
}
到现在呢,暴露接口以及数据传递已经进行完了,接下来呢,我们就需要注册与导出React Native原生模块了。 注册与导出React Native原生模块为了向React Native注册我们刚才创建的原生模块,我们需要实现 /** * React Native Android原生模块开发 * Author: CrazyCodeBoy * 技术博文:http://www.devio.org * GitHub:https://github.com/crazycodeboy * Email:crazycodeboy@gmail.com */
public class ImageCropReactPackage implements ReactPackage {
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new ImageCropModule(reactContext));
return modules;
}
}
查看源码 在上述代码中,我们实现一个 @Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),new ImageCropReactPackage()//在这里将我们刚才创建的ImageCropReactPackage添加进来
);
}
原生模块注册完成之后呢,我们接下来就需要为我们的原生模块导出一个js模块,以方便我们使用它。 我们创建一个ImageCrop.js文件,然后添加如下代码: import { NativeModules } from 'react-native';
export default NativeModules.ImageCrop;
这样以来呢,我们就可以在其他地方通过下面方式来使用我们所导出的这个模块了: import ImageCrop from './ImageCrop' //导入ImageCrop.js
//...省略部分代码
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
})
});
}
//...省略部分代码
}
现在呢,我们这个原生模块就开发好了,而且我们也使用了我们的这个原生模块。关于Android拍照、从相册或文件中选择照片,裁剪以及压缩照片等更高级的功能实现,大家也可以参考开源项目TakePhoto 关于线程在React Native中,JS模块运行在一个独立的线程中。在我们为React Native开发原生模块的时候,如果有耗时的操作比如:文件读写、网络操作等,我们需要新开辟一个线程,不然的话,这些耗时的操作会阻塞JS线程。在Android中我们可以借助AsyncTask来实现多线程。另外,如果原生模块中需要更新UI,我们需要获取主线程,然后在主线程中更新UI,如: activity.runOnUiThread(new Runnable() {
@Override
public void run() {
if (!activity.isFinishing()) {
mSplashDialog = new Dialog(activity,fullScreen? R.style.SplashScreen_Fullscreen:R.style.SplashScreen_SplashTheme);
mSplashDialog.setContentView(R.layout.launch_screen);
mSplashDialog.setCancelable(false);
if (!mSplashDialog.isShowing()) {
mSplashDialog.show();
}
}
}
});
可参考:SplashScreen.java
如果,大家在开发原生模块中遇到问题可以在本文的下方进行留言,我看到了后会及时回复的哦。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |