rn+与android+交互,「React Native」与「Android」的交互方式总结

React Native 作为一个混合开发解决方案,因为业务、性能上的种种原因,总是避免不了与原生进行交互。在开发过程中我们将 RN 与原生交互的几种方式进行了梳理,按照途径主要分为以下几类:

通过原生 Module 进行交互

通过原生 View 进行交互

通过发送事件 Event 进行交互

一、通过原生 Module 进行交互

通过原生 Module 进行交互是最高频的使用方式。封装原生 Module 可以将定义好的原生方法交给 RN 在 JS 端进行调用,JS 端可以在调用方法时通过传参的方式直接将数据传输给原生端,而原生端可以在方法执行过后将需要返回的数据通过 Promise 或者 Callback 将数据返回给 JS 端。

1.1 封装原生 Module

封装原生 Module 的步骤包括以下几步:

创建自定义 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,完成注册。

public class MyReactPackage implements ReactPackage {

@Nonnull

@Override

public List createNativeModules(@Nonnull ReactApplicationContext reactContext) {

List modules = new ArrayList<>();

modules.add(new MyModule(reactContext)); // 将新建的 MyModule 实例加入到 List 中完成注册

return modules;

}

@Nonnull

@Override

public List createViewManagers(@Nonnull ReactApplicationContext reactContext) {

return Collections.emptyList();

}

}

复制代码

1.1.3 注册自定义 Package

仅仅完成了自定义 Module 在自定义 Package 的注册还不能让 RN 使用我们的 Module,我们需要将刚刚新建的 ReactPackage 实例注册到 ReactApplication 的 ReactNativeHost 实例中。在默认的 RN 工程下,ReactApplication 通常为 MainApplication,你也可以让自己原有安卓项目中的 Application 类实现 ReactApplication 接口。

public class MainApplication extends Application implements ReactApplication {

private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {

@Override

public boolean getUseDeveloperSupport() {

return BuildConfig.DEBUG;

}

@Override

protected List getPackages() {

return Arrays.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 与原生的几种通信方式。

1.2 JS 端获取原生端自定义 Module 预设的常量值

在自定义 Module 中,我们可以通过重写 getConstants() 方法,返回一个 Map。这个 Map 的 key 为 RN 中可以被访问的常量名称,value 为预设的常量值。

private static final String CUSTOM_CONST_KEY = "TEXT";

@Nullable

@Override

// 获取模块预定义的常量值

public Map getConstants() {

final Map constants = new HashMap<>();

constants.put(CUSTOM_CONST_KEY, "这是模块预设常量值");

return constants;

}

复制代码

在 RN 中使用 Text 组件调用这个常量显示内容:

{ customModule.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); // 显示: 这是从原生返回的字符串

})

复制代码

二、通过原生 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 {

@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 createNativeModules(@Nonnull ReactApplicationContext reactContext) {

List modules = new ArrayList<>();

modules.add(new MyModule(reactContext));

return modules;

}

@Nonnull

@Override

public List createViewManagers(@Nonnull ReactApplicationContext reactContext) {

List 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 (

);

}

...

复制代码

2.2 原生端通过 @ReactProps 将方法暴露给 JS 端,接收 JS 端为组件设定的属性

在刚刚建立的 ViewManager 类中,我们可以通过 @ReactProps 注解方法,为组件添加属性,这里我们为 MyButton 添加 text 属性:

public class MyButtonViewManager extends SimpleViewManager {

@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 (

);

}

复制代码

2.3 原生端通过注册 View 事件与 JS 端的映射,使 JS 端可以接收原生端发送的事件和数据

有些时候我们不仅仅需要将数据以属性值的形式,从 JS 端传输到原生端,还需要原生端对 JS 端发送数据完成交互,这时我们需要使用事件 Event,通过发送事件完成交互。这里我们可以将 MyButton 的点击事件通知给 JS 端:

public class MyButtonViewManager extends SimpleViewManager {

@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 (

{

// 这里接收 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 (

ref={view => this.nativeUI = view}

text='这是个按钮'

onReactClick={data=>{

this.changeButtonText; // 点击时回传给原生端命令

}}/>

);

}

复制代码

在原生端,我们需要在 ViewManager 中重写 getCommandsMap() 方法建立命令的映射表,然后重写 receiveCommand() 方法完成接收命令后的操作

public class MyButtonViewManager extends SimpleViewManager {

@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 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;

}

}

}

复制代码

三、通过发送事件 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 getConstants() {

Map constants = new HashMap<>();

constants.put("SUCCESS", "ThisIsSuccessConstant");

return constants;

}

@Override

public String getName() {

return "EventEmitter";

}

}

复制代码

原文链接:tech.meicai.cn/detail/96

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值