「React Native」與「Android」的交互方式總結

React Native 做爲一個混合開發解決方案,由於業務、性能上的種種緣由,老是避免不了與原生進行交互。在開發過程當中咱們將 RN 與原生交互的幾種方式進行了梳理,按照途徑主要分爲如下幾類:javascript

  • 經過原生 Module 進行交互
  • 經過原生 View 進行交互
  • 經過發送事件 Event 進行交互

1、經過原生 Module 進行交互

經過原生 Module 進行交互是最高頻的使用方式。封裝原生 Module 能夠將定義好的原生方法交給 RN 在 JS 端進行調用,JS 端能夠在調用方法時經過傳參的方式直接將數據傳輸給原生端,而原生端能夠在方法執行事後將須要返回的數據經過 Promise 或者 Callback 將數據返回給 JS 端。java

1.1 封裝原生 Module

封裝原生 Module 的步驟包括如下幾步:react

  • 建立自定義 Module
  • 建立自定義 Package 註冊自定義 Module
  • 註冊自定義 Package
  • 在RN中使用自定義 Module

1.1.1 建立自定義 Module

建立自定義 Module 其實就是將 RN 但願調用的原生功能封裝成中間件的形式,這個中間件須要繼承 ReactContextBaseJavaModule 類,並重寫它的 getName() 方法。getName() 方法返回了 JS 能夠訪問的自定義 Module 名稱,使得咱們在 JS 端能夠經過 NativeModules.自定義 Module 名稱 的形式訪問這個中間件。小程序

public class MyModule extends ReactContextBaseJavaModule {
    
    public MyModule(@Nonnull ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @Nonnull
    @Override
    public String getName() {
        return "MyNativeModule"; // 暴露給RN的模塊名,在JS端經過 NativeModules.MyNativeModule 便可訪問到本模塊
    }
}
複製代碼

1.1.2 建立自定義 Package 註冊自定義 Module

自定義 Package 實現了 ReactPackage 接口,該接口提供了2個方法來分別註冊自定義 Module 和自定義 ViewManager,其中自定義 ViewManager 用於封裝原生 View 與 RN 進行交互,具體內容能夠查看後面的「經過原生 View 進行交互」。咱們須要在 createNativeModules 方法中返回一個包含咱們新建的自定義 Module 實例的 List,完成註冊。react-native

public class MyReactPackage implements ReactPackage {
    @Nonnull
    @Override
    public List<NativeModule> createNativeModules(@Nonnull ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();
        modules.add(new MyModule(reactContext)); // 將新建的 MyModule 實例加入到 List 中完成註冊
        return modules;
    }

    @Nonnull
    @Override
    public List<ViewManager> createViewManagers(@Nonnull ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }
}
複製代碼

1.1.3 註冊自定義 Package

僅僅完成了自定義 Module 在自定義 Package 的註冊還不能讓 RN 使用咱們的 Module,咱們須要將剛剛新建的 ReactPackage 實例註冊到 ReactApplication 的 ReactNativeHost 實例中。在默認的 RN 工程下,ReactApplication 一般爲 MainApplication,你也可讓本身原有安卓項目中的 Application 類實現 ReactApplication 接口。promise

public class MainApplication extends Application implements ReactApplication {

    private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
        @Override
        public boolean getUseDeveloperSupport() {
            return BuildConfig.DEBUG;
        }

        @Override
        protected List<ReactPackage> getPackages() {
            return Arrays.<ReactPackage>asList(
                    new MainReactPackage(),
                    new MyReactPackage() // 將新建的 MyReactPackage 實例註冊到 ReactPackage 列表中
            );
        }

        @Override
        protected String getJSMainModuleName() {
            return "index";
        }
    };

    @Override
    public ReactNativeHost getReactNativeHost() {
        return mReactNativeHost;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        SoLoader.init(this, /* native exopackage */ false);
    }
}
複製代碼

1.1.4 在 RN 中使用自定義 Module

完成原生端的註冊後,RN 便可使用咱們封裝的自定義 Module 了。在 JS 端引用的代碼以下:微信

import { NativeModules } from "react-native";

let customModule = NativeModules.MyNativeModule; // 此處引用的自定義 Module 名必須與自定義 Module 中 getName() 方法返回的字符串一致
複製代碼

接下來咱們能夠經過這個自定義 Module 實現 RN 與原生的幾種通訊方式。ide

1.2 JS 端獲取原生端自定義 Module 預設的常量值

在自定義 Module 中,咱們能夠經過重寫 getConstants() 方法,返回一個 Map。這個 Map 的 key 爲 RN 中能夠被訪問的常量名稱,value 爲預設的常量值。函數

private static final String CUSTOM_CONST_KEY = "TEXT";

@Nullable
@Override
// 獲取模塊預約義的常量值
public Map<String, Object> getConstants() {
    final Map<String, Object> constants = new HashMap<>();
    constants.put(CUSTOM_CONST_KEY, "這是模塊預設常量值");
    return constants;
}
複製代碼

在 RN 中使用 Text 組件調用這個常量顯示內容:工具

<Text>{ customModule.TEXT }</Text>
複製代碼

1.3 原生端經過 @ReactMethod 暴露方法給 JS 端,接受 JS 端調用方法時傳入的參數數據

在原生端,若是想將方法暴露給 RN 調用,能夠在方法前加上 @ReactMethod 註解,但要注意,這個方法的訪問權限和返回類型必需要設置爲 public void 才能夠。

@ReactMethod
public void myFunction(String parmas) {
    // To Do Something
    // 字符串 params 即爲 RN 傳入的參數
}
複製代碼

在 JS 端調用這個函數,並傳遞參數:

customModule.myFunction("這裏是參數 params 的內容");
複製代碼

注:傳入的參數並不侷限於例子中的 String 類型,且參數數量能夠是多個。而 JS 與 Java 的類型對應以下:

JavaScript Java
Bool Boolean
Number Integer
Number Double
Number Float
String String
Function Callback
Object ReadableMap
Array ReadableArray

1.4 原生端經過 Callback 回調函數返回數據給 JS 端

經過上面的類型對應,咱們瞭解到, JS 中的 function 做爲參數傳輸到 Java 中就是個 Callback。因此咱們可使用回調函數,在完成原生方法的執行事後,將須要的結果或狀態經過 Callback.invoke() 方法回調給 JS。

@ReactMethod
public void myFunction(String params, Callback success, Callback failture) {

    try {
        if (params != null && !params.equals("")){
            // 回調成功,返回結果信息
            success.invoke("這是從原生", "返回的字符串");
        }
    }catch (IllegalViewOperationException e) {
        // 回調失敗,返回錯誤信息
        failture.invoke(e.getMessage());
    }
}
複製代碼

在 JS 中須要定義回調函數的執行內容,這裏定義了一個匿名函數做爲回調函數。你能夠根據本身的業務需求替換爲相應的回調函數。

customModule.myFunction(
    "這是帶Callback回調的函數方法",
     (parma1, parma2) => {
        var result = parma1 + parma2;
        console.log(result); // 顯示: 這是從原生返回的字符串
    },
    errMsg => {
        console.log(errMsg);
    }
);
複製代碼

1.5 原生端經過 Promise 函數返回數據給 JS 端

除了使用 Callback 進行回調,咱們還能夠在 ReactMethod 中將 Promise 做爲最後一個參數,使用 Promise 實例,完成數據的回傳。Promise 具備 resolve() 和 reject() 兩個方法,能夠用於處理正常和異常的回傳。在使用時,咱們一般在自定義 Module 中定義一個 Promise 類型的私有變量,在調用 ReactMethod 時,對這個私有變量進行賦值,而後在須要回傳數據的地方使用這個私有變量進行回傳,這樣能夠更靈活的控制回傳時機。但要注意的是,Promise 實例只能被 resolve() 或 reject() 一次,若屢次回調將會報錯。

private Promise mPromise;
private Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            mPromise.resolve((String) msg.obj);
        }
    };

@ReactMethod
public void myFunction(String params, Promise promise) {
    mPromise = promise;
    try {
        if (params != null && !params.equals("")){
            // 回調成功,返回結果信息
            Message message = handler.obtainMessage();
            message.obj = params;
            handler.sendMessage(message);
        }
    }catch (IllegalViewOperationException e) {
        // 回調失敗,返回錯誤信息
        mPromise.reject('error', e.getMessage());
    }
}
複製代碼

在 JS 端的調用狀況

customModule.myFunction("這是使用 Promise 回調的函數方法")
            .then(result => {
                console.log(result); // 顯示: 這是從原生返回的字符串
            })
複製代碼

2、經過原生 View 進行交互

經過自定義 Module 進行交互已經能夠解決咱們的大部分開發需求,然而有的時候,基於性能和開發工做量的角度考慮,咱們能夠將原生的組件或佈局封裝好,併爲這個原生 View 創建一個繼承自 SimpleViewManager 或 ViewGroupManager 的 ViewManager 類。經過這個 ViewManager 能夠註冊一系列原生端和 JS 端的參數及事件映射,達到交互的目的。

2.1 封裝原生 View

封裝原生 View 包括如下幾步:

  • 建立原生 View 類
  • 建立 ViewManager
  • 將 ViewManager 在自定義 Package 中註冊
  • 在 Js 端進行調用

2.1.1 建立原生 View 類

這裏以封裝一個簡單的原生 Button 的子類爲例:

public class MyButton extends Button {
    public MyButton(Context context) {
        super(context);
    }
}
複製代碼

2.1.2 建立相應的 ViewManager 類

簡單的 View 能夠建立 ViewManager 類繼承 SimpleViewManager ,而經過佈局生成的複雜 View 能夠繼承自 ViewGroupManager 類,這裏咱們繼承 SimpleViewManager:

public class MyButtonViewManager extends SimpleViewManager<MyButton> {
    @Override
    public String getName() {
        return "NativeMyButton"; // 此名稱用於在 JS 中引用
    }

    // 建立 View 實例
    @Override
    protected MyButton createViewInstance(ThemedReactContext reactContext) {
        return new MyButton(reactContext);
    }
}
複製代碼

2.1.3 將 ViewManager 在自定義 Package 中註冊

以前咱們建立了自定義 Package 類 MyReactPackage,咱們只是使用了 createNativeModules() 方法完成了自定義 Module 的註冊,接下來咱們須要在 createViewManagers() 方法中註冊剛剛建立的 MyButtonViewManager 實例:

public class MyReactPackage implements ReactPackage {
    @Nonnull
    @Override
    public List<NativeModule> createNativeModules(@Nonnull ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();
        modules.add(new MyModule(reactContext));
        return modules;
    }

    @Nonnull
    @Override
    public List<ViewManager> createViewManagers(@Nonnull ReactApplicationContext reactContext) {
        List<ViewManager> views = new ArrayList<>();
        views.add(new MyButtonViewManager()); // 建立 MyButtonViewManager 實例並註冊到 ViewManager List 中
        return views;
    }
}
複製代碼

注意,若是你沒有完成第一章中 Package 在 Application 的註冊步驟,這裏也須要將 MyReactPackage 註冊到 Application 中。

2.1.4 在 JS 端完成調用

完成了上面的原生代碼,咱們就能夠在 JS 端完成調用了:

import { requireNativeComponent, View} from 'react-native';

let MyButton = requireNativeComponent('NativeMyButton');

...
render() {
    return (
      <MyButton/>
    );
  }
...
複製代碼

2.2 原生端經過 @ReactProps 將方法暴露給 JS 端,接收 JS 端爲組件設定的屬性

在剛剛創建的 ViewManager 類中,咱們能夠經過 @ReactProps 註解方法,爲組件添加屬性,這裏咱們爲 MyButton 添加 text 屬性:

public class MyButtonViewManager extends SimpleViewManager<MyButton> {
    @Override
    public String getName() {
        return "NativeMyButton";
    }

    @Override
    protected MyButton createViewInstance(ThemedReactContext reactContext) {
        return MyButton(reactContext);
    }

    // 暴露給 JS 的參數,用於設定名稱爲「text」的屬性,設定 Button 的文字
    @ReactProp(name = "text")
    public void setSrc(MyButton view, String text) {
        view.setText(text);
    }
}
複製代碼

此時能夠在 JS 端爲組件添加 text 屬性和它的值,完成設定 MyButton 的文字:

import { requireNativeComponent, View} from 'react-native';

let MyButton = requireNativeComponent('NativeMyButton');

...
render() {
    return (
      <MyButton text='這是個按鈕'/> ); } 複製代碼

2.3 原生端經過註冊 View 事件與 JS 端的映射,使 JS 端能夠接收原生端發送的事件和數據

有些時候咱們不只僅須要將數據以屬性值的形式,從 JS 端傳輸到原生端,還須要原生端對 JS 端發送數據完成交互,這時咱們須要使用事件 Event,經過發送事件完成交互。這裏咱們能夠將 MyButton 的點擊事件通知給 JS 端:

public class MyButtonViewManager extends SimpleViewManager<MyButton> {
    @Override
    public String getName() {
        return "NativeMyButton";
    }

    @Override
    protected MyButton createViewInstance(ThemedReactContext reactContext) {
        MyButton button = new MyButton(reactContext);
        button.setOnClickListener(v -> {
            WritableMap event = Arguments.createMap(); // 這裏傳了個空的 event 對象,使用時能夠在 event 中加入要傳輸的數據
            reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
                viewId,
                "onNativeClick", // 與下面註冊的要發送的事件名稱必須相同
                event);
        });
        return button;
    }

    @ReactProp(name = "text")
    public void setSrc(MyButton view, String text) {
        view.setText(text);
    }

    @Nullable
    @Override
    public Map getExportedCustomDirectEventTypeConstants() {
        return MapBuilder.of(
            "onNativeClick", MapBuilder.of("registrationName", "onReactClick"));
            // onNativeClick 是原生要發送的 event 名稱,onReactClick 是 JS 端組件中註冊的屬性方法名稱,中間的 registrationName 不可更改
    }
}
複製代碼

而後在 JS 端就可使用 onReactClick 這個屬性來響應點擊事件:

import { requireNativeComponent, View} from 'react-native';

let MyButton = requireNativeComponent('NativeMyButton');

...
render() {
    return (
      <MyButton text='這是個按鈕' onReactClick={data=>{ // 這裏接收 event 傳過來的數據 console.log(data); }}/> ); } 複製代碼

2.4 原生端經過註冊 View 命令表和響應方法,完成接收來自 JS 端的指令

JS 端不只僅只能從設定屬性值的方法來將數據傳輸給原生端,也可使用 UIManager.dispatchViewManagerCommand 方法來發送命令並攜帶數據給原生端,這裏咱們添加了一條命令 changeText 讓 MyButton 點擊後更換按鈕上的文字:

import { requireNativeComponent, View} from 'react-native';

let MyButton = requireNativeComponent('NativeMyButton');

...
changeButtonText = () => {
    UIManager.dispatchViewManagerCommand(
      findNodeHandle(this.nativeUI),
      UIManager.TemplateMenuView.Commands.changeText, //Commands.changeText須要與native層定義的命令名稱一致
      ['這是新的按鈕'] //命令攜帶的數據
    );
}

render() {
    return (
      <MyButton ref={view => this.nativeUI = view} text='這是個按鈕' onReactClick={data=>{ this.changeButtonText; // 點擊時回傳給原生端命令 }}/> ); } 複製代碼

在原生端,咱們須要在 ViewManager 中重寫 getCommandsMap() 方法創建命令的映射表,而後重寫 receiveCommand() 方法完成接收命令後的操做

public class MyButtonViewManager extends SimpleViewManager<MyButton> {
    @Override
    public String getName() {
        return "NativeMyButton";
    }

    @Override
    protected MyButton createViewInstance(ThemedReactContext reactContext) {
        MyButton button = new MyButton(reactContext);
        button.setOnClickListener(v -> {
            WritableMap event = Arguments.createMap();
            reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
                viewId,
                "onNativeClick",
                event);
        });
        return button;
    }

    @ReactProp(name = "text")
    public void setSrc(MyButton view, String text) {
        view.setText(text);
    }

    @Nullable
    @Override
    public Map getExportedCustomDirectEventTypeConstants() {
        return MapBuilder.of(
            "onNativeClick", MapBuilder.of("registrationName", "onReactClick"));
    }

    @Override
    public Map<String, Integer> getCommandsMap() {
        return MapBuilder.of(
            「changeText」, 0 // changeText 是命令名稱,0 是命令 id
        );
    }

    @Override
    public void receiveCommand(MyButton view, int commandId, @Nullable ReadableArray args) {
        switch (commandId){
            case 0: // 當命令 id 爲 0 時
                String newText = args.getString(0); // 咱們在 JS 只傳了一個字符串過來,在這裏接收
                view.setText(newText); // 爲按鈕設定新的文字
                break;
            default:
                break;
        }
    }
}
複製代碼

3、經過發送事件 Event 進行交互

利用原生View進行交互的時候,咱們已經利用了發送事件的機制,完成原生端與 JS 端的通訊,但前提是須要將原生 View 的屬性方法與原生事件先註冊映射,才能獲取這個事件。其實事件 Event 是能夠經過 RCTDeviceEventEmitter 靈活完成發送的,原生端將事件名稱和事件數據發送後,JS 端須要根據事件名稱預先註冊一個監聽器,來響應這個接收的事件,這也是最爲靈活的一種交互方式。

3.1 在原生端經過 RCTDeviceEventEmitter 發送事件

//定義向 RN 發送事件的函數
public void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) {
    reactContext
        .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
        .emit(eventName,params);
}

// 發送成功消息
public notifySuccessMessage(ReactContext reactContext, String msg) {
    WritableMap event = Arguments.createMap();
    event.putString("message", msg);
    sendEvent(reactApplicationContext, "SUCCESS", event);
}

// 發送失敗消息
public notifyErrorMessage(ReactContext reactContext) {
    WritableMap event = Arguments.createMap();
    sendEvent(reactApplicationContext, "ERROR", event);
}
複製代碼

3.2 在 JS 端註冊監聽器,並在合適的時機移除監聽器

componentDidMount() {
  // 收到監聽
  this.listener = DeviceEventEmitter.addListener('SUCCESS', (message) => {
    // 收到監聽後想作的事情,’SUCCESS‘ 必須與原生層傳遞的 eventName 一致
    console.warn(message);
    dosomething...
  });
  this.errorListener = DeviceEventEmitter.addListener('ERROR', (message) => {
    // 收到監聽後想作的事情,’ERROR‘ 必須與原生層傳遞的 eventName 一致
    console.warn(message);
    dosomething...
  });
}
componentWillUnmount() {
  // 移除監聽
  if (this.listener) { this.listener.remove() }
  if (this.errorListener) { this.listener.remove() }
}
複製代碼

以上能夠看出發送事件能夠在任什麼時候機,調用 sendEvent() 方法便可。但並非任什麼時候機均可以順利得到 ReactContext 上下文對象,因此最好將發送事件的方法封裝成一個工具類,在 App 生命週期較早的時機進行初始化,傳入 ReactContext 上下文對象,而後便可在想發送事件的時機,只傳遞事件名和數據便可。

Update:

上面的寫法中在 Js 端使用的是 DeviceEventEmitter.addListener(eventName, function) 方法註冊的監聽器,這是由於安卓端使用了 DeviceEventManagerModule.RCTDeviceEventEmitter.class 類完成的事件發送。但 iOS 端發送事件時,使用上面的方法註冊監聽器是沒法響應的,這是由於在發送事件時使用的是 RCTEventEmitter 類,這個類中也是調用了 RCTDeviceEventEmitter 類完成的事件發送,但在發送前檢查了註冊的監聽器數量:

if (_listenerCount > 0) {
   [_bridge enqueueJSCall:@"RCTDeviceEventEmitter"
               method:@"emit"
                 args:body ? @[eventName, body] : @[eventName]
           completion:NULL];
}
複製代碼

爲了兼容兩端,因此 JS 端應該使用 NativeEventEmitter.addListener(eventName, function) 方法來註冊監聽器,完整的 JS 端代碼:

const eventEmitter = NativeModules.EventEmitter;
const nativeEmitter = new NativeEventEmitter(eventEmitter);
const subscription = nativeEmitter.addListener(
   eventEmitter.SUCCESS,
   message => {
       console.warn(message);
       dosomething...
   }
);
複製代碼

能夠看出 JS 端的代碼是經過使用自定義 Module 完成的監聽器註冊,因此安卓端也應該增長一個自定義 Module,代碼以下:

public class ReactEventEmitterModule extends >ReactContextBaseJavaModule {

   public ReactEventEmitterModule(ReactApplicationContext reactContext) {
       super(reactContext);
   }

   @Override
   public Map<String, Object> getConstants() {
       Map<String, Object> constants = new HashMap<>();
       constants.put("SUCCESS", "ThisIsSuccessConstant");
       return constants;
   }

   @Override
   public String getName() {
       return "EventEmitter";
   }
}
複製代碼

原文連接: tech.meicai.cn/detail/96 也可微信搜索小程序「美菜產品技術團隊」,乾貨滿滿且每週更新,想學習技術的你不要錯過哦。

相關文章
相關標籤/搜索