开发React Native Android原生组件

4,648 阅读3分钟

1.什么是React Native原生开发

先看一张React Native的技术架构图(图片来源)

对于一个简单的APP来说,我们只需要进行JS的开发即可(图中绿色的部分)。但是某些情况下,我们使用一些平台相关的原生能力,这时候就需要做RN原生开发(途中黄色的部分)。比如以下场景:

  • 需要使用原生的系统能力,但是React Native社区中找不到提供相关接口的组件,我们需要自己包一下;
  • 使用第三方的lib,比如IM、直播、广告等功能,官方提供了原生的库,我们将其包成RN原生模块后才能使用;
  • 遇到性能问题或需要特殊的UI动画效果,这种场景我们需要直接使用原生组件来提升性能。

当你掌握了RN原生开发,大部分的APP需求都可以满足了。

2.如何入手原生开发

RN的原生开发分为两种:

  • 原生模块开发(Native Modules)
  • 原生UI组件开发(Native UI Components)

从使用方式上很容易弄清两者的区别:

1.原生模块的使用

import {NativeModules} from 'react-native'
const {ModuleA} = NativeModules

ModuleA.show()

2.原生UI组件的使用

import {requireNativeComponent} from 'react-native'
const UIComponentB = requireNativeComponent("UIComponentB")

render () => <UIComponentB props={...}></UIComponentB>

这次主要讨论原生模块的开发,原生UI组件先放在一边

2.1.安卓原生模块开发

原生模块开发主要涉及到3个部分:

  • 业务相关原生代码
  • bridge原生代码
  • js代码

2.1.1 一个最简单的例子

拿FB官方的Toast例子来说明,我们需要一个提醒窗,使用安卓的原生Toast实现。

Step1 编写安卓原生业务代码

我们在项目目录android/app/src/main/java/your_package_dir/下创建一个ToastModule.java文件(与MainApplication.java文件平级)

// ToastModule.java

package com.your-app-name;

import android.widget.Toast;

import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;

import java.util.Map;
import java.util.HashMap;

public class ToastModule extends ReactContextBaseJavaModule {
  private static ReactApplicationContext reactContext;

  private static final String DURATION_SHORT_KEY = "SHORT";
  private static final String DURATION_LONG_KEY = "LONG";

  // 构造函数,没有特殊需求时照猫画虎即可
  ToastModule(ReactApplicationContext context) {
    super(context);
    reactContext = context;
  }
  
  // 模块名称,决定了在js中引用的模块名字
  @Override
  public String getName() {
    return "ToastExample";
  }
  
  // 可选方法,定义一些常量供js使用。
  @Override
  public Map<String, Object> getConstants() {
    final Map<String, Object> constants = new HashMap<>();
    constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT);
    constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG);
    return constants;
  }
  
  // 通过ReactMethod注释器将show方法暴露出去,供js使用
  @ReactMethod
  public void show(String message, int duration) {
    Toast.makeText(getReactApplicationContext(), message, duration).show();
  }
}

Step2 编写bridge原生代码

ToastModule.java同级目录创建一个CustomToastPackage.java文件

注意,createJSModules方法在React Native 0.47版本中移除了,所以在比较老的组件中可能会见到此方法,在0.47之后的版本汇总不再使用。现在只有 createViewManagers 和 createNativeModules两个方法

// CustomToastPackage.java

package com.your-app-name;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CustomToastPackage implements ReactPackage {

  // UI Components 在此注册
  @Override
  public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
    return Collections.emptyList();
  }

  // Native Modules 在此注册
  @Override
  public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
    List<NativeModule> modules = new ArrayList<>();

    modules.add(new ToastModule(reactContext));

    return modules;
  }

}

Step3 编写javascript代码

为了方便使用,我们一般会在js中把原生组件简单包装一下再使用。在JS代码中创建一个Toast.js文件

import {NativeModules} from 'react-native';
module.exports = NativeModules.ToastExample;

这样,我们就可以在RN项目中使用Toast组件了

import Toast from './Toast';

// 这里的Toast.SHORT使我们在原生代码中通过getConstants暴露出来的
Toast.show('Awesome', Toast.SHORT);

2.1.2 高级特性

在实际应用中上述例子只能称为一个玩具,其实是无法满足真实需求的。

通常情况下我们需要在js和原生代码之间有一个双向的交互,等待原生代码返回结果或异常;通过js注入一些钩子到原生代码中;监听原生代码抛出的事件,诸如此类。

好在RN在此方面提供了比较完整的解决方案,比如Callback, Promise, RCTDeviceEventEmitter等,利用这些特性,几乎可以满足所有需求,尽管有时候实现的会有些丑陋。

关于这些特性的介绍,本文中不再赘述,直接看FB的文档即可,传送门:Navtive Modules开发文档

2.2 更进一步,将组件发布到npm

通过上面的学习,我们几乎可以把任何原生功能集成到项目中。但是在实际项目中,还是不够的。

当我们开发多个RN工程时,会希望自己的RN原生组件能够像社区中的那些开源组件一样,通过yarn install安装后即可使用;在发现组件BUG后,只需要执行yarn upgrade react-native-xxx即可修复,从而不用在每个项目的原生代码中折腾。

因此,我们需要将原生模块发布到npm仓库中,方便维护和复用。

最近项目中正好有集成广告sdk的需求,以此为例谈一谈如何开发一个RN原生组件并发布到npm仓库中。

3.开发安卓广告RN原生组件并发布

此次我们集成了优量汇(广点通)以及穿山甲(头条)两个广告平台的sdk,本文中以集成优量汇举例。

3.1 初始化一个RN组件工程

使用react-native-create-library初始化一个RN组件工程,该工具会为我们创建一个react native组件工程骨架。

$ npm install -g react-native-create-library
$ react-native-create-library --package-identifier com.qhkj.rn.advert --platforms android,ios advert
$ mv advert react-native-advert

其中 com.qhkj.rn.advert是包名, advert是文件夹名称。

3.2 编写原生代码接入优量汇广告

3.2.1 独立广告sdk接入逻辑

为了能够在其他的纯原生项目中使用,把原生功能码放在单独的module中开发。 因此在项目中新建一个moduleqhkj-android-advert(可以使用android studio来创建 File->New->New Module->Android Library),并修改两个文件

#/android/settings.gradle
include ':qhkj-android-advert'

#/android/build.gradle
dependencies {
    ...
    implementation "com.facebook.react:react-native:+"
    api project(':qhkj-android-advert')
}

这里简单说明一下dependencies中,使用implementationapi关键字是有区别的。implementation是用来引用在工程内部使用的依赖,当把当前工程给提供给其它项目使用时,通过implementation引入的库是不能被外部项目使用的。而通过api引入的库的接口是可以供外部项目使用的。由于我们需要暴露qhkj-android-advert中的接口,所以此处使用api,而不是implementation.

目录结构

如上图,

  • qhkj-android-advert文件夹中为纯原生代码,用于集成各个平台的广告sdk,直接将优量汇的demo移植到工程中改一改即可,此处不做更多描述,具体可参考文末项目开源代码;
  • com.qhkj.rn.advert中为RN桥接代码,用于把原生广告能力暴露出去,包含一个Module文件和一个Package文件。

需要注意的是,在我们的android libaray qhkj-android-advert中除了Java代码外,我们还把资源文件如layout, drawable, xml, AndroidManifest.xml等全部集成进来,简化外部使用。

由于RNAdvertModule用到了几个高级特性,这里详细说明一下。

3.2.2 RNAdvertModule的实现

需求:

  • 对于激励视频这类广告来说,我们需要知道用户是否观看完了广告,以决定是否给予用户相应的激励和提示。显然,这是一个异步操作,我们需要Promise特性。
  • 另外,由于我们引入的广告sdk实际是以Activity的方式调用的,我们还需要在MainActivity和AdvertActivity之间传递数据。这里我们用到了安卓的startActivityForResult接口。
// RNAdvertModule.java

public class RNAdvertModule extends ReactContextBaseJavaModule {

  // 定义激励视频Activity的返回request值
  private static final int SHOW_REWARD_VIDEO_REQUEST = 2;

  // 定义一个全局promise对象,用于保存js传入的promise对象
  private Promise mAdvertPromise;

  // 定义一个activity事件监听器
  private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() {

    // 在此函数中处理广告activity的返回结果,并通过promise完成这个异步流程
    @Override
    public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent intent) {
      if (requestCode == SHOW_SPLASH_REQUEST
              || requestCode == SHOW_REWARD_VIDEO_REQUEST) {
        if (mAdvertPromise != null) {
          if (resultCode == Activity.RESULT_CANCELED) {
            // 调用js传入的promise.resolve方法
            mAdvertPromise.resolve(false);
          } else if (resultCode == Activity.RESULT_OK) {
            // 调用js传入的promise.resolve方法
            mAdvertPromise.resolve(true);
          }

          mAdvertPromise = null;
        }
      }
    }
  };

  public RNAdvertModule(ReactApplicationContext reactContext) {
    super(reactContext);

    // 将Activity事件处理器注册到MainActivity中
    reactContext.addActivityEventListener(mActivityEventListener);
  }


  @Override
  public String getName() {
    return "RNAdvert";
  }

  @ReactMethod
  public void init(ReadableMap config) {
    mConfig = config;
  }

  // 拉起激励视频的方法,注意这里的入参promise
  @ReactMethod
  public void showRewardVideo(final Promise promise) {
    Context context = getReactApplicationContext();
    Intent intent;

    mAdvertPromise = promise;

    // 随机拉起广点通或者穿山甲的激励视频广告
    double random = Math.random();
    if (random <= 0.5) {
      intent = new Intent(context, GDTRewardVideoActivity.class);
      intent.putExtra("app_id", mConfig.getString("gdtAppId"));
      intent.putExtra("pos_id", mConfig.getString("gdtRewardVideoPosId"));
    } else {
      intent = new Intent(context, TTRewardVideoActivity.class); // mContext got from your overriden constructor
      intent.putExtra("horizontal_rit", mConfig.getString("ttRewardVideoHPosId"));
      intent.putExtra("vertical_rit", mConfig.getString("ttRewardVideoVPosId"));
    }

    try {
      // 拉起广告Activity并接受返回结果
      getCurrentActivity().startActivityForResult(intent, SHOW_REWARD_VIDEO_REQUEST);
      // 禁止原生动画
      getCurrentActivity().overridePendingTransition(0, 0);
    } catch (Exception e) {
      // 处理异常,调用promise.reject
      mAdvertPromise.reject("拉起激励视频广告失败!", e);
      mAdvertPromise = null;
    }
  }

}

通过上述处理,我们js代码中即可同步调用showRewardVideo方法,并根据返回结果进行相应的处理。

import {NativeModules} from 'react-native'
const {RNAdvert} = NativeModules

try {
    const finish = await RNAdvert.showRewardVideo()
    if (finish) {
      Navigation.showToast({ message: '恭喜获得3个积分!' })
      dispatch(Actions.incPointProfile, { value: 3 })
      console.log('获得激励')
    } else {
      console.log('未获得激励')
    }
  } catch (err) {
    console.log(err)
  }

3.2.3 JS封装

作为一个react native组件,我们希望在使用时不要每次都引入NativeModules,或则希望把接口进行二次封装方便使用。

为此,我们可以在组件工程的index.js中在做一次封装

// react-native-advert/index.js
import { NativeModules } from 'react-native';

const { RNAdvert } = NativeModules;

export default RNAdvert;

我们在使用时就可以这样:

import Advert from 'react-native-advert'
...

3.2.4 支持ReactNative的Autolinking特性

ReactNative在0.60版本中引入了Autolinking,极大简化了引入原生组件的流程, 关于Autolinking特性的说明可参考《一文读懂ReactNative0.60的 Autolinking 新特性》

由于我们的安卓工程中使用了multi project结构,我们需要指定packageImportPath,否则autolink会使用错误的包名。

如果是IOS平台,需要加入.podspec文件,以支持Autolinking特性 创建一个react-native-advert/react-native.config.js文件,填入如下代码

// react-native-advert/react-native.config.js

module.exports = {
  dependency: {
    platforms: {
      android: {
        packageImportPath: 'import com.qhkj.rn.advert.RNAdvertPackage;',
      },
    },
  },
};

3.4 发布到npm仓库

npm仓库是javascript的包管理中心,全世界的开发者都把自己开发的js组件发布到这里。

我们需要把组件发布到npm仓库中,此后便可通过npm install / yarn install来使用。

3.4.1 注册并登录npm

1.在www.npmjs.com网站中创建你的npm账号

2.在终端中登录

这里需要注意,因为npm官方仓库下载慢的问题,我们通常会设置为淘宝的镜像,所以我们在登录npm仓库和发布时需要带上--registry=http://registry.npmjs.org来指定官方仓库地址

npm login --registry=http://registry.npmjs.org

你可以使用npm whoami命令来确认本地是否成功登陆认证成功

$ qhkj npm whoami
qianhaikeji

3.4.2 修改package.json文件

package.json文件中定义了组件名、版本、作者、描述、依赖等发布信息,你需要修改为自己的信息,比如:

{
  "name": "react-native-advert",
  "version": "1.0.1",
  "description": "A ReactNative Advert Component for android",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "react-native",
    "android",
    "advert",
    "gdt",
    "tt"
  ],
  "author": {
    "name": "qhkj",
    "email": "service@qianhaikeji.cn"
  },
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "git@github.com:qianhaikeji/react-native-advert.git"
  },
  "devDependencies": {
    "react": "16.9.0",
    "react-native": "^0.61.1"
  },
  "peerDependencies": {
    "react-native": ">=0.47"
  }
}

3.4.3 发布npm包

进入项目目录下

$ cd react-native-advert
$ npm publish --registry=http://registry.npmjs.org

发布成功后,进入项目页面查看是否发布成功:www.npmjs.com/package/rea…

3.4.4 更新包版本后不生效的问题

在升级npm包的时候,很多人应该会碰到这个问题,自己明明在npm仓库中已经发布了新版本,但是在项目中使用yarn install或者yarn upgrade还是老版本,这种一般都是因为我们在本地配置了淘宝镜像源导致的。

淘宝的镜像源是定时拉取同步npm主站的资源,所以会有一定的滞后,我们需要手动同步一下。

1.打开npm.taobao.org/淘宝源网站

2.在右上角的搜索框中搜索你的包名,比如react-native-advert,进入项目页面

3.然后点击SYNC按钮,即可完成手动同步

3.5 项目开源地址

github.com/qianhaikeji…

欢迎留言交流~


关于我们

深圳市浅海科技有限公司

我们是一个高效、热情、有责任的技术团队,承接各种软件系统定制需求。

长期招聘远程开发者,如果您喜欢尝试新技术,有一点代码洁癖,能够使用文档进行高效的沟通,React/nodejs/ES6任意一种玩的飞起,那么,欢迎来撩~(想赚快钱的请绕道,谢谢)

简历请发送到:service@qianhaikeji.cn

当然,也欢迎甲方爸爸把项目甩我们脸上。添加微信:bdalbbtx