如何開發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》

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容