上半年我定的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这个框架的实践咱们团队已经掌握的新技术栈去为业务赋能,接下来的工做就是不断提高和优化新的研发体验,让统一技术栈这个目标不是说说而已。将来也将会输出更多干货,帮助业内的朋友也能加入到终端的研发变革中来。