欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

React Native踩坑:集成到现有Android原生应用、RN与Android相互调用

程序员文章站 2022-07-02 22:09:29
...

前言

本来打算从开发环境搭建开始写的,但是网上已经有挺多这类文章了,同时RN与Android相互调用这类文章也比较多,但是我翻来翻去都是那几个例子,而且没法解决我的问题 RN调用Android方法出错,找不到我的原生模块 ,所以从这个集成到现有Android原生应用开始写一下,免得之后自己也忘了。

效果

React Native踩坑:集成到现有Android原生应用、RN与Android相互调用

搭建开发环境

这部分我的参考资料是官方中文网站和常见问题

主要出现问题会在新建项目那里
React Native踩坑:集成到现有Android原生应用、RN与Android相互调用

npm config set registry https://registry.npm.taobao.org --global
npm config set disturl https://npm.taobao.org/dist --global

还有就是要将react-native记录到环境变量里,不然以后会出现下面这种问题。

react-native:command not found
zsh: command not found: react-native

参考方法:@曹九朵_ 配置reactNative(RN)过程中 出现react-native:command not found 和 zsh: command not found: react-native

集成到现有Android原生应用

这里我们就新建一个空白的Android来模拟现有的Android应用,这部分不详细描述了,相信能找到这篇文章的都是Android开发者。
参考资料是官方说明:React Native 中文网 集成到现有原生应用

创建项目结构

首先我们需要创建一个目录,名字是你的项目名称,如ReactNativeTest
React Native踩坑:集成到现有Android原生应用、RN与Android相互调用
然后将你的Android项目整个复制到这个文件夹里面,然后修改Android项目文件夹名称为android,这样你就得到了以下这种文件目录

  • ReactNativeTest
    • android
      • app
      • build
      • gradle
      • … …

接着,继续在ReactNativeTest里创建一个新文件package.json

{
  "name": "ReactNativeTest",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "android": "react-native run-android",
    "start": "react-native start"
  }
}

示例中的version字段没有太大意义(除非你要把你的项目发布到 npm 仓库)。scripts中是用于启动 packager 服务的命令。

安装 React 和 React Native 模块

接下来我们使用 yarn 或 npm(两者都是 node 的包管理器)来安装 React 和 React Native 模块。请打开一个终端/命令提示行,进入到项目目录中(即包含有 package.json 文件的目录),然后运行下列命令来安装:

yarn add react-native

这样默认会安装最新版本的 React Native,同时会打印出类似下面的警告信息(你可能需要滚动屏幕才能注意到):

warning "aaa@qq.com" has unmet peer dependency "aaa@qq.com".

这是正常现象,意味着我们还需要安装指定版本的 React:

yarn add aaa@qq.com16.11.0

注意必须严格匹配警告信息中所列出的版本,高了或者低了都不可以。

把 React Native 添加到 Android 应用中

添加依赖

打开Android项目,在app的build.gradle中输入如下内容

apply plugin: 'com.android.application'
...

// 这个不用会报错
project.ext.react = [
        enableHermes: false,  // clean and rebuild if changing
]
def jscFlavor = 'org.webkit:android-jsc:+'
def enableHermes = project.ext.react.get("enableHermes", false);

android {
    ...
}

dependencies {
    ...
    // 这个不用会报错
    implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
    
    // 下面用+号代表版本,会直接引用模块内的版本
    implementation "com.facebook.react:react-native:+"

    // 这个不用会报错
    if (enableHermes) {
        def hermesPath = "../../node_modules/hermes-engine/android/";
        debugImplementation files(hermesPath + "hermes-debug.aar")
        releaseImplementation files(hermesPath + "hermes-release.aar")
    } else {
        implementation jscFlavor
    }
}

在项目的build.gradle中输入以下内容


buildscript {
    ...
}

allprojects {
    repositories {
        ...

        maven {
            // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
            url("$rootDir/../node_modules/react-native/android")
        }
        maven {
            // Android JSC is installed from npm
            url("$rootDir/../node_modules/jsc-android/dist")
        }
        
    }
}
...

配置权限

接着,在 AndroidManifest.xml 清单文件中声明网络权限:

<uses-permission android:name="android.permission.INTERNET" />

Network Security Config (API level 28+)

在Android9为target的项目上要加Network Security Config。
在res文件夹中创建xml文件夹,再创建network_security_config.xml
React Native踩坑:集成到现有Android原生应用、RN与Android相互调用
输入以下内容

<?xml version="1.0" encoding="utf-8"?>
<network-security-config xmlns:android="http://schemas.android.com/apk/res/android">
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="false">localhost</domain>
        <domain includeSubdomains="false">10.0.2.2</domain>
        <domain includeSubdomains="false">10.0.3.2</domain>
    </domain-config>
</network-security-config>

再打开AndroidManifest.xml,在application节点里输入

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.dlong.reactnativetest">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        ...
        android:networkSecurityConfig="@xml/network_security_config"
        tools:targetApi="n">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
    </application>

</manifest>

编写 React Native 组件

ReactNativeTest文件夹里创建一个新文件index.js,输入以下内容

import React from 'react';
import {AppRegistry, StyleSheet, Text, View} from 'react-native';

class HelloWorld extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.hello}>Hello, World</Text>
      </View>
    );
  }
}
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
  },
  hello: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
});

AppRegistry.registerComponent('ReactNativeTest', () => HelloWorld);

Activity 中插入 React Native 组件

回到 Android studio 打开页面布局activity_main.xml,修改成以下内容

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/darker_gray"
        tools:context=".MainActivity">

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="以下是接入RN页面"
            android:textColor="@color/colorAccent"
            android:textSize="24sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:onClick="clickButton"
            android:text="点击+1"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.498"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView" />

        <LinearLayout
            android:id="@+id/ll_view"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginTop="16dp"
            android:orientation="vertical"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/button" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

我里面用了DataBinding,不清楚的朋友可以查看我这篇文章
Android Kotlin学习 Jitpack 组件之DataBinding

接着编写MainActivity,主要就是启动一个react-native的组件


class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private lateinit var mReactRootView: ReactRootView
    private lateinit var mReactInstanceManager: ReactInstanceManager

    private var mTouchTime = 0

    companion object{
        private var mInstance: MainActivity? = null
        
        @JvmStatic
        fun getInstance() = mInstance
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mInstance = this
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        mReactRootView = ReactRootView(this)
        mReactInstanceManager = ReactInstanceManager.builder()
            .setApplication(application)
            .setCurrentActivity(this)
            .setBundleAssetName("index.android.bundle")
            .setJSMainModulePath("index")
            .addPackage(MainReactPackage())
            .setUseDeveloperSupport(BuildConfig.DEBUG)
            .setInitialLifecycleState(LifecycleState.RESUMED)
            .build()

        mReactRootView.startReactApplication(mReactInstanceManager, "ReactNativeTest", null);
        // 使用 ViewGroup 的 addView 方法将 react-native 组件加进来显示
        binding.llView.addView(mReactRootView)
    }

    @Synchronized
    fun clickButton(view: View) {
        mTouchTime ++
    }

测试集成结果

运行应用首先需要启动开发服务器(Packager)。你只需在项目根目录中执行以下命令即可

yarn start

USB连接手机,手机需要打开开发者调试,然后新打开一个终端输入

adb devices   

回复一个设备列表,查看设备列表

List of devices attached
c853ef19	device

继续输入

adb reverse tcp:8081 tcp:8081

回复

8081

最后运行Android项目,顺利的话应该能看到中间的Hello, World

Android 调用 RN

现在我们的目标是点击按钮+1,mTouchTime变量+1,RN组件实时显示mTouchTime变量的值,我们首先修改一下index.js

import React from 'react';
import {AppRegistry, StyleSheet, Text, View, DeviceEventEmitter, Button, NativeModules} from 'react-native';

class HelloWorld extends React.Component {
  constructor(){
    super();
    // 预设一个变量来显示原生代码传过来的值
    this.state = {
      strValue: "HelloWorld"
    }
  }
  UNSAFE_componentWillMount() {
    // 注册接收器
    this.updateListener = DeviceEventEmitter.addListener("update", e => {
      // 改变变量
      this.setState({
        strValue: e.strValue
      });
    });
  }
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.hello}>{this.state.strValue}</Text>
      </View>
    );
  }
}
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    backgroundColor: '#ffffff',
  },
  hello: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
    backgroundColor: '#00ff00',
  },
  buttonContainer: {
    margin: 20
  },
});

AppRegistry.registerComponent('ReactNativeTest', () => HelloWorld);

修改MainActivity


class MainActivity : AppCompatActivity() {
    ...

    @Synchronized
    fun clickButton(view: View) {
        mTouchTime ++
        updateTouchTimeUI()
    }

    /** Android调用RN */
    private fun updateTouchTimeUI() {
        val map = Arguments.createMap()
        map.putString("strValue", "$mTouchTime")
        mReactInstanceManager.currentReactContext
            ?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
            ?.emit("update", map)
    }
}

重新运行项目,点击按钮+1验证。能看到数字也+1显示。

RN 调用 Android

这个就比较复杂了,可以参考官方的这个介绍:React Native 中文网 原生模块
不过官方资料也是有坑的,我下面会有提到

创建Module

Module就是存放提供给RN调用的方法函数,继承了ReactContextBaseJavaModule,我们创建一个CustomModule,内容如下:

class CustomModule (
    reactContext: ReactApplicationContext
) : ReactContextBaseJavaModule(reactContext) {

    /** 这里返回的名字将是在RN中调用的变量名 */
    override fun getName(): String {
        return "CustomModule"
    }

    /** RN调用Android */
    @ReactMethod
    fun setIntValue(value: Int) {
        Log.e("测试", "$value")
        MainActivity.getInstance()?.setTouchTime(value)
    }
}

创建Package

创建文件CustomPackage

class CustomPackage : ReactPackage {

    override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
        val list = mutableListOf<NativeModule>()
        // 添加提供RN调用类
        list.add(CustomModule(reactContext))
        return list
    }

    override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<View, ReactShadowNode<*>>> {
        return emptyList()
    }
}

在 Application 中提供

打开你的 Application

class MainApplication : Application(), ReactApplication {


    override fun getReactNativeHost(): ReactNativeHost {
        return object : ReactNativeHost(this) {
            override fun getPackages(): MutableList<ReactPackage> {
                val list = mutableListOf<ReactPackage>()
                list.add(MainReactPackage(null))
                // 加入你的Package
                list.add(CustomPackage())
                return list
            }

            override fun getUseDeveloperSupport(): Boolean {
                return BuildConfig.DEBUG
            }

            override fun getJSMainModuleName(): String {
                return "index"
            }
        }
    }

    override fun onCreate() {
        super.onCreate()
        SoLoader.init(this, false)
    }
}

在 RN 组件中使用

修改index.js

import React from 'react';
import {AppRegistry, StyleSheet, Text, View, DeviceEventEmitter, Button, NativeModules} from 'react-native';

class HelloWorld extends React.Component {
  ...
  UNSAFE_componentWillMount() {
    // 注册接收器
    ...
  }
  // 点击事件
  _onPressButton() {
    NativeModules.CustomModule.setIntValue(0);
  }
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.hello}>{this.state.strValue}</Text>
        <View style={styles.buttonContainer}>
          <Button
            onPress={this._onPressButton}
            title="归0"
          />
        </View>
      </View>
    );
  }
}
const styles = StyleSheet.create({
  ...
  buttonContainer: {
    margin: 20
  },
});

AppRegistry.registerComponent('ReactNativeTest', () => HelloWorld);

官方介绍,和网上搜索出来的介绍都是到这里了,但是如果你发现你去测试,点击归0的时候都是报错的话,那你就要往下再看了,这摸索了我一天,头发都掉了不少

在 ReactInstanceManager 中增加 Package

修改MainActivity


class MainActivity : AppCompatActivity() {

    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...

        mReactRootView = ReactRootView(this)
        mReactInstanceManager = ReactInstanceManager.builder()
            .setApplication(application)
            .setCurrentActivity(this)
            .setBundleAssetName("index.android.bundle")
            .setJSMainModulePath("index")
            .addPackage(MainReactPackage())
            // 最重要的地方
            .addPackage(CustomPackage())
            .setUseDeveloperSupport(BuildConfig.DEBUG)
            .setInitialLifecycleState(LifecycleState.RESUMED)
            .build()

        ...
    }

    ...

    fun setTouchTime(time: Int) {
        mTouchTime = time
        updateTouchTimeUI()
    }
}

完事