最近在因为项目需求需要,需要在原本的Android工程中集成RN,用RN来开发需求经常变更的、变更周期短的业务。写下这篇文章用来记述集成过程中的细节注意点以及一些学习经验。本文主要介绍RN与Android原生之间的一些交互操作,以及原生中间件的封装流程。涉及如何调用原生接口、传参、获取回调值、获取常量值、调用原生UI、监听原生发送的事件、线程操作等。
一、自定义原生模块
- 创建自定义模块
- 注册自定义模块
- 在RN中使用自定义模块
- 获取原生模块预设常量值
- 导出带参函数方法
- 导出带参函数方法,并使用Callback回调函数返回结果信息
- 导出带参函数方法,并使用Promises返回结果信息
1、创建自定义模块
ReactNative在设计之初就考虑能够在其基础上通过原生代码封装来间接达到编写原生代码的能力。比如当我们需求在RN中调用原生的某个模块功能时,我们可以通过将原生代码封装成可以提供给RN调用的中间件形式,提供相应的功能。通常这样的原生模块需要继承ReactContextBaseJavaModule的Java类。
- 在Android项目中创建CustomModule.java,并继承自ReactContextBaseJavaModule类
- 实现初始化函数:
public CustomModule(ReactApplicationContext reactContext) { super(reactContext) }
- 实现
getName
方法,该方法返回自定义模块名称,在RN中我们将通过NativateModules.自定义模块名称
的形式访问该模块
public class CustomModule extends ReactContextBaseJavaModule {
public CustomModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
// 设置模块名称,需要与RN中调用时的模块名称保持一致
public String getName() {
return "CustomModule";
}
}
完成上述步骤我们就简单创建了一个提供RN使用的原生功能组件,但是现在RN中还不能够直接调用该模块。我们还需要向RN注册该模块,将模块的功能代码注入到JavaScript中,最终才能在RN中才能够使用。接下来我们去注册模块...
2、注册自定义模块
我们通过ReactPackage类的createNativeModules
方法中添加自定义模块实例,实现自定义模块向RN的注册。为了方便后期项目中统一管理自定义模块的注册,我们创建一个AndroidReactPackage
管理类,并实现ReactPackage
类的构造方法。
public class AndroidReactPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new CustomModule(reactContext));
return modules;
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
完成模块注册的最后一步,就是将自定义的AndroidReactPackage添加到ReactPkage中。具体方法就是在MainApplication.java文件中的getPackages方法中添加AndroidReactPackage的实例。
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new AndroidReactPackage()
);
}
3、在RN中使用自定义原生模块
在原生项目中实现向RN中注册自定义模块之后,我们可以在JavaScript中通过NativeModules获取对应的模块。通常我们将原生模块封装成一个JavaScript模块,省去直接从NativeModules中获取对应模块的步骤。
// CustomModule.js
/**
* This exposes the native CustomModule module as a JS module. This has a
* function 'sendRequest' which takes the following parameters:
*
* 1. String parmas: A string with the parmas
*/
import { NativeModules } from "react-native";
module.exports = NativeModules.CustomModule;
注意:这儿NativeModules.后面的名称是原生模块中getName方法返回的字符串,必须要保持一致。
在JavaScript中引用封装的JS模块:
import CustomModule from '../NativeModules/CustomModule'
4、获取模块预设常量值
自定义模块的时候,通常我们的模块中会有一些预设的参数值。在RN中想要获取这些数值时,需要在原生的模块代码中实现getConstants
方法,该方法返回一个Map<String, Object>
。HashMap的Key值为RN中对应的属性名称,Value值为RN中对应的属性的值。
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>{ CustomModule.TEXT }</Text>
5、导出带参函数方法
使用@ReactMethod注解可以导出函数方法供JavaScript调用:
// 导出带参函数方法
@ReactMethod
public void sendRequest(String parmas) {
// To Do Something
Toast.makeText(getReactApplicationContext(), parmas, Toast.LENGTH_SHORT).show();
}
在JavaScript中调用函数方法,并传递参数:
import CustomModule from '../NativeModules/CustomModule'
CustomModule.sendRequest("This is a string of parma");
6、导出带参函数,并使用Callback返回结果
如果函数方法需要返回结果,那么可以使用Callback回调函数,将返回值传回给JavaScript:
// 导出带参函数方法,并使用Callback回调函数返回回调结果
@ReactMethod
public void sendRequest(String message, Callback success, Callback failture) {
try {
String parma1 = message;
String parma2 = "收到回调信息";
// 回调成功,返回结果信息
success.invoke(parma1, parma2);
}catch (IllegalViewOperationException e) {
// 回调失败,返回错误信息
failture.invoke(e.getMessage());
}
}
在JavaScript中调用带Callbak回调函数的方法,并返回结果信息:
CustomModule.sendRequest(
"这是带Callback回调的函数方法",
(parma1, parma2) => {
var result = parma1 + parma2;
console.log(result);
},
errMsg => {
console.log(errMsg);
}
);
7、导出带参函数,并使用Promises返回结果
除了设置Callback回调函数的方法外,RN还提供了设置桥接方法的最后一个参数为一个Promise,并搭配 ES2016(ES7)标准的async/await语法的方式来返回调用结果的方式。
private static final String E_FUNCTION_ERROR = "E_FUNCTION_ERROR";
// 导出带参函数方法,并使用Promise简化回调结果方法
@ReactMethod
public void sendRequest(String message, Promise promise) {
try {
String result = message + ",收到回调信息";
WritableMap map = Arguments.createMap();
map.putString("content", result);
// 回调成功,返回结果信息
promise.resolve(map);
}catch (IllegalViewOperationException e) {
// 回调失败,返回错误信息
promise.reject(E_FUNCTION_ERROR, e);
}
}
在JavaScript端调用上面方法后会返回一个promise对象。在一个声明了async的异步函数内使用await关键字来调用,并等待结果的返回。
async function testSendRequest() {
try {
var { content } = await CustomModule.sendRequest(
"这是使用Promise回调的函数方法",
);
console.log(content);
}catch (e) {
console.error(e);
}
}
testSendRequest();
二、自定义视图组件
ReactNative除了可以封装原生模块之外,还可以将原生UI视图封装成组件后供RN使用。接下来我们来说说如何封装一个原生的UI视图组件及其属性值设置、事件通知,视图跳转等。
- 创建自定义视图组件
- 注册自定义视图组件
- 封装对应的JavaScript组件代码
- 导出自定义视图组件属性设置器
- 处理自定义原生视图组件的事件通知
- 跳转Activity视图
1、创建自定义视图组件
在自定义原生模块的时候,我们知道第一步就是创建一个继承ReactContextBaseJavaModule类的子类。同样的,创建自定义原生UI视图需要被一个ViewMangager或者SimpleViewManager的派生类创建和管理。一个SimpleViewManager的子类包含许多公共属性,包括背景色、透明度、Flexbox布局等。每一个子类都是一个单例类。它们被NativeViewHierarchyManager所管理,在合适的时候NativeViewHierarchyManager会委托原生UI视图组件去更新相应的视图属性。 ViewMangager还会代理原生视图的所有委托,在适当的时候向RN发送对应的事件通知。
简单创建自定义按钮视图组件的具体步骤如下:
- 在Android项目中创建SimpleViewManager的子类。
<Button>
指定RCTCustomButton这个视图管理类所管理的对象类型是原生组件Button类型。- 实现
getName
方法,该方法返回自定义视图组件的名称- 实现
createViewInstance
方法,创建原生Button实例并返回。
package com.rnproject;
import android.widget.Button;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
public class RCTCustomButton extends SimpleViewManager<Button> {
private ThemedReactContext mReactContext;
@Override
public String getName() {
return "RCTCustomButton";
}
@Override
protected Button createViewInstance(ThemedReactContext reactContext) {
this.mReactContext = reactContext;
Button button = new Button(reactContext);
return button;
}
}
2、注册自定义视图组件
我们通过ReactPackage类的createViewManagers
方法中添加自定义视图组件实例,实现自定义视图组件向RN的注册。之前我们创建一个AndroidReactPackage
管理类,并实现了ReactPackage
类的构造方法。我们可以在这个Package中注册自定义的视图组件。
public class AndroidReactPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new CustomModule(reactContext));
return modules;
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
List<ViewManager> managers = new ArrayList<>();
managers.add(new RCTCustomButton());
return managers;
}
}
3、封装对应的JavaScript组件代码
在JavaScript中,我们通过requireNativeComponent
引入自定义视图组件,requireNativeComponent
接受的参数为视图组件的名称,与原生组件中getName
方法返回的字符串保持一致。
// RCTCustomButton.js
import { requireNativeComponent } from "react-native";
/**
* Composes `Button`.
*/
module.exports = requireNativeComponent("RCTCustomButton");
在RN布局中使用自定义视图组件:
import RCTCustomButton from '../RCTCustomButton'
<RCTCustomButton style = {{width:160,height:50}}/>
4、导出视图的属性设置器
使用 @ReactProp或者@ReactPropGroup注解可以将视图属性设置器的导出,提供RN设置原生视图控件属性的方法。
比如设置Button的Text属性:
// 导出视图属性设置器
@ReactProp(name = "text")
public void setText(Button button, String text) {
button.setText(text);
}
在RN布局代码中设置自动定义按钮组件的标题:
<RCTCustomButton text="原生自定义按钮组件" style = {{width:160,height:50}}/>
5、处理自定义原生视图组件的事件通知
我们知道原生按钮是有点击事件的,那么我们如何将按钮的点击事情传递给JavaScript端呢?RN如何来处理原生的事件通知呢?在原生我们封装的JavaScript组件代码中没有任何的处理事件通知的方法,那么接下来我们就去处理原生组件的事件通知。
- 首先我们需要将封装的JavaScript端的组件代码(RCTCustomButton.js)进行改动,将其封装成React组件
- 在Android原生视图组件代码(RCTCustomButton.java文件)中重写
addEventEmitters
方法,添加按钮点击事件监听通知- 在点击事件的实现方法中调用
getJSModule(RCTEventEmitter.class).receiveEvent
方法,传递事件通知。
receiveEvent
方法参数说明:
第一个参数通过getId()
方法将原生组件和React组件关联在一起
第二个参数是事件映射到JavaScript中的Key值
第三个参数是事件传递到JavaScript端的数据- 在Android原生视图组件代码(RCTCustomButton.java文件)中覆写
getExportedCustomBubblingEventTypeConstants
方法将事件通知映射到JavaScript端- 在JavaScript端的组件代码(RCTCustomButton.js)中实现映射的事件通知方法
- 在RN中重新封装RCTCustomButton.js代码,将其封装成React组件
// RCTCustomButton.js
import React, { Component } from 'react';
import { requireNativeComponent } from "react-native";
export default class RCTCustomButton extends Component {
render() {
return <CustomButton {...this.props}/>;
}
}
var CustomButton = requireNativeComponent("RCTCustomButton");
- 在Android原生视图组件RCTCustomButton.java中添加事件监听,关联视图组件并传递事件通知及其数据
private static final String EVENT_NATIVE_ONCLICK_NAME = "onNativeClick";
// 重写addEventEmitters方法,传递点击事件
@Override
protected void addEventEmitters(final ThemedReactContext reactContext, final Button button) {
super.addEventEmitters(reactContext, button);
// 添加按钮点击事件监听
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 返回数据
WritableMap dataMap = Arguments.createMap();
dataMap.putString("msg", "这是原生按钮点击事件");
// 传递事件及其数据
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
view.getId(),
EVENT_NATIVE_ONCLICK_NAME,
dataMap
);
}
});
}
注意:如果不重写addEventEmitters
方法,在createViewInstance
方法在添加事件监听也是可以的。但是优先级低于addEventEmitters
方法。
- 在Android原生视图组件代码(RCTCustomButton.java文件)中覆写
getExportedCustomBubblingEventTypeConstants
方法将事件通知映射到JavaScript端
// 覆写getExportedCustomBubblingEventTypeConstants方法,将事件映射到JavaScript端
@Nullable
@Override
public Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
return MapBuilder.<String, Object>builder().put(
EVENT_NATIVE_ONCLICK_NAME,
MapBuilder.of(
"phasedRegistrationNames",
MapBuilder.of(
"bubbled",
EVENT_JS_ONCLICK_NAME
)
)
).build();
}
踩坑啦,踩坑啦:这儿只需要更改EVENT_NATIVE_ONCLICK_NAME
对应原生组件事件Key值,EVENT_JS_ONCLICK_NAME
对应JS端事件的Key值,其他的直接复制粘贴,尤其是字符串“phasedRegistrationNames”
和“bubbled”
不要更改,不然会导致映射失败,点击按钮发现没有反应。
- 在JavaScript端的组件代码(RCTCustomButton.js)中实现映射的事件通知方法,绑定到封装的组件的方法上。
// RCTCustomButton.js
import React, { Component } from 'react';
import { requireNativeComponent } from "react-native";
export default class RCTCustomButton extends Component {
constructor(props){
super(props)
this._onJSClickEvent = this._onJSClickEvent.bind(this);
}
_onJSClickEvent(event: Event) {
if (!this.props.onClick) {
return;
}
// 获取原生事件传递的数据
this.props.onClick(event.nativeEvent.msg);
}
render() {
return <CustomButton {...this.props} onJSClick={ this._onJSClickEvent }/>;
}
}
var CustomButton = requireNativeComponent("RCTCustomButton");
完成上述步骤后,我们就可以通过onClick
属性方法回调原生组件的点击事件了O(∩_∩)O~)
在JS中调用我们自定义的原生组件:
import RCTCustomButton from '../RCTCustomButton'
<RCTCustomButton
text="原生自定义按钮组件"
style = {{width:160,height:50}}
onClick={(msg) => {
Alert.alert(msg);
}}
/>