最近公司产品想要实践下和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进行引证。

Flutter和原生混编-两种方案结合使混编更轻松

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 打包产品

Flutter和原生混编-两种方案结合使混编更轻松

由上图能够看出打出来的为framework,其间App.frameworkDart打包的,其它是运用的插件的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页面的pushpop。 详细完结如下:

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的混编,包括混编计划的挑选及详细完结,期望能够对咱们起到一些学习效果。