「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 也可微信搜索小程序「美菜产品技术团队」,干货满满且每周更新,想学习技术的你不要错过哦。

相关文章
相关标签/搜索