最近公司想分別兩頁均顯示同一個Flutter module 記錄一下

首先參考了官方的add_to_app sample

flutter/samples

commit id: 6e9824d

首先先跑起官方的sample, 今次我只研究了iOS

  #!/bin/bash
  set -e

  cd flutter_module_using_plugin
  flutter pub get
  cd ../flutter_module_books
  flutter pub get
  cd ../flutter_module
  flutter pub get

  # For Android builds:
  flutter build aar

  # For iOS builds:
  flutter build ios-framework --xcframework --output=../ios_using_prebuilt_module/Flutter
  cd ../ios_fullscreen
  pod install
  cd ../ios_using_plugin
  pod install

我是使用 ios_using_prebuilt_module 的例子

官方sample流程如下

  1. AppDelegate 先預熱(prewarm) FlutterEngine
  2. 一個Apps 只可以有一個FlutterEngine
  3. 進入ViewController viewDidLoad 時在AppDelegate 取回 FlutterEngine 並建立methodChannel
  4. 點擊button時, 在AppDelegate 取回 FlutterEngineFlutterViewControllerpresent出來

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/131c3657-0934-4770-9db5-80afdd7dec82/CleanShot_2020-12-30_at_16.08.56.gif

為了分別兩頁均顯示同一個Flutter, methodChannel 需要放到AppDelegate 因為兩頁需要共用同一個methodChannel

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    
    var flutterEngine : FlutterEngine?
    var methodChannel : FlutterMethodChannel?
    var count = 0
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Instantiate Flutter engine
        self.flutterEngine = FlutterEngine(name: "io.flutter", project: nil)
        self.flutterEngine?.run(withEntrypoint: nil)
        GeneratedPluginRegistrant.register(with: self.flutterEngine!)
        
        if let flutterEngine = (UIApplication.shared.delegate as? AppDelegate)?.flutterEngine {
            methodChannel = FlutterMethodChannel(name: "dev.flutter.example/counter",
                                                 binaryMessenger: flutterEngine.binaryMessenger)
            methodChannel?.setMethodCallHandler({ [weak self]
                (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
                if let strongSelf = self {
                    switch(call.method) {
                    case "incrementCounter":
                        strongSelf.count += 1
                        strongSelf.reportCounter()
                    case "requestCounter":
                        strongSelf.reportCounter()
                    default:
                        // Unrecognized method name
                        print("Unrecognized method name: \\(call.method)")
                    }
                }
            })
        }
        
        return true
    }
    
    func reportCounter() {
        methodChannel?.invokeMethod("reportCounter", arguments: count)
    }
}

為了更貼近真實場景,我使用了TabbarController

第一個(紅色背景)中間白色的部份顯示Flutter View

第二個NavigationController 的rootViewController 顯示Flutter View

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ca7bf9f6-d002-4aff-8348-c6edeaa2462b/Untitled.png

import UIKit
import Flutter

class ViewController: UIViewController {
    
    @IBOutlet weak var flutterView: UIView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        if let flutterEngine = (UIApplication.shared.delegate as? AppDelegate)?.flutterEngine,
           let methodChannel = (UIApplication.shared.delegate as? AppDelegate)?.methodChannel{
            flutterEngine.viewController = nil
            let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
            self.embed(flutterViewController, inView: flutterView)
            //methodChannel.invokeMethod("reportCounter", arguments: 100)
        }
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        self.removeChild()
    }
}

extension UIViewController {
    func embed(_ viewController:UIViewController, inView view:UIView){
        viewController.willMove(toParent: self)
        viewController.view.frame = view.bounds
        view.addSubview(viewController.view)
        self.addChild(viewController)
        viewController.didMove(toParent: self)
    }
    
    func removeChild() {
        self.children.forEach {
            $0.willMove(toParent: nil)
            $0.view.removeFromSuperview()
            $0.removeFromParent()
        }
    }
}
import UIKit
import Flutter

class NavigationController: UINavigationController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        if let flutterEngine = (UIApplication.shared.delegate as? AppDelegate)?.flutterEngine,
           let methodChannel = (UIApplication.shared.delegate as? AppDelegate)?.methodChannel{
            flutterEngine.viewController = nil
            let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
            self.setViewControllers([flutterViewController], animated: false)
        }
    }
}

注意事項

  1. 必須在 viewWillAppear 是放入flutterViewController, 否則可能會沒有反應

    flutter view controller can't response click event after pop from native view controller · Issue #59547 · flutter/flutter

  2. 在顯示flutterViewController 前必須先將flutterEngine.viewController 清空, 否則會有錯誤

    FlutterEngine <FlutterEngine: 0x7fa004c11c00> is already used with FlutterViewController instance <FlutterViewController: 0x7fa00882ec00>. One instance of the FlutterEngine can only be attached to one FlutterViewController at a time. Set FlutterEngine.viewController to nil before attaching it to another FlutterViewController.

    flutterEngine.viewController = nil