混合開發架構|Android工程整合React Native、Flutter、ReactJs

語言: CN / TW / HK

架構設計說明

該篇文章,介紹並記錄在大前端混合架構開發中的重要細節和流程。通過在安卓原生工程中整合兩大主流混合框架React Native、Flutter,以及ReactJs[Vue],整合三類模組module的架構的混合設計。並分別在這些主流技術棧的業務創作中,自己造輪子、使用新穎架構設計及核心技術去實現。並在編碼過程中還會創造常用工具,沉浸式狀態列底部導航欄、Flutter熱更新、Flutter多入口、 | tab1| tab2 | tab3 | tab4 |tab5| |:--------:| :-------------:|:-------------:|:----------:|:-----:| | 仿招商銀行首頁| 仿即時通訊|仿工商銀行首頁|仿抖音我的頁面|仿唯品會分類|

在原生工程中建立一個首頁,在首頁中使用五個TAB - TAB1,使用原生Java+Kotlin編碼,仿招商銀行首頁,使用優秀架構設計,完成列表各個模組的獨立解耦。 - TAB2,使用原生Java+Kotlin編碼,仿微信,通過Android Socket實現IM的即時通訊。 - TAB3,使用React Native編碼,仿工商銀行首頁。 - TAB4,使用Flutter編碼,仿抖音我的頁面。 - TAB5,使用ReactJs編碼,仿唯品會分類頁面。

建立安卓原生工程

| Android Studio版本 | gradle 外掛版本 |gradle 版本 |kotlin版本|JDK 版本 |其他 | |:--------:| :-------------:|:-------------:|:-------------:|:-------------:|:-----| | 3.6| 3.6.0 |gradle-6.7.1-all.zip(原5.6.4) |1.5.31|JDK11|compileSdkVersion 31、buildToolsVersion "30.0.0" 、minSdkVersion 21、targetSdkVersion 30|

建立Flutter

| Android Studio版本 | gradle 外掛版本 |gradle 版本 |JDK 版本 |其他 | |:--------:| :-------------:|:-------------:|:-------------:|:-------------| | 3.6| 3.6.0 |gradle-5.6.4-all.zip |JDK11|compileSdkVersion 31、buildToolsVersion "30.0.0" 、minSdkVersion 21、targetSdkVersion 30|

整合嵌入原生工程

詳盡整合介紹,請移步檢視

建立React Native

與建立Flutter相比較,React Native工程建立時,複雜很多。建立時候會遇到建立失敗問題,成功建立後,啟動Metro服務也會遇到報錯問題。而這些問題都與nodejs版本React Naitve版本有關。請詳細閱讀官方搭建環境的文件

React Native 建立指令:npx react-native init hibrid_rn --version 0.67.0 Metro服務啟動指令:npx react-native start Android apk 編譯安裝指令:yarn android

| node版本| React Native版本 |JDK 版本 |說明| |:--------:| :-------------|:-------------|----------| | v16.17.0| 0.67.0 |JDK11|詳細版本號,請移步檢視程式碼|

解決RN報錯問題

若有報錯,下面

如下報錯資訊,則是node版本號使用不當導致~ /node_modules/@react-native-community/cli/build/commands/doctor/healthchecks/index.js:48 } catch {} ^ SyntaxError: Unexpected token { at createScript (vm.js:80:10) at Object.runInThisContext (vm.js:139:10) at Module._compile (module.js:617:28) at Object.Module._extensions..js (module.js:664:10) ... ... ...

若有報錯,下面 Could not find react-native-0.71.0-rc.0-debug.aar (com.facebook.react:react-native:0.71.0-rc.0). ```dart Could not determine the dependencies of task ':app:lintVitalRelease'.

Could not resolve all artifacts for configuration ':app:debugCompileClasspath'. Could not find react-native-0.71.0-rc.0-debug.aar (com.facebook.react:react-native:0.71.0-rc.0). ```

解決方案, 指定ReactNative確定版本號!!修改ReactNativeapp/build.gradleimplementation "com.facebook.react:react-native:+" 改為implementation "com.facebook.react:react-native:0.67.0"

若有報錯,下面 Execution failed for task ':app:mergeDebugNativeLibs'. More than one file was found with OS independent path 'lib/x86_64/libfbjni.so'

在這裡插入圖片描述 解決方案,app/build.gradle android{} 中新增截圖中報錯的如lib/x86_64/libfbjni.so

dart packagingOptions { pickFirst 'lib/x86/libc++_shared.so' pickFirst 'lib/x86_64/libc++_shared.so' pickFirst 'lib/armeabi-v7a/libc++_shared.so' pickFirst 'lib/arm64-v8a/libc++_shared.so' pickFirst 'lib/x86/libfbjni.so' pickFirst 'lib/x86_64/libfbjni.so' // 截圖中有這個報錯,這裡新增該so修復 pickFirst 'lib/armeabi-v7a/libfbjni.so' pickFirst 'lib/arm64-v8a/libfbjni.so' }

整合嵌入原生工程

```groovy

// 【app/build.gradle】下進行配置 // 【配置共三步】rn第一步配置:start project.ext.react = [ entryFile : "index.android.js", enableHermes: false, bundleInDebug:true, bundleInBeta:true ]

def enableHermes = project.ext.react.get("enableHermes", false); def jscFlavor = 'org.webkit:android-jsc:+'

def safeExtGet(prop, fallback) { rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback } // 【配置共三步】rn第一步配置:end dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) ...... // 【配置共三步】rn第二步配置:start if (enableHermes) { def hermesPath = "../../hibrid_rn/node_modules/hermesvm/android/"; debugImplementation files(hermesPath + "hermes-debug.aar") releaseImplementation files(hermesPath + "hermes-release.aar") } else { implementation jscFlavor } implementation "com.facebook.react:react-native:+" // From node_modules implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" // 【配置共三步】rn第二步配置:end }

// 【project/build.gradle】下進行配置 allprojects { repositories { ...... // 【配置共三步】rn第三步配置:start maven { // All of React Native (JS, Android binaries) is installed from npm url "$rootDir/../hibrid_rn/node_modules/react-native/android" } maven { // Android JSC is installed from npm url("$rootDir/../hibrid_rn/node_modules/jsc-android/dist") } //【配置共三步】rn第三步配置:end

}

} ```

引數欄位說明: - entryFile : "index.android.js",表示配置載入安卓資源入口檔名稱。 - def hermesPath = "../../hibrid_rn/node_modules/hermesvm/android/",表示當enableHermes==true時,引入執行引擎Hermes。否則,使用JavaScriptCore執行引擎。

Hermes 是一個可選的 React Native 功能。如果要啟用Hermes,需要確保 React Native專案的版本在0.60.2版本 以上,並且還需要對android/app/build.gradle 做以下更改。我們這裡配置enableHermes: false

groovy project.ext.react = [ entryFile: "index.js", enableHermes: true // 配置開啟Hermes引擎 ]

RN整合後,啟動報錯

在原生工程中集成了RN之後,將React Native作為原生工程Activity下LayoutView的一部分,測試整合狀態。發現從Native啟動React Native報以下錯誤~

ReactNative: Exception in native call java.lang.RuntimeException: Unable to load script. Make sure you're either running Metro (run 'npx react-native start') or that your bundle 'rn/index.android.bundle' is packaged correctly for release.

在這裡插入圖片描述 報錯資訊說,Metro服務未啟動,或者說找不到bundle資源包。而事實是當前Metro服務已啟動,且直接啟動React Native工程是OK的。對此尋到以下兩種解決方案 : - 對當前React Native工程程式碼進行打包,並將打包後的bundle資源拷貝到Native工程的/main/assets/rn目錄下。然後在Native執行則無問題。 - VSCode終端執行打包指令: react-native bundle --platform android --dev false --entry-file index.js --bundle-output ../HybridArcPro/app/src/main/assets/rn/index.android.bundle --assets-dest ../HybridArcPro/app/src/main/res/ - 非打包處理。需要對Native和React Native端同時進行配置。①對安裝的debug包APP配置服務IP和埠號。②配置網路許可權,application標籤中配置 tools:targetApi="28" android:allowBackup="true"。③啟動Metro服務。

在這裡插入圖片描述

底部導航欄架構設計

使用java語言,自定義首頁底部導航欄佈局控制元件(下圖實現效果+導航原始碼),自定義UML介紹~ 在這裡插入圖片描述

  • TabBtnLayoutBottomNav 底部導航欄佈局自定義View。繼承自FrameLayout,實現自介面ITabLayout。底部導航欄佈局,內部擺放TabBtnBottom
  • TabBtnBottom底部導航欄佈局中的單個Tab。繼承自RelativeLayout,實現自I介面ITab(ITab繼承了點選事件的監聽介面OnTabSelectedListener)。
  • TabBtnLayoutBottomNav 內部封裝單個Tab集合List<OnTabSelectedListener>(包含所有TabBtnBottom和TabBtnLayoutBottomNav新增的監聽OnTabSelectedListener),當用戶點選Tab時,點選事件通過TabBtnBottom.setOnClickListener觸發集合List的遍歷,此時將點選事件傳遞給每個TabBtnBottomz,同時TabBtnLayoutBottomNav新增的監聽回撥。由此單個Tab和TabBtnLayoutBottomNav產生了點選事件的關聯,並能為整合fragment點選切換顯示做下伏筆。 在這裡插入圖片描述在這裡插入圖片描述
  • TabBtnFragmentLayout,顯示fragment頁面的自定義佈局控制元件,放在佈局檔案TabBtnLayoutBottomNav中。由TabBtnLayoutBottomNav新增的監聽回撥index,方法setCurrentItem獲得指示並顯示相應fragment頁面。
  • TabBtnFragmentAdapter,顯示fragment頁面的介面卡類。具體指示顯示fragment頁面邏輯,實現在方法instantiateItem中。 在這裡插入圖片描述

原生仿招商銀行首頁

原生Android Socket 即時通訊

React Native仿工商銀行首頁

使用RN仿工商銀行首頁(圖示自己費勁吧啦找的),然後實現StatusBar和TitleBar**滑動漸變**效果。此處由原生啟動並開啟RN,效果如下~

在這裡插入圖片描述

原生傳遞props初始物件,RN使用

```dart // React Native中配置bundle val bundle = Bundle() rnBundle.putCharSequence("device-info","裝置資訊物件") rnBundle.putCharSequence("state","使用者登入狀態") mReactRootView!!.startReactApplication(mReactInstanceManager, "hibrid_rn", bundle)

// 在對應的ReactNative的Coponent中獲取,則可通過this.props得到! ```

原生與RN通訊

已實現效果介紹

在Native端橋接類JRNBridge.kt中定義了一個呼叫安卓Toast的方法,供ReactNative端呼叫。效果如下 在這裡插入圖片描述

開發中注意事項

這裡以本專案作為示例,介紹下在實現RN和C端通訊時,開發步驟邏輯。 1, 邏輯原始碼C端中定義橋接類工具JRNBridge.kt。橋接類繼承自ReactContextBaseJavaModule.java,實現方法getName—— 獲取到的name會在ReactNative端使用,

dart // bridge/index.js import {NativeModules} from 'react-native' module.exports = NativeModules.RNBridge // 這裡的 RNBridge 就是getName得到。

2, ReactNative和C端Native進行通訊的方法,需要加註解@ReactMethod

3, 建立JReactPackage.kt 繼承ReactPackage.kt,重寫方法createNativeModules和createViewManagers。重寫方法createNativeModules是將JReactPackage新增到NativeModule列表為之後註冊。 | createNativeModules |createViewManagers | |:--------| :-------------| | ReactNative呼叫Native方法時重寫並新增NativeModule | Native UI,作為ReactNative UI是重寫並新增ViewManager |

4, 將MainReactPackage()和JReactPackage()添加註冊到ReactInstanceManager中。 其中MainReactPackage()及JReactPackage()必須, 否則報錯'StatusBarManager' could not be found. Verify that a module by this name is registered in the native binary. 在這裡插入圖片描述

JReactPackage()否則報錯, 找不到RNBridge.toast({toast:'正在取號中,請稍後...'})

5, ReactNative中匯出在NativeModules中已註冊的JRNBridge。然後在ReactNative各個地方引入並使用。 在這裡插入圖片描述

ReactNative端原始碼 dart import RNBridge from '@bridge/index' <TouchableOpacity onPress={()=>{RNBridge.toast({toast:'正在取號中,請稍後...'})}} >

Flutter仿抖音我的頁面

Flutter熱更新

Flutter熱更新,通過動態.so檔案的載入實現。

.so檔案動態載入實現思路 以反射修改FlutterLoader.java類FlutterApplicationInfo.aotSharedLibraryName的值,從而修改了FlutterLoader將要載入的原libapp.solibapp**.so。之後,將用來替換的libapp**.so拷貝到原libapp.so所在的目錄即可。拷貝的方式,如首先打包一個新的release-apk,然後解壓提取出此時的libapp.so檔案(修改名稱為libapp**.so),放到目錄assets/下。之後,當執行程式碼拷貝時,會將assets/目錄下的so包拷貝到新指定的將會載入的libapp.so所在的目錄,然後則順理成章完成熱更新。

在這裡插入圖片描述

在這裡插入圖片描述

多FlutterEngine引擎建立,多dart入口實現

flutter一個投資理財頁,作為第二個dart入口。並通過下面定義的對應引擎啟開。 在這裡插入圖片描述

多FlutterEngine引擎建立

通過FlutterEngine建立引擎例項物件。建立時傳入的JFlutterLoader,重新定義了原FlutterLoader獲取dart程式碼包的方式。之後,根據給定的DartEntrypoint開始執行Dart程式碼。並快取已建立的Flutter引擎例項。其中傳入給DartEntrypointmoduleName是dart入口名稱findAppBundlePath是flutter資產目錄flutterAssetsDir ```kotlin // 初始化,根據moduleName(dart入口名稱)建立多個Flutter引擎 private fun initFlutterEngine(context: Context, moduleName: String): FlutterEngine? { var flutterEngine:FlutterEngine = FlutterEngine(context, JFlutterLoader.get(), FlutterJNI())

flutterEngine.dartExecutor
        .executeDartEntrypoint(DartExecutor.DartEntrypoint(JFlutterLoader.get().findAppBundlePath(), moduleName))
FlutterEngineCache.getInstance().put(moduleName, flutterEngine) // 快取起來
return flutterEngine

} ```

多dart入口建立

```kotlin // 在flutter的dart程式碼中main.dart // 至少有一個預設入口,如 'main' void main() { runApp(const MyApp()); init(); }

// 此時,可仿照預設入口,通過註解,建立多個不同的dart入口 - 工行的'投資理財'詳情頁面 @pragma('vm:entry-point') void finance() { runApp(FinanceEntryApp()); } ```

結合Flutter原始碼,分析從原生端啟動Flutter

原生與Flutter通訊

在這裡插入圖片描述 型別 | 說明 -------- | ----- MethodChannel | 用於傳遞方法呼叫invokeMethod一次性通訊:如Flutter呼叫埋點功能。

這裡且介紹MethodChannel,在Native與Flutter間如何通訊~Flutter側傳送Native側接收處理。 Flutter側傳送,結合原始碼看,通過建立一個MethodChannel例項並指定渠道名稱name。且兩側的name須一致相同。然後使用MethodChannel例項呼叫執行方法invokeMethod,該方法傳入Native側將被呼叫方法名稱method及通訊訊息內容arguments。之後,便啟動了由Flutter向Native側傳遞呼叫。 Native側接收處理,建立一個與Flutter側渠道名稱name相同的MethodChannel例項。使用MethodChannel例項呼叫執行方法setMethodCallHandler,用以匹配Flutter側方法名稱method接收處理Flutter側傳送來的資訊。詳盡設計實現,請移步檢視

ReactJs仿唯品會分類頁

詳盡開發介紹,請移步檢視

工程原始碼地址

點選進入倉庫,檢視工程原始碼