Post

已有项目接入Flutter

已有项目接入Flutter

参考:

前言

世界唯一不变的是一直在变,最近项目里计划把部分模块使用flutter的SDK的方式接入,记得之前写flutter还是19年的时候,现在已经过去了5年多,时间真实过的快 现在把已经存在的项目里接入flutter模块的流程及遇到的问题记录下

一、新电脑安装flutter环境

1
2
3
4
5
6
# 检查 Flutter 是否安装
flutter --version
# 没有安装的话直接使用brew的方式安装
brew install flutter
# 安装好后检查环境
flutter doctor

二、在已有的iOS项目里添加flutter模块 方式1、源码接入 1、在项目的最外层初始化flutter的模块,模块名随便定,我的叫my_flutter_module

1
2
3
4
5
6
# 创建flutter项目或直接复制已存在的flutter项目
flutter create --template module my_flutter_module
# 清理flutter缓存
flutter clean
# 更新flutter依赖
flutter pub get

2、修改Podfile如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# Uncomment the next line to define a global platform for your project
platform :ios, '12.0'
source 'https://github.com/CocoaPods/Specs.git'

# flutter配置1(引入flutter的项目位置)
flutter_application_path = '../my_flutter_module'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

abstract_target 'abstract_pod' do  #这里的abstract_pod在实际targets中不存在,是虚拟
   inhibit_all_warnings!
   use_frameworks!
   
   pod 'Alamofire', '~> 4.9.1'
   pod 'SnapKit', '~> 5.0.1'
   pod 'Kingfisher', '~> 5.14.1'

  target 'HelloWorld' do
    # flutter配置2(必须卸载target里,如果写在abstract_target里会报错 [!] Invalid `Podfile` file: [!] Script phases cannot be added to abstract targets..)
    # install_all_flutter_pods(flutter_application_path)
  end

end

post_install do |installer|
    # flutter配置3
    flutter_post_install(installer) if defined?(flutter_post_install)
    installer.pods_project.targets.each do |target|
      if target.name == 'BoringSSL-GRPC'
        # 解决BoringSSL-GRPC报错问题
        target.source_build_phase.files.each do |file|
          if file.settings && file.settings['COMPILER_FLAGS']
            flags = file.settings['COMPILER_FLAGS'].split
            flags.reject! { |flag| flag == '-GCC_WARN_INHIBIT_ALL_WARNINGS' }
            file.settings['COMPILER_FLAGS'] = flags.join(' ')
          end
        end
      end
      target.build_configurations.each do |config|
        # 解决M系列芯片报错问题
        config.build_settings['ONLY_ACTIVE_ARCH'] = 'NO'
        config.build_settings['LD_NO_PIE'] = 'NO'
        config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
        config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
      end
    end
end

3、项目里修改使用 Appdelegate.swift增加如下懒加载代码,方便在使用时再初始化Flutter引擎

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import Flutter
import FlutterPluginRegistrant

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    private var flutterEngine : FlutterEngine?
    var engine: FlutterEngine? {
        get {
            if let engine = flutterEngine {
                return engine
            }
            let engine = FlutterEngine(name: "xxxx", project: nil)
            self.flutterEngine = engine
            self.flutterEngine?.run(withEntrypoint: nil)
            GeneratedPluginRegistrant.register(with: engine)
            return engine
        } set {
            if newValue == nil {
                // 清空
                flutterEngine = nil
            } else {
                flutterEngine = newValue
            }
        }
    }
}

在调用的页面MyViewController.swift调用APP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import UIKit
import Flutter

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let btn = UIButton(type: .custom)
        btn.frame = CGRect(x: 50, y: 100, width: 200, height: 50)
        btn.backgroundColor = .blue
        btn.setTitle("present 进入flutter页面", for: .normal)
        btn.addTarget(self, action: #selector(enterFlutterPage(btn:)), for: .touchUpInside)
        self.view.addSubview(btn)
        
        let btn2 = UIButton(type: .custom)
        btn2.frame = CGRect(x: 50, y: 200, width: 200, height: 50)
        btn2.backgroundColor = .blue
        btn2.setTitle("push 进入flutter页面", for: .normal)
        btn2.tag = 2
        btn2.addTarget(self, action: #selector(enterFlutterPage(btn:)), for: .touchUpInside)
        self.view.addSubview(btn2)
        
    }

    var methodChannel : FlutterMethodChannel?
    var count = 0
    lazy var counterLabel: UILabel = {
        let label = UILabel()
        label.textAlignment = .left
        label.font = UIFont.systemFont(ofSize: 17)
        label.frame = CGRectMake(10, 50, 200, 40)
        label.backgroundColor = .green
        self.view.addSubview(label)
        return label
    }()
    // MARK: App进入flutter页面
    @objc func enterFlutterPage(btn: UIButton) {
        if let engine = (UIApplication.shared.delegate as? AppDelegate)?.engine {        
            regsiterFlutterHandler(engine: engine)
            let flutterViewController = MyFlutterViewController(engine: engine, nibName: nil, bundle: nil)
            // 全屏展示
            if btn.tag == 2 {
                self.navigationController?.pushViewController(flutterViewController, animated: false)
            } else {
                //            flutterViewController.modalPresentationStyle = .fullScreen
                self.present(flutterViewController, animated: false, completion: nil)
            }
        }
   
    }
    
    // MARK: 注册让flutter可以调用原生App
    func regsiterFlutterHandler(engine: FlutterEngine) {
        methodChannel = FlutterMethodChannel(name: "dev.flutter.example/counter",
                                             binaryMessenger: engine.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.counterLabel.text = "Current counter: \(strongSelf.count)"
                    strongSelf.reportCounter()
                case "requestCounter":
                    strongSelf.reportCounter()
                case "requestDismiss":
                    engine.viewController?.dismiss(animated: true)
                default:
                    // Unrecognized method name
                    print("Unrecognized method name: \(call.method)")
                }
            }
        })
    }
    // MARK: 原生APP调用flutter
    func reportCounter() {
        methodChannel?.invokeMethod("reportCounter", arguments: count)
    }
}

class MyFlutterViewController: FlutterViewController {
    deinit {
        print("deinit--\(self)")
    }
    
    override func viewDidLoad() {
        print("viewDidLoad--\(self)")
        self.view.backgroundColor = .white
        super.viewDidLoad()
        
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        print("viewDidDisappear--isBeingDismissed:\(self.isBeingDismissed)--isMovingFromParent:\(self.isMovingFromParent)--\(self)")
        super.viewDidDisappear(animated)
        // 判断是否是被销毁
        if self.isBeingDismissed || self.isMovingFromParent {
            // isBeingDismissed: present展示的,拖拽到底部、或调用dismiss关闭时,值为true
            // isMovingFromParent:push展示的,侧滑、或者调用pop返回关闭时,值为true
            // 执行销毁或清理操作
            if let engine = (UIApplication.shared.delegate as? AppDelegate)?.engine,
               engine.viewController == self {
                engine.viewController = nil
                engine.destroyContext()
                (UIApplication.shared.delegate as? AppDelegate)?.engine = nil
            }
            print("FlutterViewController 被销毁")
        } else {
            // 只是暂时隐藏,不执行销毁操作
            print("FlutterViewController 被隐藏")
        }
    }
    
    
    override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
        print("dismiss(animated:completion:)--\(self)")
        super.dismiss(animated: flag, completion: completion)
    }
}


使用安装的tree工具查看的项目结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 比如存在一个HelloWorld的项目
# tree -L 3
.
├── HelloWorld
│   ├── HelloWorld
│   │   ├── AppDelegate.swift
│   │   ├── Assets.xcassets
│   │   ├── Base.lproj
│   │   ├── Info.plist
│   │   ├── SceneDelegate.swift
│   │   └── ViewController.swift
│   ├── HelloWorld.xcodeproj
│   │   ├── project.pbxproj
│   │   ├── project.xcworkspace
│   │   └── xcuserdata
│   ├── HelloWorld.xcworkspace
│   │   ├── contents.xcworkspacedata
│   │   ├── xcshareddata
│   │   └── xcuserdata
│   ├── HelloWorldTests
│   │   └── HelloWorldTests.swift
│   ├── HelloWorldUITests
│   │   ├── HelloWorldUITests.swift
│   │   └── HelloWorldUITestsLaunchTests.swift
│   ├── Podfile
│   ├── Podfile.lock
│   └── Pods
│       ├── Alamofire
│       ├── Headers
│       ├── Kingfisher
│       ├── Local Podspecs
│       ├── Manifest.lock
│       ├── Pods.xcodeproj
│       ├── SnapKit
│       └── Target Support Files
└── my_flutter_module
    ├── README.md
    ├── analysis_options.yaml
    ├── lib
    │   └── main.dart
    ├── my_flutter_module.iml
    ├── my_flutter_module_android.iml
    ├── pubspec.lock
    ├── pubspec.yaml
    └── test
        └── widget_test.dart

为了引入flutter项目,需要修改Podfile

方式2、framework

遇到问题1

[!] Invalid Podfile file: [!] Script phases cannot be added to abstract targets..

原因 install_all_flutter_pods(flutter_application_path)写在了abstract_target里了,一定要写在target 'xxx' do

遇到问题2

No such module 'Flutter'

解决方案 重新执行下pod install --verbose, 然后clean下在Xcode执行下clean:cmd + shift + K

遇到问题3

复制粘贴别的已存在的flutter项目到项目里时,报Because flutter_module_using_plugin depends on analysis_defaults from path which doesn't exist (could not find package analysis_defaults at "../../../analysis_defaults") 删除 pubspec.yaml里的analysis_defaults相关的内容

遇到问题4

/Users/xxxx/.pub-cache/hosted/pub.dev/xxxx/ios/PrivacyInfo.xcprivacy /Users/xxx/Documents/.pub-cache/hosted/pub.dev/xxx/ios/PrivacyInfo.xcprivacy: No such file or directory

查看了下缓存其实存在,但是缓存的位置是/Users/xxx/.pub-cache,并且缓存是存在的

1
2
3
4
5
6
7
8
# 查看cache文件
# 这个是空的
cho $PUB_CACHE
# 缓存列表能知道缓存在/Users/xxx/.pub-cache
flutter pub cache list
# 如果经过各种清理缓存仍然报这个错,可以创建个软连接
mkdir -p /Users/xxx/Documents
ln -s /Users/xxx/.pub-cache /Users/xxx/Documents/.pub-cache

问题5

APP不管是present后,不管是拖拽到底部关闭flutter页面,还是在flutter里调用SystemNavigator.pop()或者原生APP里调用dismiss,flutter都不会销毁,导致下次再也展示不了

解决方案:

需要保证flutterEngine.viewController在展示前设置为nil 自定义一个MyFlutterViewController继承FlutterViewController, 在viewDidDisappear(:)里增加判断把engine、channel都断开,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class MyFlutterViewController: FlutterViewController {
    deinit {
        print("deinit--\(self)")
    }
    
    override func viewDidLoad() {
        print("viewDidLoad--\(self)")
        self.view.backgroundColor = .white
        super.viewDidLoad()
        
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        print("viewDidDisappear--isBeingDismissed:\(self.isBeingDismissed)--isMovingFromParent:\(self.isMovingFromParent)--\(self)")
        super.viewDidDisappear(animated)
        // 判断是否是被销毁
        if self.isBeingDismissed || self.isMovingFromParent {
            // isBeingDismissed: present展示的,拖拽到底部、或调用dismiss关闭时,值为true
            // isMovingFromParent:push展示的,侧滑、或者调用pop返回关闭时,值为true
            // 执行销毁或清理操作
            if let engine = (UIApplication.shared.delegate as? AppDelegate)?.engine,
               engine.viewController == self {
                engine.viewController = nil
                engine.destroyContext()
                (UIApplication.shared.delegate as? AppDelegate)?.engine = nil
            }
            print("FlutterViewController 被销毁")
        } else {
            // 只是暂时隐藏,不执行销毁操作
            print("FlutterViewController 被隐藏")
        }
    }
    
    
    override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
        print("dismiss(animated:completion:)--\(self)")
        super.dismiss(animated: flag, completion: completion)
    }
}

This post is licensed under CC BY 4.0 by the author.