React Native Android原生模块开发实战|教程|心得|怎样创建React Native Android原生模块

尊重版权,未经授权不得转载

本文出自:贾鹏辉的技术博客(http://blog.csdn.net/fengyuzhengfan/article/details/54691503)

告诉大家一个好消息。为大家精心准备的React Native视频教程公布了,大家现能够看视频学React Native了。

前言

一直想写一下我在React Native原生模块封装方面的一些经验和心得。来分享给大家,但实在抽不开身。今天看了一下日历发现立即就春节了。所以就赶在春节之前将这篇博文写好并公布(事实上是两篇:要看iOS篇的点这里《React Native iOS原生模块开发》)。

我平时在用React Native开发App时会用到一些原生模块,比方:在做社会化分享、第三方登录、扫描、通信录。日历等等。想必大家也是一样。

关于在React Native中使用原生模块。在这里引用React Native官方文档的一段话:

有时候App须要訪问平台API,但在React Native可能还没有对应的模块。或者你须要复用一些Java代码,而不想用JavaScript再又一次实现一遍;又或者你须要实现某些高性能的、多线程的代码。譬如图片处理、数据库、或者一些高级扩展等等。

我们把React Native设计为能够在其基础上编写真正的原生代码。并且能够訪问平台全部的能力。这是一个相对高级的特性,我们并不期望它应当在日常开发的过程中常常出现,但它确实不可缺少,并且是存在的。

假设React Native还不支持某个你须要的原生特性,你应当能够自己实现对该特性的封装。

上面是我翻译React Native官方文档上的一段话,大家假设想看英文版能够点这里:Native Modules

在这篇文章中呢,我会带着大家来开发一个从相冊获取照片并裁切照片的项目,并结合这个项目来详细解说一下怎样一步步开发React Native Android原生模块的。

React Native Android原生模块开发实战|教程|心得|怎样创建React Native Android原生模块

提示:告诉大家一个好消息,React Native视频教程公布了。大家现能够看视频学React Native了。

首先。让我们先看一下,开发Android原生模块的主要流程。

开发Android原生模块的主要流程

在这里我把构建React Native Android原生模块的流程概括为以下三大步:

  1. 编写原生模块的相关Java代码。
  2. 暴露接口与数据交互。
  3. 注冊与导出React Native原生模块。

接下来让我们一起来看一下每一步所须要做的一些事情。

原生模块开发实战

在这里我们就以开发一个从相冊获取照片并裁切照片的实战项目,来详细解说一下怎样开发React Native Android原生模块的。

编写原生模块的相关Java代码

这一步我们须要用到AndroidStudio。

首先我们用AndroidStudio打开React Native项目根文件夹下的android文件夹。如图:

React Native Android原生模块开发实战|教程|心得|怎样创建React Native Android原生模块

用AndroidStudio第一次打开这个Android项目的时候,AndroidStudio会下载一些此项目所须要的依赖,比方项目所依赖的Gradle版本号等。这些依赖下载完毕之后呢。AndroidStudio会对项目进行初始化,初始化成功之后在AndroidStudio的工具栏中能够看到一个名为“app”的一个可执行的模块。如图:

React Native Android原生模块开发实战|教程|心得|怎样创建React Native Android原生模块

接下来呢,我们就能够编写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,"裁剪失败");
}
}
} //...省略部分代码 private void onCrop(Uri targetUri,Uri outputUri){
this.activity.startActivityForResult(IntentUtils.getCropIntentWith(targetUri,outputUri,aspectX,aspectY),RC_CROP);
}
}

查看视频教程

关于Android拍照、从相冊或文件里选择照片。裁剪以及压缩照片等更高级的功能实现,大家能够參考开源项目TakePhoto

实现了从相冊选择照片以及裁切照片的功能之后呢,接下来我们须要将public void selectWithCrop(int aspectX, int aspectY, Promise promise)暴露给React Native,以供js调用。

暴露接口与数据交互

接下了我们就向React Native暴露接口以及做一些数据交互部分的操作。为了暴露接口以及进行数据交互我们须要借助React Native的ReactContextBaseJavaModule类,在这里我们创建一个ImageCropModule.java类让它继承自ReactContextBaseJavaModule

创建一个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, 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.java类中。我们重写了public String getName()方法。来暴露我们原生模块的名字。

并在public void selectWithCrop(int aspectX, int aspectY, Promise promise)上加入了@ReactMethod注解来暴露接口。这样以来我们就能够在js文件里通过ImageCrop.selectWithCrop来调用我们所暴露给React Native的接口了。

接下来呢,我们来看一下原生模块和js模块是怎样进行数据交互的?

原生模块和JS进行数据交互

在我们要实现的从相冊选择照片并裁切的项目中。js模块须要告诉原生模块照片裁切的比例,等照片裁切完毕后,原生模块须要对js模块进行回调来告诉js模块照片裁切的结果,在这里我们须要将照片裁切后生成的图片的路径告诉js模块。

提示:在全部的情况下js和原生模块之前进行通信都是在异步的情况下进行的。

接下来我们就来看下一JS是怎样向原生模块传递数据的?

JS向原生模块传递数据:

为了实现JS向原生模块进行传递数据。我们能够直接通过调用原生模块所暴露出来的接口。来为接口方法设置參数。

这样以来我们就能够将数据通过接口參数传递到原生模块中,如:

  /**
* 选择并裁切照片
* @param outputX
* @param outputY
* @param promise
*/
void selectWithCrop(int outputX,int outputY,Promise promise);

通过上述代码我们能够看出,js模块能够通过selectWithCrop方法来告诉原生模块要裁切照片的宽高比,最后一个參数是一个Promise,照片裁剪完毕之后呢。原生模块能够通过Promise来对js模块进行回调,来告诉裁切结果。

既然是js和Java进行数据传递。那么他们两者之间是怎样进行类型转换的呢:

在上述样例中我们通过@ReactMethod注解来暴露接口,被 @ReactMethod标注的方法支持例如以下几种数据类型。

@ReactMethod标注的方法支持例如以下几种数据类型的參数:

Boolean -> Bool
Integer -> Number
Double -> Number
Float -> Number
String -> String
Callback -> function
ReadableMap -> Object
ReadableArray -> Array

原生模块向JS传递数据:

原生模块向JS传递数据我们能够借助Callbacks与Promises。接下来就讲一下怎样通过他们两个进行数据传递的。

Callbacks

原生模块支持一个特殊类型的參数-Callbacks,我们能够通过它来对js进行回调,以告诉js调用原生模块方法的结果。

将我们selectWithCrop的參数改为Callbacks之后:

@Override
public void selectWithCrop(int aspectX, int aspectY, 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,"裁剪失败");
}

在上述代码中我们通过Callbackinvoke方法来对js进行对调,以下我们来看一下Callback.java的源代码:

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);
}

Callback.java的源代码中我们能够看出,它是一个仅仅有一个public void invoke(Object... args)方法的接口,invoke方法接受一个可变參数,所以我们能够向js传递多个參数。

接下来呢,我们在js中就能够这样来调用我们所暴露的接口:

ImageCrop.selectWithCrop(parseInt(x),parseInt(y),(error)=>{
console.log(error);
},(result)=>{
console.log(result);
})

提示:另外要告诉大家的是。不管是Callback还是我接下来要讲的Promise,我们仅仅能调用一次,也就是”you call me once,I can only call you once”。

Promises

除了上文所讲的Callback之外React Native还为了我们提供了第二种回调js的方式叫-Promise。

假设我们暴露的接口方法的最后一个參数是Promise时。如:

@Override @ReactMethod
public void selectWithCrop(int aspectX, int aspectY, Promise promise) {
getCrop().selectWithCrop(aspectX,aspectY,promise);
}

那么当js调用它的时候将会返回一个Promsie:

ImageCrop.selectWithCrop(parseInt(x),parseInt(y)).then(result=> {
this.setState({
result: result
})
}).catch(e=> {
this.setState({
result: e
})
});

另外。我们也能够使用ES2016的 async/await语法,来简化我们的代码:

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模块发送随意次数的事件,当中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注冊我们刚才创建的原生模块,我们须要实现ReactPackageReactPackage主要为注冊原生模块所存在,仅仅有已经向React Native注冊的模块才干在js模块使用。

/**
* 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;
}
}

查看视频教程

在上述代码中,我们实现一个ReactPackage,接下来呢,我们还须要在android/app/src/main/java/com/your-app-name/MainApplication.java中注冊我们的ImageCropReactPackage

@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

告诉大家一个好消息,为大家精心准备的React Native视频教程公布了,大家现能够看视频学React Native了。

假设,大家在开发原生模块中遇到问题能够在本文的下方进行留言,我看到了后会及时回复的哦。

另外也能够关注我的新浪微博,或者关注我的Github来获取很多其它有关React Native开发的技术干货

推荐学习:视频教程《React Native开发跨平台GitHub App》

上一篇:ROS(indigo) 用于机器人控制的图形化编程工具--code_it robot_blockly


下一篇:Razor视图引擎基础语法