iOS&Flutter混合开发的探索历程

660 阅读5分钟

学习并使用新的技术与开发方案是每个程序员的必修之路。混合开发作为当下比较流行的项目解决方案,已经发展出了很多成熟的技术语言,如Weex、Rect Native、Flutter等,而Flutter因自绘引擎特性得已自成一派,受许多开发者追捧。

接入流程

这部分主要讲述如何在原iOS项目中添加集成Flutter模块,以及在引入Flutter资源过程中踩过的一些坑,对于其他开发场景可能会不太适用。我会在文章末尾附上一些开发过程中查阅的博客和网站,希望能够帮助大家更好的了解混合开发。

集成Flutter资源

安装Flutter SDK

使用及开发Flutter模块前,需要开发者前往Flutter官网下载对应版本的SDK文件(如果是协同开发,SDK版本同步很重要),并根据自己的需要安装到电脑上。推荐安装到根目录下新建的development目录下,方便日后更新。 接下来需要配置一些环境变量,以Mac OS系统为例,首先在根目录下的 ~/.bash_profile 中添加如下语句:

#FLUTTER_INSTALL_PATH为Flutter SDK安装目录名称,如development
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
export PATH=/Users/[user]/[FLUTTER_INSTALL_PATH]/flutter/bin:$PATH

配置保存后,执行Flutter doctor命令,如果输出结果正常,则配置完成。

有的小伙伴可能会发现配置完后每次重新打开终端都要重新 source ~/.bash_profile 才可以正常使用 flutter 命令,这是因为 zsh 加载的是 ~/.zshrc 文件,而 .zshrc 文件中并没有定义任务环境变量。 解决办法是在 .zshrc 里面加入一行 source ~/.bash_profile,配置完成后就不用重复使用 source ~/.bash_profile 了。

使用Cocoapods引入Flutter项目

如果Flutter项目维护周期不是特别频繁,开发者可以选择导入Flutter Framework的方向引入Flutter项目,过程和使用其他静态库的方式一样,比较容易上手。 在Cocoapods的 Podfile 文件中加入以下命令:

#将
flutter_application_path = '../xxxx_flutter/'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'xxxx' do
   # Comment the next line if you don't want to use dynamic frameworks
   use_frameworks!

   install_all_flutter_pods(flutter_application_path)

end

执行 pod install 后,iOS项目就成功的把Flutter项目资源集成进来了。值得一提的是,Flutter不支持 Bitcode ,需要开发者在 Build Settings 禁用 Bitcode。如果 Archive 过程中出现在错误,还需要在 Podfile 中添加:

#放在文件的最后面就可以
post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['ENABLE_BITCODE'] = 'NO'
    end
  end
end

以上是将Flutter引入到现有iOS项目中的解决方案,每次Flutter资源有较大更新时,都需要在Flutter文件目录下执行 flutter pub get,然后执行 pod install,使用Framework方式集成则不需要这个步骤。

在Swift中使用Flutter

碍于Flutter对iOS Native支持的局限性,在iOS中使用Flutter原生API,很容易产生内存泄漏以及一些未知的错误。在本篇文章中,我推荐使用咸鱼团队提供的 FlutterBoost 框架,在其内部集成了对Flutter的模块的封装接口,可以更轻松便捷地使Native和Flutter进行交互。 FlutterBoost OC代码编写,在Swift中需要先使用桥接文件声明,接下来按照文档要求实现对应的 Route 管理类:

class FlutterRouteManager: NSObject, FLBPlatform {
    
    // MARK: - open
    static func openPage(_ url: String, _ present: Bool = false, completion: (([AnyHashable : Any]) -> ())? = nil) {
        FlutterBoostPlugin.open(route.name(), urlParams: present ? ["present": present] : [:], exts: ["animated": true], onPageFinished: { (result) in
            completion?(result)
        }) { (state) in
            //
        }
    }
    
    // MARK: - delegate
    func open(_ url: String, urlParams: [AnyHashable : Any], exts: [AnyHashable : Any], completion: @escaping (Bool) -> Void) {
        var animated = true
        if let extsAnimated = exts["animated"] as? Bool {
            animated = extsAnimated
        }
        let viewController = ZYFlutterViewController()
        viewController.setName(url, params: urlParams)
        navigationController()?.pushViewController(viewController, animated: animated)
        completion(true)
    }
    
    func present(_ url: String, urlParams: [AnyHashable : Any], exts: [AnyHashable : Any], completion: @escaping (Bool) -> Void) {
        var animated = true
        if let extsAnimated = exts["animated"] as? Bool {
            animated = extsAnimated
        }
        let viewController = ZYFlutterViewController()
        viewController.setName(url, params: urlParams)
        navigationController()?.present(viewController, animated: animated, completion: {
            completion(true)
        })
    }
    
    func close(_ uid: String, result: [AnyHashable : Any], exts: [AnyHashable : Any], completion: @escaping (Bool) -> Void) {
        var animated = true
        if let extsAnimated = exts["animated"] as? Bool {
            animated = extsAnimated
        }
        let presentedVC = navigationController()?.presentingViewController
        let viewController = presentedVC as? FLBFlutterViewContainer
        if viewController?.uniqueIDString() == uid {
            viewController?.dismiss(animated: animated, completion: {
                completion(true)
            })
        } else {
            navigationController()?.popViewController(animated: animated)
        }
    }
}

当我们需要打开某个Flutter页面时,只需要调用对应的类方法就可以实现。

FlutterRouteManager.openPage(<#T##url: String##String#>, <#T##present: Bool##Bool#>, completion: <#T##(([AnyHashable : Any]) -> ())?##(([AnyHashable : Any]) -> ())?##([AnyHashable : Any]) -> ()#>)

通过尝试后,我们已经可以正常打开某个Flutter页面,但是在页面跳转的过程中,我们可以看到一个很明显的 Launch 页面,这显然不符合我们的程序要求。接下来我们需要在iOS程序启动时,提前注册Flutter资源。通过 FlutterBoost,我们可以使用以下方式来完成。

#在AppDelegate文件或恰当的时机使用如下方式预置Flutter资源
FlutterBoostPlugin.sharedInstance().startFlutter(with: FlutterRouteManager()) { (flutterEngine) in
     DispatchQueue.main.async {
         //bind channel
         //可以在这里绑定和Flutter的交互句柄
     }
}

Native与Flutter交互

在Flutter中提供与JS类似的与Native进行交互的接口,FlutterMethodChannel 类能够很好的帮助开发者完成类似的需求,在Swift中,我们可以使用以下代码来实现向Flutter端传递消息。

let assetPluginChannel = FlutterMethodChannel(name: "AssetPlugin", binaryMessenger: messenger)
assetPluginChannel?.invokeMethod("Givemetext", arguments: nil, result: { (result) in
     //交互结果
})

在开发中也可以使用消息回传的方式来达到交互的目的,像下面这段代码一样。

#使用flutterResult可以在需要时传递参数到flutter
assetPluginChannel?.setMethodCallHandler { (methodCall, flutterResult) in
      DispatchQueue.main.async {
          //根据method或者arguments完成需要的动作
          switch methodCall.method {
              case "someText":
                print("Show me some text!")
              default:
                break
          }
      }
}

存在的问题

  • 在iOS页面跳转过程中可以发现还是会有一些内存泄漏的现象出现,如果要跳转的Flutter页面资源较多,还可能会出现卡顿的现象,流畅性不如原生开发的页面。
  • 如果某个Flutter页面中跳转了下一级Flutter页面,使用iOS自带的侧滑功能,会同时将两个页面都 pop 掉,需要开发者在这里处理好兼容的问题。
  • 使用Cocoapods方式集成Flutter项目,不支持在 Debug 模式下进行 Archive,如果这种打包方式是必要的,建议使用Framework方式进行集成。