最近公司产品想要实践下和
flutter
的混编
,也便是基于老的原生APP项目,引进flutter进行混编,这样新的功能就能够运用flutter进行开发,能够节约本钱。我负责了该项目,对不同的混编计划进行了了解,最后将自己采纳的计划在这儿介绍一下[注:此计划咱们已进行实际开发并发布],假如咱们的项目有混编需求,期望对咱们有必定的学习意义。
一、混编计划
1.1 三端一致计划
这种计划的项目结构为三端都在都一个文件夹项目中:
— iOS项目
— 安卓项目
— flutter项目
缺陷:
- 三端放在同一个目录,对现有的原生开发项目影响较大,
- 所有人都需求装上flutter环境且版别要一致,不利于团队开发,
长处:
- 但在自己开发时分这种能够及时进行
flutter attach
进行联调,这时分显得非常有必要,所以这种形式合适在开发阶段运用。
1.2 三端别离计划
运用三端别离的形式 三端别离,iOS和安卓原生项目保持不变,创立一个flutter项目用于编写flutter端的代码,然后运用脚本将flutter编译,iOS通过
pod
引证flutter编译后的framework,然后将生成物放在私有库中供原生调用,安卓端将flutter项目打成aar
进行引证。
缺陷:
- 在开发阶段不利于联调,修正或新写一些代码后,需求打包等一系列操作后才能看到效果,效率低。
长处:
- 这种形式适用于在老项目基础上进行混编引进flutter项目,对老项目侵入性小,
- 合适团队开发
1.3 选用的计划
综合两种计划的优缺陷,终究咱们决定选用两种计划结合的计划,即在开发阶段采纳三端一致的计划,这样开发中便利进行联调,在发布阶段选用三端别离的计划,利于保护和团队开发。
详细的切换也不费事,已iOS为例:
1、创立的flutter端和原生项目放在同一个文件夹;
2、切换不同的计划只需求在podfile中心中切换即可,开发阶段引证本地的flutter端,发布阶段引证私有库的flutter打包生成物。
详细代码可参阅2.2
中代码。
二、混编完结
这儿以iOS端为例详细介绍下混编的详细细节。
2.1 flutter端打包
Flutter项目打包我运用的是脚本,将flutter项目达到framework
,然后将这些framework放到公司的私有库
中,iOS端就能够通过pod进行引证。
2.1.1 打包脚本
通过图能够看到 build_ios_output.sh
即为打包的脚本,打出来的framework放在 build_for_ios
文件夹中。
打包脚本内容:
#条件flutter必定要是app项目: pubspec.yaml里 不要加
#module:
# androidPackage: com.example.myflutter
# iosBundleIdentifier: com.example.myFlutter
echo "Clean old build"
find . -d -name "build" | xargs rm -rf
flutter clean
echo "开始获取 packages 插件资源"
flutter packages get
echo "开始构建 build for ios 默认为release,debug需求到脚本改为debug"
#flutter build ios --debug
# release下放开下一行注释,注释掉上一行代码
flutter build ios --release --no-codesign
echo "构建 release 已完结"
echo "开始 处理framework和资源文件"
rm -rf build_for_ios
mkdir build_for_ios
cp -r build/ios/Release-iphoneos/*/*.framework build_for_ios
cp -r build/ios/Release-iphoneos/App.framework build_for_ios
#cp -r build/ios/Release-iphoneos/Flutter.framework build_for_ios
cp -r .ios/Flutter/engine/Flutter.xcframework build_for_ios
cp -r .ios/Flutter/FlutterPluginRegistrant/Classes/GeneratedPluginRegistrant.* build_for_ios
在打包是,需在终端进入到flutter项目中,然后运转
sh build_ios_output.sh
2.1.2 打包产品
由上图能够看出打出来的为framework,其间App.framework
为Dart
打包的,其它是运用的插件的framework,运转脚本后将 build_for_ios
文件夹中打包物上传到私有库中即可。
2.2 原生端引证flutter
以iOS为例,原生端调用flutter是在Podfile
文件中进行调用。
调用如下所示,能够在开发阶段和发布阶段切换不同的计划:
# 联调时分运用该形式 (脚本途径为 .ios->Flutter->podhelper.rb)
# [注:]flutter_debug标志是否是debug 若为release需手动修正为false
flutter_application_path = '../xxxFlutter/xxx_flutter'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
$flutter_debug = false
target xxx do
# Flutter端混编(debug联调引证本地,release引证pod私有库中framework)
if $flutter_debug
install_all_flutter_pods(flutter_application_path)
else
# 这儿在自己联调时能够直接引证打出来的包,测试时命名为 版别号-dev,上线命名规则为 版别号-release
# pod 'XXXFlutterSDK', :path => '../XXXFlutterSDK'
pod 'XXXFlutterSDK', '0.0.1-dev'
end
end
到这儿整体的混编结构现已很明晰了,安卓端也是类似的,写个脚本将flutter端生成物放到私有库,然后通过aar调用即可。
三、两头调用
3.1 混合栈
两头混编首先要解决的问题便是混合栈问题,两头调用如原生->flutter->flutter->原生等,这中心涉及到原生的导航栈跳转到flutter的导航栈的处理,以及两个导航栈之间页面的跳转和入栈出栈等操作。这儿面还有一个问题便是
FlutterEngine
的问题,假如你只是简单的调用flutterviewcontroller
进行页面的调用,这样屡次调用会创立多个引擎,而FlutterEngine
是很耗费内存的,所以混合栈的问题必须要考虑。
######混合栈干流的有:
1.Google官方FlutterEngineGroup
(多引擎计划)
即每次运用一个新的FlutterEngine来渲染Widget树。虽然Flutter 2.0之后的创立FlutterEngine的开销大大下降,但是依然没有解决每个FlutterEngine是一个独自isolate,假如需求Flutter①和Flutter②之间交互数据的话,将会非常费事,咱们同样无法保证他们之间不会进行数据交互
2.大名鼎鼎的闲鱼flutter_boost
(单引擎计划)
######长处:
- 应用的项目多,通过了验证,可完结较好的效果
- 最近发布了3.0的bate版别,摒弃了2.0版别对引擎的侵入。 ######缺陷:
- 对项目侵入性较大
3.哈喽单车团队的flutter_thrio
(单引擎计划)
该库的优劣作者现已说得很详细了,这儿就不再赘述,感兴趣的朋友能够进传送门亲自查看,flutter_thrio的优缺陷。
4.字节跳动团队的Isolate复用计划和腾讯心悦团队的TRouter计划 很可惜,目前这两个计划并没有开源出来,但很可能字节团队的计划的侵入性相当高。
通过一系列对比后,终究挑选了较为成熟和稳定的flutter_boost
。
3.2 Flutter端完结
在pubspec.yaml中引进 flutter_boost
# flutter_boost
flutter_boost:
git:
url: 'https://github.com/alibaba/flutter_boost.git'
ref: '3.1.0'
3.2.1 路由
混编主要是原生和flutter端的彼此调用,路由的代码如下:
在main.dart
中:
Route<dynamic>? routeFactory(RouteSettings settings, String? uniqueId) {
// settings.name 首次为 /, 实际是代表主页的意思
// BoostRoute.routerMap为Boost的路由表
FlutterBoostRouteFactory? func = BoostRoute.routerMap[settings.name!];
if (func == null) {
return null;
}
return func(settings, uniqueId);
}
/// 然后build
@override
Widget build(BuildContext context) {
return FlutterBoostApp(
routeFactory,
appBuilder: appBuilder,
// initialRoute: RoutePath.storeSignExpress,
);
}
其间BoostRoute
是项目的路由表,这儿给独立为一个类,便于保护,详细代码如下:
import 'package:flutter/material.dart';
import 'package:flutter_boost/flutter_boost.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:get/get_navigation/src/extension_navigation.dart';
import 'package:self_driving_flutter/app/config/route/route_path.dart';
import 'package:self_driving_flutter/module/ehi_base_page/view.dart';
import 'package:self_driving_flutter/module/inspect_car_record/view.dart';
import 'package:self_driving_flutter/utils/tools_util.dart';
import '../../../module/feedback_content/view.dart';
/// Boost路由表
class BoostRoute {
/// 注册的路由表
static Map<String, FlutterBoostRouteFactory> routerMap = {
'/': (settings, uniqueId) {
// 联调时可设置为自己开发的页面(可直接运转AS开发)
return _buildPage(settings, YTBasePage());
}
RoutePath.feedbackContent: (settings, uniqueId) {
return _buildPage(settings, FeedbackContentPage());
},
RoutePath.inspectCarRecord: (settings, uniqueId) {
Map<dynamic, dynamic> arguments = settings.arguments as Map<dynamic, dynamic>;
return _buildPage(settings, InspectCarRecordPage(
orderId: arguments['orderId'],
userId: arguments['userId'])
);
},
};
static MaterialPageRoute _buildPage(settings, Widget page) {
return MaterialPageRoute(
settings: settings,
builder: (_) {
return page;
});
}
}
3.3 原生端完结
3.3.1 导航跳转类
这个类主要是操控原生和flutter页面的push
和pop
。
详细完结如下:
class YTFlutterBoostDelegate: NSObject, FlutterBoostDelegate {
///您用来push的导航栏
@objc var navigationController:UINavigationController?{
didSet{
navigationController?.delegate = self
}
}
///用来存回来flutter侧回来结果的表
var resultTable:Dictionary<String,([AnyHashable:Any]?)->Void> = [:];
// MARK: 假如结构发现您输入的路由表在flutter里边注册的路由表中找不到,那么就会调用此办法来push一个纯原生页面
func pushNativeRoute(_ pageName: String!, arguments: [AnyHashable : Any]!) {
//能够用参数来操控是push仍是pop
let isPresent = arguments["isPresent"] as? Bool ?? false
let isAnimated = arguments["isAnimated"] as? Bool ?? true
//这儿依据pageName来判断生成哪个vc
let targetViewController = dealViewController(with: pageName, arguments: arguments)
// 展示导航,到原生页面运用原生的导航
self.navigationController?.setNavigationBarHidden(false, animated: false)
if(isPresent) {
self.navigationController?.present(targetViewController, animated: isAnimated, completion: nil)
}else{
self.navigationController?.pushViewController(targetViewController, animated: isAnimated)
}
}
// MARK: 当结构的withContainer为true的时分,会调用此办法来做原生的push
func pushFlutterRoute(_ options: FlutterBoostRouteOptions!) {
let vc:FBFlutterViewContainer = FBFlutterViewContainer()
vc.setName(options.pageName, uniqueId: options.uniqueId, params: options.arguments,opaque: options.opaque)
//用参数来操控是push仍是pop
let isPresent = (options.arguments?["isPresent"] as? Bool) ?? false
let isAnimated = (options.arguments?["isAnimated"] as? Bool) ?? true
//对这个页面设置结果
resultTable[options.pageName] = options.onPageFinished
// 躲藏导航,到Flutter页面运用Flutter的导航并禁止右滑手势
self.navigationController?.setNavigationBarHidden(true, animated: false)
//假如是present形式 ,或者要不通明形式,那么就需求以present形式翻开页面
if(isPresent || !options.opaque){
self.navigationController?.present(vc, animated: isAnimated, completion: nil)
}else{
self.navigationController?.pushViewController(vc, animated: isAnimated)
}
}
// MARK: 当pop调用涉及到原生容器的时分,此办法将会被调用
func popRoute(_ options: FlutterBoostRouteOptions!) {
//假如当前被present的vc是container,那么就履行dismiss逻辑
if let vc = self.navigationController?.presentedViewController as? FBFlutterViewContainer,vc.uniqueIDString() == options.uniqueId{
//这儿分为两种状况,因为UIModalPresentationOverFullScreen下,生命周期显现会有问题
//所以需求手动调用的场景,从而使下面底部的vc调用viewAppear相关逻辑
if vc.modalPresentationStyle == .overFullScreen {
//这儿手动beginAppearanceTransition触发页面生命周期
self.navigationController?.topViewController?.beginAppearanceTransition(true, animated: false)
vc.dismiss(animated: true) {
self.navigationController?.topViewController?.endAppearanceTransition()
}
}else{
//正常场景,直接dismiss
vc.dismiss(animated: true, completion: nil)
}
}else{
self.navigationController?.popViewController(animated: true)
}
// 展示导航,到原生页面运用原生的导航
self.navigationController?.setNavigationBarHidden(false, animated: false)
//否则直接履行pop逻辑
//这儿在pop的时分将参数带出,而且从结果表中移除
if let onPageFinshed = resultTable[options.pageName] {
onPageFinshed(options.arguments)
resultTable.removeValue(forKey: options.pageName)
}
}
}
private extension YTFlutterBoostDelegate {
/// 依据pageName来判断生成哪个vc
func dealViewController(with name: String, arguments: [AnyHashable : Any]) -> UIViewController {
switch name {
case storeDetailPage: // 门店概况
let vc = YTNewStoreDetailViewController()
if let storeID = arguments["storeID"] as? Int {
vc.storeID = storeID
}
YTNavigator.push(vc)
return vc
default:
return UIViewController()
}
}
}
extension YTFlutterBoostDelegate : UINavigationControllerDelegate{
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
// 右滑回来
viewController.transitionCoordinator?.notifyWhenInteractionChanges({ (context) in
if context.isCancelled {
return;
}
self.navigationController?.setNavigationBarHidden(false, animated: false)
})
}
}
3.3.2 装备FlutterBoost
然后需求在AppDelegate中装备FlutterBoost
,在装备中也能够添加两头的交互,用户两头事情的交互,如传值等。
声明特点:
@property (nonatomic, strong) YTFlutterBoostDelegate *boostDelegate;
详细代码如下:
#pragma mark - 装备FlutterBoost及交互
- (void)configFlutterBoost:(UIApplication *)application {
self.boostDelegate = [[YTFlutterBoostDelegate alloc] init];
__block FlutterEngine *callEngine;
// 注册FlutterBoost
[[FlutterBoost instance] setup:application delegate: self.boostDelegate callback:^(FlutterEngine *engine) {
callEngine = engine;
}];
// 处理Flutter调用原生事情
self.methodChannel = [FlutterMethodChannel methodChannelWithName:@"xxx" binaryMessenger:callEngine];
[self.methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
[YTMethodChannelManager methodChannelWith:call result:result];
}];
}
3.3.3 原生调用flutter
我这儿将调用办法独自成一个类,便于保护和扩展,用于在原生代码中翻开Flutter
页面,详细代码如下:
class YTFlutterUtils: NSObject {
// MARK: 翻开Flutter页面
// pageRoute: 路由称号
// arguments: 参数
// opaque: 这个页面是否通明(默认为true)
// completion: open办法完结后的回调,仅在原生->flutter页面的时分有用
// onPageFinished: 参数回传的回调闭包,仅在原生->flutter页面的时分有用
@objc class func openFlutterPage(with pageName: String = "",
arguments: Dictionary<String, Any>? = [:],
opaque: Bool = true,
completion: ((Bool) -> ())? = nil,
onPageFinished: (((Dictionary<AnyHashable, Any>)?) -> ())? = nil
) {
let options = FlutterBoostRouteOptions()
options.pageName = pageName
options.arguments = arguments ?? ["animated": true];
options.opaque = opaque
options.completion = completion
options.onPageFinished = onPageFinished
FlutterBoost.instance().open(options)
}
}
到这儿现已完整的完结了原生段和flutter的混编,包括混编计划的挑选及详细完结,期望能够对咱们起到一些学习效果。