iOS&Flutter混合開發的探索歷程

leonard發表於2021-06-01

學習並使用新的技術與開發方案是每個程式設計師的必修之路。混合開發作為當下比較流行的專案解決方案,已經發展出了很多成熟的技術語言,如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方式進行整合。

相關文章