原生工程接入Flutter实现混编
前言
上半年我定的OKR目标是帮助团队将App切入Flutter,实现统一技术栈,变革成多端融合开发模式。Flutter目前是跨平台方案中最有潜力实现我们这个目标的,不管是Hybird还是React Native,我们的项目都有落地应用,跨平台一直是终端团队所追求的技术,能够快速研发和部署也是我们不断给自己提出的挑战。Flutter是什么我在这里就不多说了,很多文章都有介绍,本篇文章想分享的是如何在原生工程中嵌入Flutter来实现混编,帮助团队快速落地Flutter迁移,这个对小团队来说应该会有一定借鉴意义。
前置动作
在接入Flutter之前需要具备以下前置条件:
- 易于开发的操作系统(首推macOS)
- 配置Flutter开发环境(参考:https://flutter.dev/docs/get-started/install/macos )
- Android和iOS开发环境(自行搜索解决)
接入方案
业内绝大部分的App都不可能推倒重来,所以混合工程的方式接入Flutter是目前主流开发模式,下面我简单说说业界两种工程管理模式:
统一管理模式(不推荐)
- 优点
- 适合全新使用Flutter开发的项目
- 缺点
- 后期代码耦合严重,相关工具链耗时大幅增长,导致开发效率低
三端分离模式(推荐)
咸鱼方案:https://mp.weixin.qq.com/s/Q1z6Mal2pZbequxk5I5UYA?
官方方案:https://flutter.dev/docs/development/add-to-app
- 优点
- 快速实现Flutter功能“热插拔”,降低原生工程的改造成本
- 可以直接进行Dart代码和原生代码开发调试
目前我们采用的是以module的形式接入,因为我们团队人员少,沟通协作起来成本不大,初期直接源码接入也方便我们快速接入开发和调试。
踩坑实践
flutter doctor
如果想确认你当前的环境是否ok,执行下flutter doctor
命令,基本能解决大部分问题。如果遇到一直卡住,说明你当前环境是不通的,检查下代理是否配置正确。
创建Flutter module工程
如果点击Finish创建module一直卡死,说明还是网络问题,命令行输入vi ~/.bash_profile
检查下代理。如果实在不行,则通过命令行创建module:
flutter create -t module --org com.example my_flutter
Android原生工程集成Flutter
一期我们先接入Android工程,所以接下来主要以Android为主,后续如果有iOS相关的实践会补充到这里。
先看下我们的module工程:
目录结构:
- .android(隐藏目录,自动生成的Android工程)
- .ios(隐藏目录,自动生成的iOS工程)
- build(Android和iOS的构建产物)
- lib(Flutter应用源文件)
- test(测试文件)
- *.iml(工程配置文件)
- pubspec.lock(记录当前项目实际依赖信息的文件)
- pubspec.yaml(管理第三方库资源信息的配置文件)
除了工程配置文件和自动生成的工程目录之外,其他文件都需要进行托管。
了解完工程目录之后,我们开始集成:
- 打开原生工程
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
,有两种解决办法:
- 重命名module名字,命名为app
- 修改flutter脚本(我选的是这种)
这样,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页面
上一个主题我们解决掉一些坑之后终于把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工程单独以git仓库托管
- 以submodule的方式将flutter module工程管理
- 调整依赖路径如下:
// 引入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文件同级.
- 通过持续构建系统搭建Flutter构建环境,满足日常开发构建
总结
以module方式接入Flutter适合大部分存量的项目,目前我们项目已经以这种方式跑起来并且打通持续构建,目前已经踩了部分坑,总得来说经过这段时间对Flutter这个框架的实践我们团队已经掌握的新技术栈去为业务赋能,接下来的工作就是不断提升和优化新的研发体验,让统一技术栈这个目标不是说说而已。未来也将会输出更多干货,帮助业内的朋友也能加入到终端的研发变革中来。
原生工程接入Flutter实现混编的更多相关文章
- iOS原生 和 react native视图混编
在iOS原生功能中加入RN,请看之前 写的 RN与iOS交互系列文章.本篇只讲下视图混编. 关键点只有二: 1.通过 RCTRootView 加载RN视图. 2.RN中,只需要AppRegistry. ...
- Flutter学习笔记(30)--Android原生与Flutter混编
如需转载,请注明出处:Flutter学习笔记(30)--Android原生与Flutter混编 这篇文章旨在学习如何在现有的Android原生项目上集成Flutter,实现Android与Flutte ...
- Flutter和iOS混编详解
前言 下面的内容是最近在使用Flutter和我们自己项目进行混编时候的一些总结以及自己踩的一些坑,处理完了就顺便把整个过程以及一些我们可能需要注意的点全都梳理出来,希望对有需要的小伙伴有点帮助,也方便 ...
- iOS开发--Swift 如何完成工程中Swift和OC的混编桥接(Cocoapods同样适用)
由于SDK现在大部分都是OC版本, 所以假如你是一名主要以Swift语言进行开发的开发者, 就要面临如何让OC和Swift兼容在一个工程中, 如果你没有进行过这样的操作, 会感觉异常的茫然, 不用担心 ...
- 在OC项目工程中混编Swift
1.创建一个OC项目工程,然后在Build Settings中找到如下字段,修改. 2.然后在项目中创建swift文件,如果系统提示是否需要创建桥接文件的时候,点击确定. 然后在Build Setti ...
- unity导出工程导入到iOS原生工程中详细步骤
一直想抽空整理一下unity原生工程导入iOS原生工程中的详细步骤.做iOS+vuforia+unity开发这么长时间了.从最初的小小白到现在的小白.中间趟过了好多的坑.也有一些的小小收货.做一个喜欢 ...
- 【转载】混编ObjectiveC++
原文:混编ObjectiveC++ 最近有点炒冷饭的嫌疑,不过确实以前没有Git Or Blog的习惯,所以很多工作上的技术分享就存留在了电脑的文档里,现在还是想重新整理一下,再分享出来. 混编C++ ...
- Swift和Objective-C混编注意事项
前言 Swift已推出数年,与Objective-C相比Swift的语言机制及使用简易程度上更接地气,大大降低了iOS入门门槛.当然这对新入行的童鞋没来讲,的确算是福音,但对于整个iOS编程从业者来讲 ...
- iOS 里面 Swift与Objective-C混编,Swift与C++混编的一些比较
即使你尽量用Swift编写iOS程序,难免会遇到部分算法是用C++语言编写的.那你只能去问问”度娘“或“狗哥”怎么用Swift调用C++算法. 一,C,C++, Objective-C,S ...
随机推荐
- C++ 虚函数表与多态 —— 关键字 final 的用法
final 字面上最终.最后.不可改变的意思,final 这个关键字在 Jave PHP C++中都有用到,其作用也基本一致. C++中的 final 是C++11新增,他可以用来修饰类,让类无法被继 ...
- 六个步骤,从零开始教你搭建基于WordPress的个人博客
摘要:WordPress是使用PHP语言开发的博客平台,是免费开源的.用户可以在支持PHP和MySQL数据库的服务器上架设属于自己的网站,也可以把WordPress当作一个内容管理系统(CMS)来使用 ...
- antdv的Upload组件实现前端压缩图片并自定义上传功能
Ant Design of Vue的Upload组件有几个重要的api属性: beforeUpload: 上传文件之前的钩子函数,支持返回一个Promise对象. customRequest: 覆盖组 ...
- Windows安装Pytorch并配置Anaconda与Pycharm
1 开发环境准备 Python 3.7+Anaconda3 5.3.1(64位)+CUDA+Pycharm Community 2 安装Anaconda 2.1 进入官网下载: 根据windows版本 ...
- Helm 带你飞
文章目录 目录 文章目录 在没使用 Helm之前,向 K8S部署应用,我们要依次部署 deployment. svc 等,步骤较繁琐.况且随着很多项目微服务化,复杂的应用在容器中部署以及管理显得较为复 ...
- 让你轻松掌握 Python 中的 Hook 钩子函数
1. 什么是Hook 经常会听到钩子函数(hook function)这个概念,最近在看目标检测开源框架mmdetection,里面也出现大量Hook的编程方式,那到底什么是hook?hook的作用是 ...
- SpringBoot整合任务调度框架Quartz及持久化配置
目录 本篇要点 SpringBoot与Quartz单机版快速整合 引入依赖 创建Job 调度器Scheduler绑定 自动配置,这里演示SimpleScheduleBuilder 手动配置,这里演示C ...
- Vue开发中的一些常见套路和技巧
属性排放 export default { name: '名称', components: { // 组件挂载a}, created(){} // 数据获取 beforeMount() {}, // ...
- intellij idea svn不能更新和提交
进入设置–version control – subversion如下图,将前边的选项的勾全部去掉,点击ok
- 解决在Filter中读取Request中的流后, 然后再Control中读取不到的做法
摘要: 大家知道, StringMVC中@RequestBody是读取的流的方式, 如果在之前有读取过流后, 发现就没有了. 我们来看一下核心代码: filter中主要做的事情, 就是来校验请求是否合 ...