美文网首页
Flutter && Flutter_Boost 之 iOS 混

Flutter && Flutter_Boost 之 iOS 混

作者: 阿潇Hardy | 来源:发表于2019-07-31 16:56 被阅读0次

    Mac系统Flutter环境集成

    使用镜像

    由于在国内访问Flutter有时可能会受到限制,Flutter官方为中国开发者搭建了临时镜像,可以将如下环境变量加入到用户环境变量中:

    export PUB_HOSTED_URL=https://pub.flutter-io.cn
    export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
    

    获取Flutter SDK

    官网下载安装包:https://flutter.io/sdk-archive/#macos
    解压后拷贝安装到想安装的目录(安装到哪里都可以,但是后面需要将这个安装路径添加到环境变量中)
    我的安装目录(装在这里不是很好,但是添加了环境变量,就先不动了):

    /Users/HUANGXIAO/flutter
    

    添加flutter相关工具到环境变量中:

    cd /Users/HUANGXIAO/flutter
    export PATH=`pwd`/flutter/bin:$PATH
    

    现在只是设置了临时环境变量,长期使用需要将其设置为永久的环境变量。
    在家目录打开 .bash_profile 文件:

    cd ~
    open -e .bash_profile
    

    配置环境变量:
    添加flutter安装目录到path中,使用命令添加或者直接编辑.bash_profile

    export PATH='你的安装目录'/bin:$PATH
    

    我的环境变量如下

    export JAVA_HOME=$(/usr/libexec/java_home)
    
    export ANDROID_HOME="/Users/HUANGXIAO/Library/Android/sdk"
    export PATH=${PATH}:${ANDROID_HOME}/tools
    export PATH=${PATH}:${ANDROID_HOME}/platform-tools
    
    export PUB_HOSTED_URL=https://pub.flutter-io.cn 
    export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn 
    export PATH=/Users/HUANGXIAO/flutter/bin:$PATH
    
    [[ -s "$HOME/.rvm/scripts/rvm" ]] && source "$HOME/.rvm/scripts/rvm" # Load RVM into a shell session *as a function*
    

    更新环境变量:

    source .bash_profile
    

    验证目录是否在已经在PATH中:

    echo $PATH
    
    1563505094101.jpg

    到此Flutter 已安装好,并导入到环境变量中。

    安装Flutter依赖

    终端输入:

    flutter doctor
    

    根据提示安装相应的依赖软件,比如 Android Studio、XCode、VSCode等,并安装相应的Flutter插件。
    可参照Flutter中文网教程安装:https://flutterchina.club/setup-macos/

    这一步可能会遇到一些问题,这篇文章总结得比较全面,可以参考,在此感谢!
    https://www.jianshu.com/p/603649a02956

    最终安装完后会全部是√,说明环境和依赖已经OK了,接下来就可以进行开发了。


    image2019-4-10_16-44-52.png

    iOS 项目集成Flutter编译环境

    Flutter 与 原生项目 混编有两种方案:

    1. 自动创建Android和iOS项目

    如果项目之初就已经决定使用Flutter与Native混编方案,那么可以直接用Flutter生成项目,其中会自动生成iOS和Android相对应的原生项目。这也是比较简单而且高效的混编方案。
    Andriod Studio 创建:
    File → New → New Flutter Project
    注意选择混编是Android和iOS的开发语言:


    image.png

    命令创建:
    同样注意选择混编是Android和iOS的开发语言:

    flutter create -i swift -a kotlin Name
    

    创建好后的目录结构:
    android为Android项目文件,ios为iOS项目文件。


    image.png

    这样就创建好混编项目,以iOS为例,用Xcode打开Runner.xcworkspace项目,可以看到:


    image.png
    Flutter已自动将Flutter与iOS代码集成好了。可以根据需要修改项目配置,如Display Name, Bundle Id 等。
    iOS项目自动采用cocopod集成第三方库。

    2. iOS老项目集成Flutter

    官方指导:https://github.com/flutter/flutter/wiki/Add-Flutter-to-existing-apps

    如果已经有了iOS项目,需要Flutter与iOS混编。
    创建Flutter module,请确保安卓、iOS、Flutter三个项目的根目录必须在同一目录下:

    flutter create -t module flutter_module
    

    如果iOS使用swift语言,请加上 -i swift
    安卓、iOS、Flutter三个项目的根目录必须在同一目录下:


    image.png
    • iOS项目使用cocoapods管理第三方依赖包,并在Podfile加入如图以下代码,注意Flutter项目路径:
    flutter_application_path = 'path/to/my_flutter/'
    eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)
    
    image.png

    打开Flutter项目 , 获取包:

    flutter packages get
    

    然后,将Flutter跑一遍,已确保Flutter生成iOS项目集成需要的依赖产物:

    flutter build ios
    

    Flutter项目运行成功后,iOS项目执行:

    pod install
    

    成功后可以在pods - Development Pods下看到Flutter 安装的包:


    image.png
    • iOS项目Enable Bitcode改为NO


      image.png
    • iOS项目添加脚本,注意脚本位置要放在check pods manifest.lock之后:
    "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
    "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
    
    image.png

    如果一切顺利,编译iOS项目⌘B编译应该会成功。此时iOS项目Flutter编译环境已集成完成。

    iOS Flutter混编

    AppDelegate.swift修改

    import UIKit
    import Flutter
    import FlutterPluginRegistrant // Only if you have Flutter Plugins.
    
    @UIApplicationMain
    class AppDelegate: FlutterAppDelegate {
      var flutterEngine : FlutterEngine?;
      // Only if you have Flutter plugins.
      override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        self.flutterEngine = FlutterEngine(name: "io.flutter", project: nil);
        self.flutterEngine?.run(withEntrypoint: nil);
        GeneratedPluginRegistrant.register(with: self.flutterEngine);
        return super.application(application, didFinishLaunchingWithOptions: launchOptions);
      }
    
    }
    
    

    Native跳转Flutter:

    官方方法:

    import UIKit
    import Flutter
    
    class ViewController: UIViewController {
      override func viewDidLoad() {
        super.viewDidLoad()
        let button = UIButton(type:UIButtonType.custom)
        button.addTarget(self, action: #selector(handleButtonAction), for: .touchUpInside)
        button.setTitle("Press me", for: UIControlState.normal)
        button.frame = CGRect(x: 80.0, y: 210.0, width: 160.0, height: 40.0)
        button.backgroundColor = UIColor.blue
        self.view.addSubview(button)
      }
    
      @objc func handleButtonAction() {
        let flutterEngine = (UIApplication.shared.delegate as? AppDelegate)?.flutterEngine;
        let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)!;
        self.present(flutterViewController, animated: false, completion: nil)
      }
    }
    

    指定Flutter路由:

    flutterViewController.setInitialRoute("route1")
    

    在我实际集成的时候,使用flutterEngine是没办法指定跳转的路由的,也就是flutterViewController.setInitialRoute("route1")无效!无效!无效!
    如果不使用flutterEngine,直接初始化FlutterViewController并指定路由跳转是能够成功跳转相应路由的!但是,不使用flutterEngine会导致FlutterViewController()关闭是内存无法释放!内存无法释放!内存无法释放!

    let flutterViewController = FlutterViewController()
    flutterViewController.setInitialRoute("route1")
    self.present(flutterViewController, animated: false, completion: nil)
    

    这是Flutter官方最大的bug,也是最大的坑!
    只有等待谷歌官方后续解决吧。

    寻求解决办法:

    1.使用消息传递机制跳转页面
    iOS代码:

    // 带默认返回操作Fluttervc
        class func createFluttervcWithDefaultHandler(paramStr:String) -> (BaseFlutterViewController) {
            let engine = HX_AppDelegate.flutterEngine
            let messageChannel = HX_AppDelegate.messageChannel
            
            let flutterVC = BaseFlutterViewController(engine: engine, nibName: nil, bundle: nil)!
            let channelName = "com.novasoftware.ShoppingMall.address"
            let channel = FlutterMethodChannel(name: channelName, binaryMessenger: flutterVC)
            channel.setMethodCallHandler { (call: FlutterMethodCall, result: FlutterResult) in
                print(call.method)
                if call.method == "back" {
                    flutterVC.dismiss(animated: true, completion: nil)
                }
            }
            print("Native:" + paramStr)
            engine!.navigationChannel.invokeMethod("", arguments: paramStr)
            messageChannel!.sendMessage(paramStr)  // 发送消息
    
            return flutterVC
        }
        
        // 自定义handler的Fluttervc
        class func createFluttervcWithHandler(paramStr:String, handler:@escaping FlutterMethodCallHandler) -> (BaseFlutterViewController) {
            let engine = HX_AppDelegate.flutterEngine
            let messageChannel = HX_AppDelegate.messageChannel
            
            let flutterVC = BaseFlutterViewController(engine: engine, nibName: nil, bundle: nil)!
            let channelName = "com.novasoftware.ShoppingMall.address"
            let channel = FlutterMethodChannel(name: channelName, binaryMessenger: flutterVC)
            channel.setMethodCallHandler(handler)
            print("Native:" + paramStr)
            engine!.navigationChannel.invokeMethod("", arguments: paramStr)
            messageChannel!.sendMessage(paramStr)    // 发送消息
            return flutterVC
        }
    

    Native页面跳转Flutter指定页面,并带参数传递:

            var flutterVC = BaseFlutterViewController()
            let handler : FlutterMethodCallHandler = { (call: FlutterMethodCall, result: FlutterResult) in
                print(call.method)
                if call.method == "back" {
                    // 消息交互
                    flutterVC.dismiss(animated: true, completion: nil)
                }
            }
    
            let paramStr = "MyCoupon?" + (SM_token ?? "")
            flutterVC = HXHelper.createFluttervcWithHandler(paramStr: paramStr, handler: handler)
            self.viewContainingController()?.present(flutterVC, animated: true, completion: nil)
    

    Flutter代码:

    const String _kReloadChannelName = 'reload';
    const BasicMessageChannel<String> _kReloadChannel =
        BasicMessageChannel<String>(_kReloadChannelName, StringCodec());
    
    void main() {
      _kReloadChannel.setMessageHandler(run);
      print("Flutter---" + ui.window.defaultRouteName);
      run(ui.window.defaultRouteName);
    }
    
    Future<String> run(String route) async {
      print("Flutter---" + route);
      switch (_getPageName(route)) {
        case 'address':
          String param = _getPageParamJsonStr(route);
          AddressParamEntity entity =
              AddressParamEntity.fromJson(json.decode(param));
          runApp(AddressPage1(
            entity: entity,
          ));
          break;
        case 'ReceiveCoupon':
          runApp(CouponPage(token: _getPageParamJsonStr(route)));
          break;
        case 'MyCoupon':
          runApp(MyCouponPage(token: _getPageParamJsonStr(route)));
          break;
        default:
    
          break;
      }
      return '';
    }
    
    String _getPageName(String s) {
      if (s.indexOf("?") == -1) {
        return s;
      } else {
        return s.substring(0, s.indexOf("?"));
      }
    }
    
    String _getPageParamJsonStr(String s) {
      if (s.indexOf("?") == -1) {
        return "";
      } else {
        return s.substring(s.indexOf("?") + 1);
      }
    }
    
    int getAddressId(String s) {
      return int.parse(s.substring(0, s.indexOf(",")));
    }
    
    String getAddressToken(String s) {
      return s.substring(s.indexOf(",") + 1);
    }
    
    

    这样能使Native跳转到Flutter相应的页面,并传递参数。
    并且可以使用消息传递机制做一些交互:

    static const  platform = const MethodChannel('com.novasoftware.ShoppingMall.address');
      Future<Null> back() async {
        try {
          await platform.invokeMethod('back');
        } on PlatformException catch (e) {
        }
      }
    
    var flutterVC = BaseFlutterViewController()
    let handler : FlutterMethodCallHandler = { (call: FlutterMethodCall, result: FlutterResult) in
        print(call.method)
        if call.method == "back" {
            flutterVC.dismiss(animated: true, completion: nil)
        }
    }
    
    let paramStr = "ReceiveCoupon?" + (SM_token ?? "")
    flutterVC = HXHelper.createFluttervcWithHandler(paramStr: paramStr, handler: handler)
    self.present(flutterVC, animated: true, completion: nil)
    

    这样测试了一段时间,本以为就此搞定。没想到后面页面出现乱码,视图出现马赛克,等等一些问题。
    可以确定,是Flutter内存泄漏导致的,这样做还是有个问题,同一个页面关闭后内存并没有释放,只是第二次打开这个页面的时候,不会重新申请内存新建页面,也就是同一个Flutter页面不会多次创建,但是也不会释放。比如,第二次打开这个页面,上次在输入框输入的文本并没有清空,还显示在那里,而且,连光标都没有释放!

    这种办法行不通,另外寻求解决办法。
    后面发现阿里巴巴的闲鱼团队在使用Flutter框架,并且有了很多成功的案例。
    最重要的,他们还开源了一套Flutter Native混合开发的框架Flutter Boost!

    集成Flutter Boost

    在对应的pubspec.yaml文件中加入依赖

    flutter_boost: ^0.0.415
    
    image.png

    之后调用:

    flutter packages get
    

    执行:

    flutter build ios
    

    在iOS的根目录下执行:

    pod install
    

    使iOS和flutter都添加FlutterBoost插件。


    image.png
    • Dart 代码集成
    void main() {
      runApp(MyApp());
    }
    class MyApp extends StatefulWidget {
      @override
      _MyAppState createState() => _MyAppState();
    }
    class _MyAppState extends State<MyApp> {
      @override
      void initState() {
        super.initState();
        FlutterBoost.singleton.registerPageBuilders({
          'Address': (pageName, params, _) {
            print(params);
            AddressParamEntity entity =
            AddressParamEntity.fromJson(json.decode(params["object"]));
            return AddressPage1(
                entity: entity
            );
          }, // 页面1
    
          'ReceiveCoupon': (pageName, params, _) {
            print(params);
            return CouponPage(
                token: params["token"]
            );
          }, // 页面2
    
          'MyCoupon': (pageName, params, _) {
            print(params);
            return MyCouponPage(
                token: params["token"]
            );
          }// 页面3
        );
        FlutterBoost.handleOnStartPage();
      }
    
      Map<String, WidgetBuilder> routes = {
        "second": (BuildContext context) =>
            MyCoupon(token: "")
      };
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
            title: 'Flutter Boost example',
            builder: FlutterBoost.init(postPush: _onRoutePushed),
            routes: routes,
            home: Container());
      }
    
      void _onRoutePushed(
          String pageName, String uniqueId, Map params, Route route, Future _) {
      }
    
    }
    
    • iOS代码集成
      需要 将libc++ 加入 "Linked Frameworks and Libraries"
      这个主要是项目的General 的Linked Frameworks and Libraries 栏下,点击加号(+)搜索libc++,找到libc++.tbd即可。


      image.png

    修改AppDelegate.swift

    import UIKit
    import CoreData
    import Flutter
    import FlutterPluginRegistrant
    import flutter_boost
    
    
    @UIApplicationMain
    class AppDelegate: FLBFlutterAppDelegate {
    
        var flutterEngine : FlutterEngine?
        var messageChannel : FlutterBasicMessageChannel?
        
        override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
            // Override point for customization after application launch.
    
            self.setupIQKeyBoardManager()
            
            if (HX_Defaults_Standard.string(forKey: DEFAULT_TOKEN) == nil) {
                let login = R.storyboard.login().instantiateInitialViewController()
                self.window?.rootViewController = login
            } else {
                let tabbar = BaseTabBarController()
                self.window?.rootViewController = tabbar
            }
            
            // ------------------------
            let router = HXFlutterRouter.sharedRouter
            router.navigationController = self.window?.rootViewController?.navigationController
            // 初始化FlutterBoost,也可以在其他地方做初始化
            FlutterBoostPlugin.sharedInstance()?.startFlutter(with: router, onStart: { (flutterVC) in
                // 或许这里需要些什么代码,让Flutter能跳转Native页面,还需要研究
            })
            // ------------------------
            
            self.window?.backgroundColor = UIColor.white
            self.window?.makeKeyAndVisible()
    
            return super.application(application, didFinishLaunchingWithOptions: launchOptions);
            
        }
        ....
    }
    

    实现FLBPlatform协议

    import UIKit
    
    class HXFlutterRouter : NSObject, FLBPlatform{
        
        var navigationController: UINavigationController?
        static let sharedRouter = HXFlutterRouter()
        let accessibilityEnable = true
        
        func openPage(_ name: String, params: [AnyHashable : Any], animated: Bool, completion: @escaping (Bool) -> Void) {
            if let present = params["present"] as? Bool {
                if present {
                    let vc = FLBFlutterViewContainer()
                    vc.setName(name, params: params)
                    navigationController?.present(vc, animated: animated, completion: {
                        completion(true)
                    })
                    return
                }
            }
           
            let vc = FLBFlutterViewContainer()
            vc.setName(name, params: params)
            navigationController?.pushViewController(vc, animated: animated)
            completion(true)
        }
        
        func closePage(_ uid: String, animated: Bool, params: [AnyHashable : Any], completion: @escaping (Bool) -> Void) {
            if let vc = navigationController?.presentedViewController as? FLBFlutterViewContainer {
                if vc.isKind(of: FLBFlutterViewContainer.self) && vc.uniqueIDString == uid {
                    vc.dismiss(animated: animated) { }
                    return
                }
            }
            navigationController?.popViewController(animated: animated)
        }
        
        func flutterCanPop(_ canpop: Bool) {
            navigationController?.interactivePopGestureRecognizer?.isEnabled = canpop
        }
        
    }
    

    其中的openPage 方法会接收来至flutter-->native以及native-->flutter的页面跳转,可以根据需求书写。

    Native 跳转 Dart

    let paramsTem : [String : Any] = ["id": self.entity.OrderId,
                                                  "orderNumber": self.entity.CustomOrderNumber ?? "",
                                                  "token": SM_token as Any]
    //根据与Dart代码约定的参数传递方式,转成json字符串,当然也可以约定直接用Dictionary。
    let params = ["object": HXHelper.convertDictionaryToString(dict: paramsTem)] 
                
    HXFlutterRouter.sharedRouter.navigationController = self.viewContainingController()!.navigationController
    let flutterVC = FLBFlutterViewContainer()
    flutterVC.setName("AfterSellPage", params: params)
    self.viewContainingController()!.present(flutterVC, animated: true, completion: nil)
    
    

    Dart 与 Native 交互

    同样可以使用消息传递机制使Dart与Native交互

    static const  platform = const MethodChannel('com.novasoftware.ShoppingMall.address');
      Future<Null> use() async {
        try {
          // Dart 传递“Use”消息给 Native
          await platform.invokeMethod('use');
        } on PlatformException catch (e) {
        }
      }
    
    HXFlutterRouter.sharedRouter.navigationController = self.navigationController
    let flutterVC = FLBFlutterViewContainer()
    flutterVC.setName("MyCoupon", params: ["token": SM_token as Any])
    
    if let flutterVc = FlutterBoostPlugin.sharedInstance()?.currentViewController() {
        let channelName = "com.novasoftware.ShoppingMall.address"
        let channel = FlutterMethodChannel(name: channelName, binaryMessenger:flutterVc)
        channel.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: FlutterResult) in
            print(call.method)
            if call.method == "use" {
                // Native接收消息
                flutterVC.dismiss(animated: true, completion: {
                    self?.tabBarController?.selectedIndex = 0
                })
            }
        }
    }
    self.present(flutterVC, animated: true, completion: nil)
    

    问题:

    1. 打开一个dart页面后,侧滑返回手势失效

    打开一个dart页面返回后。发现侧滑返回手势失效了。
    打断点调试,发现打push一个Dart页面后,

    interactivePopGestureRecognizer?.isEnabled == false.
    
    image

    应该是flutter boost打开一个Dart页面后,将其设置未false了。检测发现FLBPlatform协议有控制方法。

    image

    实现该方法,返回true即可。

    funcflutterCanPop(_canpop:Bool) {
        navigationController?.interactivePopGestureRecognizer?.isEnabled = true
    }
    

    总结:

    到此Flutter Boost初步集成。中间踩了太多坑,当然还有很多坑还没跳出来。
    目前使用发现还是有内存泄露,不过已经比直接使用好用很多。
    还是非常感谢闲鱼团队。

    待解决的问题:
    1. Dart 跳转Native 页面。 爬文档并没找到Dart是怎么跳转iOS 原生页面代码的,不过安卓是能实现这个功能的。
    2. 老项目集成Flutter后,每次编译都非常慢,大概需要花费两三分钟那样子,应该是每次编译都要去跑一遍flutter代码导致的。
    3. Flutter Boost 还有一点内存泄露问题。

    相关文章

      网友评论

          本文标题:Flutter && Flutter_Boost 之 iOS 混

          本文链接:https://www.haomeiwen.com/subject/evcjlctx.html