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

1,731 阅读11分钟

尊重版权,未经授权不得转载
本文出自:贾鹏辉的技术博客(www.devio.org)

前言

一直想写一下我在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原生模块

提示:本文中所开发的项目的源码已开源到GitHub,供大家学习使用。

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

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

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

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

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

原生模块开发实战

在这里我们就以开发一个从相册获取照片并裁切照片的实战项目,来具体讲解一下如何开发React Native Android原生模块的。

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

这一步我们需要用到AndroidStudio。 首先我们用AndroidStudio打开React Native项目根目录下的android目录,如图:

open-react-native-android-native-project

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

open-react-native-android-native-project-success

接下来呢,我们就可以编写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);
    }
}

查看源码

关于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

另外,告诉大家一个好消息,我已经本博文所用到的项目的源码放到了Github上供大家学习使用:项目源码

如果,大家在开发原生模块中遇到问题可以在本文的下方进行留言,我看到了后会及时回复的哦。 另外也可以关注我的新浪微博,或者关注我的Github来获取更多有关 React Native开发的技术干货