React Native踩坑:集成到現有Android原生應用、RN與Android相互調用

前言

本來打算從開發環境搭建開始寫的,但是網上已經有挺多這類文章了,同時RN與Android相互調用這類文章也比較多,但是我翻來翻去都是那幾個例子,而且沒法解決我的問題 RN調用Android方法出錯,找不到我的原生模塊 ,所以從這個集成到現有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
在這裏插入圖片描述
然後將你的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 "[email protected]" has unmet peer dependency "[email protected]".

這是正常現象,意味着我們還需要安裝指定版本的 React:

yarn add react@16.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
在這裏插入圖片描述
輸入以下內容

<?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()
    }
}

完事

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章