Flutter && Flutter_Boost 之 iOS 混编开发

  • 官网:https://flutter.dev/
  • Flutter 中文网:https://flutterchina.club/
  • Flutter Git: https://github.com/flutter/flutter
  • Flutter boost: https://github.com/alibaba/flutter_boost

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了,接下来就可以进行开发了。


Flutter && Flutter_Boost 之 iOS 混编开发_第1张图片
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的开发语言:


Flutter && Flutter_Boost 之 iOS 混编开发_第2张图片
image.png

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

flutter create -i swift -a kotlin Name

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


Flutter && Flutter_Boost 之 iOS 混编开发_第3张图片
image.png

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


Flutter && Flutter_Boost 之 iOS 混编开发_第4张图片
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三个项目的根目录必须在同一目录下:


Flutter && Flutter_Boost 之 iOS 混编开发_第5张图片
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)
Flutter && Flutter_Boost 之 iOS 混编开发_第6张图片
image.png

打开Flutter项目 , 获取包:

flutter packages get

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

flutter build ios

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

pod install

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


Flutter && Flutter_Boost 之 iOS 混编开发_第7张图片
image.png
  • iOS项目Enable Bitcode改为NO


    Flutter && Flutter_Boost 之 iOS 混编开发_第8张图片
    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
Flutter && Flutter_Boost 之 iOS 混编开发_第9张图片
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 _kReloadChannel =
    BasicMessageChannel(_kReloadChannelName, StringCodec());

void main() {
  _kReloadChannel.setMessageHandler(run);
  print("Flutter---" + ui.window.defaultRouteName);
  run(ui.window.defaultRouteName);
}

Future 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 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
Flutter && Flutter_Boost 之 iOS 混编开发_第10张图片
image.png

之后调用:

flutter packages get

执行:

flutter build ios

在iOS的根目录下执行:

pod install

使iOS和flutter都添加FlutterBoost插件。


Flutter && Flutter_Boost 之 iOS 混编开发_第11张图片
image.png
  • Dart 代码集成
void main() {
  runApp(MyApp());
}
class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}
class _MyAppState extends State {
  @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 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即可。


    Flutter && Flutter_Boost 之 iOS 混编开发_第12张图片
    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 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协议有控制方法。

Flutter && Flutter_Boost 之 iOS 混编开发_第13张图片
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 混编开发)