已有项目接入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)
}
}