如何开发React Native 原生模块(Native Modules)?看完这篇文章就够了(Android)

期待已久的新课上线啦!解锁React Native开发新姿势,一网打尽React Native最新与最热技术,点我Get!!!

前言

一直想写一下我在React Native原生模块封装方面的一些经验和心得,来分享给大家,但实在抽不开身,今天看了一下日历发现2018年马上就结束了,所以就赶年底将这篇博文写好并发布(其实是两篇:要看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原生模块的。

[图片上传失败...(image-611f80-1555862685270)]

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

首先,让我们先看一下,开发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,"裁剪失败");
            }
        }
    }

    //...省略部分代码
  
    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,如:http://coding.imooc.com/class/304.html

        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了。

如果,大家在开发原生模块中遇到问题可以在课程的对应章节的右边进行留言,我看到了后会及时回复的哦。

推荐学习:视频教程《最新版React Native+Redux打造高质量上线App》

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容