上半年我定的OKR目標是幫助團隊將App切入Flutter,實現統一技術棧,變革成多端融合開發模式。Flutter目前是跨平臺方案中最有潛力實現咱們這個目標的,不論是Hybird仍是React Native,咱們的項目都有落地應用,跨平臺一直是終端團隊所追求的技術,可以快速研發和部署也是咱們不斷給本身提出的挑戰。Flutter是什麼我在這裏就很少說了,不少文章都有介紹,本篇文章想分享的是如何在原生工程中嵌入Flutter來實現混編,幫助團隊快速落地Flutter遷移,這個對小團隊來講應該會有必定借鑑意義。java
在接入Flutter以前須要具有如下前置條件:android
業內絕大部分的App都不可能推倒重來,因此混合工程的方式接入Flutter是目前主流開發模式,下面我簡單說說業界兩種工程管理模式:ios
統一管理模式(不推薦)git
三端分離模式(推薦)github
鹹魚方案:https://mp.weixin.qq.com/s/Q1z6Mal2pZbequxk5I5UYA?
官方方案:https://flutter.dev/docs/development/add-to-appshell
目前咱們採用的是以module的形式接入,由於咱們團隊人員少,溝通協做起來成本不大,初期直接源碼接入也方便咱們快速接入開發和調試。macos
flutter doctor
若是想確認你當前的環境是否ok,執行下flutter doctor
命令,基本能解決大部分問題。若是遇到一直卡住,說明你當前環境是不通的,檢查下代理是否配置正確。json
建立Flutter module工程
若是點擊Finish建立module一直卡死,說明仍是網絡問題,命令行輸入vi ~/.bash_profile
檢查下代理。若是實在不行,則經過命令行建立module:bash
flutter create -t module --org com.example my_flutter
Android原生工程集成Flutter網絡
一期咱們先接入Android工程,因此接下來主要以Android爲主,後續若是有iOS相關的實踐會補充到這裏。
先看下咱們的module工程:
目錄結構:
除了工程配置文件和自動生成的工程目錄以外,其餘文件都須要進行託管。
瞭解完工程目錄以後,咱們開始集成:
setting.gradle
,加入如下配置// 引入flutter module setBinding(new Binding([gradle: this])) // new evaluate(new File( // new settingsDir.parentFile, // new 'edu_flutter_module/.android/include_flutter.groovy' // new )) include ':edu_flutter_module' project(':edu_flutter_module').projectDir = new File('../edu_flutter_module')
能夠看到目前咱們依賴的flutter module,是在原生工程目錄同級的。
build.gradle
文件,在dependencies下加入如下配置:implementation project(":flutter")
ok,這兩步是官方的指引,配置完以後就完事了? 太天真了,還須要有一些額外的調整。構建一下就知道了:
異常1:Gradle DSL method not found: 'google()'
項目中用的gradle版本仍是比較舊的,須要升級一下:
異常2:AAPT error:resource android:attr/fontVariationSettings not found
這個異常須要將compileSdkVersion升級到28,以前是26。
異常3:assert appProject !=null
這個問題巨坑,咱們的主工程名是course
,但flutter的構建腳本是硬編碼爲app
,有兩種解決辦法:
這樣,flutter腳本就能找到咱們的工程,編譯也ok了。
但其實還有問題,由於目前咱們還未升級support包到AndroidX版本,而建立出來的module工程默認是支持AndroidX的,因此咱們須要進行降級,等後續升級工程以後再處理。
修改edu_flutter_module/pubspec.yaml
,將androidX改成false:
module: androidX: false androidPackage: com.tencent.edu iosBundleIdentifier: com.tencent.edu
改完這個以後,終於工程編譯經過了,但這就結束了嗎,還有坑等着你呢。
上一個主題咱們解決掉一些坑以後終於把flutter做爲一個module集成到咱們的工程中,接下來咱們嘗試寫個頁面嵌入到咱們頁面。
目前課堂用的flutter版本是:v1.12.13+hotfix.5
,這個版本的使用跟以前的版本會有些差別,能夠參考官方的wiki:
https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects
這裏我嘗試把課堂的首頁替換成Flutter頁面,作了如下調整:
@Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { // TODO: 2020-04-01 增長flutter視圖 View view = inflater.inflate(R.layout.fragment_index, container, false); FlutterEngine flutterEngine = new FlutterEngine(getActivity()); flutterEngine.getDartExecutor().executeDartEntrypoint( DartExecutor.DartEntrypoint.createDefault() ); flutterEngine.getNavigationChannel().setInitialRoute("route1"); FlutterView flutterView = new FlutterView(getActivity()); FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); FrameLayout flContainer = view.findViewById(R.id.fl_content); // 關鍵代碼,將Flutter頁面顯示到FlutterView flutterView.attachToFlutterEngine(flutterEngine); flContainer.addView(flutterView, lp); return view; }
fragment_index.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <!-- 嵌入flutter視圖 --> <FrameLayout android:id="@+id/fl_content" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
dart代碼實現:
main.dart
import 'package:edu/home_page.dart'; import 'package:flutter/material.dart'; import 'dart:ui'; import 'dart:convert'; void main() { runApp(_widgetForRoute(window.defaultRouteName)); } // 獲取路由名稱 String _getRouteName(String s) { if (s.indexOf('?') == -1) { return s; } else { return s.substring(0, s.indexOf('?')); } } // 獲取參數 Map<String, dynamic> _getParamsStr(String s) { if (s.indexOf('?') == -1) { return Map(); } else { return json.decode(s.substring(s.indexOf('?') + 1)); } } Widget _widgetForRoute(String url) { String route = _getRouteName(url); Map<String, dynamic> params = _getParamsStr(url); switch (route) { default: return MaterialApp( theme: ThemeData( primaryColor: Color(0xFF008577), primaryColorDark: Color(0xFF00574B), ), home: HomePage(route, params), ); } }
home_page.dart
import 'package:flutter/material.dart'; class HomePage extends StatefulWidget { String route; Map<String, dynamic> params; HomePage(this.route, this.params); @override State<StatefulWidget> createState() { return _HomePageState(); } } class _HomePageState extends State<HomePage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Flutter頁面'), automaticallyImplyLeading: false, ), body: Center( child: Text('首頁'), ), ); } }
ok,Demo代碼到這裏就寫完了,而後信心滿滿的run起來,發現直接崩了。這就是我要跟你說的其中一個坑,so架構的問題:
大部分老項目工程中用到的是armeabi架構,但flutter最低支持到armeabi-v7a,若是不作特殊處理,就會出現上面的Crash。怎麼辦?解決辦法天然有,就是找到flutter module工程的構建物,把armeabi-v7a
下的libFlutter.so
拿出來,放到原生工程的armeabi
下,我寫了個shell腳本,而後經過Hook Gradle Task的方式插入到編譯流程中去。
copyFlutterSo.sh
#!/bin/bash # 當前目錄 CURRENT_DIR="`pwd`" # 當前build目錄,具體以工程爲準 BUILD_DIR="`pwd`/build" # gradle 5.6.2 armeabi so路徑 #ARMEABI_DIR="$BUILD_DIR/intermediates/merged_native_libs/debug/out/lib/armeabi" # gradle 4.10.1 armeabi so路徑 ARMEABI_DIR="$BUILD_DIR/intermediates/transforms/mergeJniLibs/$1/0/lib/armeabi" # armeabi-v7a so存放路徑 ARMEABI_V7A_DIR="$BUILD_DIR/intermediates/transforms/mergeJniLibs/$1/0/lib/armeabi-v7a" echo -e "\033[47;30m ========== copy $1 libflutter.so ========== \033[0m" if [[ "$1" == "debug" ]]; then # 將libflutter.so copy到armeabi架構中去 cp -rf ${ARMEABI_V7A_DIR}/libflutter.so ${ARMEABI_DIR} echo "copy ${ARMEABI_V7A_DIR}/libflutter.so to ${ARMEABI_DIR}" elif [[ "$1" == "profile" ]]; then # 將libflutter.so copy到armeabi架構中去 cp -rf ${ARMEABI_V7A_DIR}/libflutter.so ${ARMEABI_DIR} # 將libapp.so也copy到armeabi架構中去 cp -rf ${ARMEABI_V7A_DIR}/libapp.so ${ARMEABI_DIR} echo "copy ${ARMEABI_V7A_DIR}/libflutter.so to ${ARMEABI_DIR}" echo "copy ${ARMEABI_V7A_DIR}/libapp.so to ${ARMEABI_DIR}" elif [[ "$1" == "release" ]]; then # 將libflutter.so copy到armeabi架構中去 cp -rf ${ARMEABI_V7A_DIR}/libflutter.so ${ARMEABI_DIR} # 將libapp.so也copy到armeabi架構中去 cp -rf ${ARMEABI_V7A_DIR}/libapp.so ${ARMEABI_DIR} echo "copy ${ARMEABI_V7A_DIR}/libflutter.so to ${ARMEABI_DIR}" echo "copy ${ARMEABI_V7A_DIR}/libapp.so to ${ARMEABI_DIR}" fi
Hook Gradle Task
afterEvaluate { project -> android.applicationVariants.each { variant -> /** * 因爲flutter不支持armeabi,此處在merge(Debug|Profile|Release)NativeLibs與strip(Debug|Profile|Release)DebugSymbols之間插入一個任務, * 將libflutter.so和libapp.so拷貝到merged_native_libs/(debug|profile/release)/out/lib/armeabi目錄下,使它們能打到最終的apk裏。 * * 詳情見copyFlutterSo.sh */ def taskPostfix = variant.name.substring(0, 1).toUpperCase() + variant.name.substring(1) project.task("copyFlutterSo$taskPostfix") { doLast { exec { // 執行shell腳本 commandLine "sh", "./copyFlutterSo.sh", variant.name } } } // 注意這個是在gradle 5.6.2版本的task // project.tasks["copyFlutterSo$taskPostfix"].dependsOn(project.tasks["merge$taskPostfix" + "NativeLibs"]) // project.tasks["strip$taskPostfix" + "DebugSymbols"].dependsOn(project.tasks["copyFlutterSo$taskPostfix"]) // // gradle 4.10.1,注意插入task的依賴順序 project.tasks["copyFlutterSo${taskPostfix}"].dependsOn(project.tasks["transformNativeLibsWithMergeJniLibsFor${taskPostfix}"]) project.tasks["process${taskPostfix}JavaRes"].dependsOn(project.tasks["copyFlutterSo$taskPostfix"]) } } }
這樣咱們每次執行assembleDebug
或者assembleRelease
都能自動將對應的armeabi-v7a
的libflutter.so
和libapp.so
複製到armeabi
下。
而後再run一次,這個時候就真正把咱們的混合工程跑起來了。
這裏我提咱們目前的作法:
// 引入flutter module setBinding(new Binding([gradle: this]))// new // module工程和setting.gradle文件同級 evaluate(new File( // new settingsDir, // new 'edu_flutter_module/.android/include_flutter.groovy' // new )) include ':edu_flutter_module' project(':edu_flutter_module').projectDir = new File('edu_flutter_module')
主要改動是將module工程和setting.gradle文件同級.
以module方式接入Flutter適合大部分存量的項目,目前咱們項目已經以這種方式跑起來而且打通持續構建,目前已經踩了部分坑,總得來講通過這段時間對Flutter這個框架的實踐咱們團隊已經掌握的新技術棧去爲業務賦能,接下來的工做就是不斷提高和優化新的研發體驗,讓統一技術棧這個目標不是說說而已。將來也將會輸出更多幹貨,幫助業內的朋友也能加入到終端的研發變革中來。