【RN】ReactNative与原生交互之Android篇


最近在因为项目需求需要,需要在原本的Android工程中集成RN,用RN来开发需求经常变更的、变更周期短的业务。写下这篇文章用来记述集成过程中的细节注意点以及一些学习经验。本文主要介绍RNAndroid原生之间的一些交互操作,以及原生中间件的封装流程。涉及如何调用原生接口、传参、获取回调值、获取常量值、调用原生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设置原生视图控件属性的方法。
比如设置ButtonText属性:

    // 导出视图属性设置器
    @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)中实现映射的事件通知方法
  1. 在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");
  1. 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方法。

  1. 在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”不要更改,不然会导致映射失败,点击按钮发现没有反应。

  1. 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);
    }}
 />
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,398评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,680评论 2 59
  • 那只是一方小小的窗台。 那时的我必须爬上板凳,才能够找外面的世界。楼房并不高,但足以俯瞰并眺望远方。...
    Letsstart阅读 145评论 0 0
  • 儿子小时候真是多灾多难,总是碰到惊险的事。那时候我一个人带孩子,平时其本每天有男孩子来帮忙带的。有的岁数小点,有的...
    A寒秋阅读 497评论 12 13
  • 没有一个女人不美——一位色鬼朋友 在哈市,老房子是韶华已逝的美人马迭尔眼尾纹里有宁静风暴允许你进入她身体或者内心索...
    阿剑啊阅读 299评论 5 13