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