集成Flutter模块

1.集成方式

现有iOS工程中,可以添加flutter_module以集成flutter模块。这些模块会以framework的形式被集成到iOS工程中。同时在集成flutter模块前,需要导入依赖的Flutter engine等。为完成这些工作,Flutter根据不同需求提供了三种方式:

1.全自动

CocoaPods管理依赖,这种方式下每次我们build应用时,flutter_module模块中的文件都会被自动编译。这相当于懒人模式,CocoaPods帮我们一键导入,这也是Flutter官方推荐的方式。

2.全手动

对于并非每个人都安装了CocoaPods的团队,可以选择在flutter模块中手动执行flutter build ios-framework命令,为Flutter engine、 你的Dart代码、Flutter插件创建framework,集成这些framework到现有工程并手动更新工程配置。

3.半自动

手动为你的Dart代码、Flutter插件创建framework,同时将Flutter engine作为podspec,用Cocoapods自动集成到工程中。

2.创建flutter模块

首先要切换到现有iOS工程的根目录:

1
cd /Desktop/Hello

在根目录中创建Flutter模块:

1
flutter create --template module flutters  //我这里给flutter模块起名flutters

执行命令行之后,会在/flutters目录中创建flutter模块的子工程。其目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
└── flutters
├── .ios/
│ ├── Runner.xcworkspace
│ └── Flutter/podhelper.rb
├── README.md
├── analysis_options.yaml
├── build
│ └── b6b68957fc5ed9202596a46ebc5bde9b
├── flutters.iml
├── flutters_android.iml
├── lib
│ ├── login.dart //这是我后期创建的Flutter UI代码
│ └── main.dart
├── pubspec.lock
├── pubspec.yaml
└── test
└── widget_test.dart

其中的/lib目录用于存放我们的Dart代码,例如login.dart就是我后面创建的登录UI。

pubspec.yaml是Flutter用到的依赖,如包和插件。

有个隐藏的/.ios目录,需解除隐藏后才能看到(快捷键command+shift+.)。这里存放的是与我们编写的Flutter UI对应的iOS工程,它通过脚本把Dart代码编译成framework,并用CocoaPods把我们的 Flutter 模块自动集成到现有iOS工程中。这个目录下的代码是Flutter自动生成的,无需加入Git管理中。在新设备中运行我们的iOS原生工程前,需要先在/flutters目录中执行以下命令,以便重新生成该/.ios目录:

1
flutter pub get

3.导入framework到iOS工程中

推荐使用CocoaPods的方式导入,原生项目中Podfile文件的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//配置flutter模块的路径
flutter_application_path = './flutters/'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'Hello' do
use_frameworks!
#这里是原生工程的依赖库
pod 'RxSwift'
pod 'RxCocoa'
pod 'RxAlamofire'

#每个需要集成Flutter的target都需要下面这条
install_all_flutter_pods(flutter_application_path)
end

在原生项目根目录下执行命令行:

1
pod install`

这里 podhelper.rb 脚本会把Flutter模块中的插件、Flutter.framework、App.framework 嵌入到我们的原生工程里。其中:

  • Flutter.framework 是 Flutter engine 所在的目录;
  • App.framework 是 Flutter 子项目编译后的 Dart 代码所在目录;

注意,通过CocoaPods这种方式集成时,需要本地安装好Flutter SDK

如果 pubspec.yaml 中更新了依赖的插件,则需要在Flutter模块的目录/flutters中执行以下代码来更新 podhelper.rb 要用到的插件:

1
flutter pub get

接着在iOS原生项目的根目录下执行一次pod install,同步 Flutter 模块的更新。

4.创建flutter页面

/lib目录下main.dart是Flutter自动帮我们生成的一个页面,是默认的flutter页面主入口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import 'package:flutter/material.dart';
import 'login.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const LoginPage(), //展示登录页
);
}
}

现在我们自定义一个登录页面login.dart

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
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class LoginPage extends StatefulWidget {
const LoginPage({Key? key}) : super(key: key);

@override
_LoginPageState createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
// 控制器
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();

// 原生Swift接口
static const platform = MethodChannel('com.Hello.flutters');

// 登录按钮的回调函数
void _handleLogin() async {

String username = _usernameController.text;
String password = _passwordController.text;
String response;

// 调用原生Swift接口
try {
final String result = await platform.invokeMethod('login',
{'username': username,
'password': password}
);
response = '登录成功,欢迎回来,$result!';
} on PlatformException catch (e) {
response = '登录失败:${e.message}';
}

// 在控制台中打印响应信息
print(response);
}

// 返回按钮的回调函数
void _handleBack() {
try {
platform.invokeMethod("back");
} catch (e) {
print('error: $e.message');
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios), onPressed: _handleBack),
title: const Text('用户登录'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: _usernameController,
decoration: const InputDecoration(
hintText: '请输入用户名',
),
),
const SizedBox(height: 20.0),
TextField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(
hintText: '请输入密码',
),
),
const SizedBox(height: 20.0),
ElevatedButton(
onPressed: _handleLogin,
child: const Text('登录'),
),
],
),
),
);
}
}

5.集成flutter页面

集成flutter页面到iOS工程需要FlutterEngineFlutterViewControllerFlutterEngine充当Dart VM和Flutter运行时的主机,FlutterViewController用来向Flutter传递用户输入事件和展示FlutterEngine渲染的画面。

1.注册FlutterEngine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import UIKit
import Flutter
import FlutterPluginRegistrant

@UIApplicationMain
class AppDelegate: FlutterAppDelegate {

lazy var flutterEngine = FlutterEngine(name: "FlutterInSwift")

override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

// 注册Flutter引擎
flutterEngine.run()
GeneratedPluginRegistrant.register(with: flutterEngine)

return true
}
}
2.展示FlutterViewController

创建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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import UIKit
import Flutter

class DKFlutterInSwiftController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

let button = UIButton.init(type: .custom)
button.setTitle("Click to flutter page", for: .normal)
button.setTitleColor(UIColor.white, for: .normal)
button.backgroundColor = UIColor.systemBlue
button.layer.cornerRadius = 6
button.addTarget(self, action: #selector(handleClick), for: .touchUpInside)
button.sizeToFit()
view.addSubview(button)
view.backgroundColor = UIColor.white

let width:CGFloat = button.frame.size.width+20
let height:CGFloat = 45.0
let frame = CGRect.init(x: (view.frame.size.width - width) / 2.0, y: (view.frame.size.height - height) / 2.0, width: width, height: height)
button.frame = frame
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(false, animated: true)
}

@objc func handleClick(){
//跳转Flutter页面
let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine
let flutterController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
navigationController?.setNavigationBarHidden(true, animated: true)
navigationController?.pushViewController(flutterController, animated: true)

//Swift与Flutter通信
let channel = FlutterMethodChannel(name: "com.Hello.flutters", binaryMessenger: flutterController as! FlutterBinaryMessenger)
channel.setMethodCallHandler { [weak self] (call:FlutterMethodCall, result:@escaping FlutterResult) in
//登录返回
if (call.method == "back") {
self?.navigationController?.popViewController(animated: true)
}
//登录
else if (call.method == "login") {
result("This is a response from Swift");
}
}
}
}

在Swift页面中放置了一个按钮,点击后会跳转到flutter页面。具体是哪个flutter页面呢?这是由Dart的主入口函数决定的。FlutterEngine默认会加载flutter模块下 lib/main.dart 文件中的主入口main()函数。前面的flutter模块代码中,我们在main.dart中显示的是一个自定义的登录页面login.dart。因此,FlutterViewController最终展示的就是这个登录页面。

至此,我们已经将flutter模块中的登录页面集成到iOS工程中了~

flutter登录页

6.修改启动配置

上面集成flutter页面时使用的是默认的启动配置,即加载main()入口,展示其home:登录页。

有时我们需要自定义Dart入口,或者加载home以外的其他flutter页面。这时我们就需要修改对应的启动配置。

1.修改入口

使用lib/main.dart文件中main()以外的函数作为主入口时,需要对此函数做特殊标记:

1
2
3
4
//当前在main2.dart文件中

@pragma('vm:entry-point')
void myNewEntry() { ... };
2.重新指定入口

默认情况下,我们使用flutterEngine.run()来加载默认入口函数main()。标记并修改入口函数后,我们可以在Swift中使用run(withEntrypoint:)来重新指定入口:

1
flutterEngine.run(withEntrypoint: "myNewEntry", libraryURI: "main2.dart")

这样最终展示的就是myNewEntry函数中设置的根路由页面了。

3.修改默认路由

Dart中除了根路由外还有其他页面,可以给这些页面命名并注册到路由表中,以便通过名字直接加载这些页面。我们在iOS工程中展示的flutter页面肯定也不止一个,使用命名路由可以方便我们指定需要跳转的页面。

下面我们再自定义一个flutter页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import 'package:flutter/material.dart';

class WelcomePage extends StatelessWidget {
const WelcomePage({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios), onPressed: () {}),
title: const Text('新路由'),
),
body: const Center(
child: Text(
"欢迎~",
style: TextStyle(
fontSize: 25, fontWeight: FontWeight.bold, color: Colors.blue),
),
),
);
}
}

这只是一个简单的欢迎页面,接下来我们修改main()中的路由配置:

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
import 'package:flutter/material.dart';
import 'login.dart';
import 'welcome.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);

// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: "/",
//注册路由表
routes: {
"/": (context) => const LoginPage(), //注册首页路由
"welcome": (context) => const WelcomePage(),
});
}
}

接下来我们就可以通过名字welcome来加载这个flutter欢迎页了。

第一是种修改FlutterEnginerun方法;

第二是种修改FlutterViewController的初始化方法。

二者选其一即可,比如:

3.1.修改FlutterEngine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import Flutter
import FlutterPluginRegistrant

@UIApplicationMain
class AppDelegate: FlutterAppDelegate {

lazy var flutterEngine = FlutterEngine(name: "FlutterInSwift")

override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

// 给Flutter引擎指定路由名
flutterEngine.run(withEntrypoint: "main", initialRoute: "welcome")
//这里"main"是Dart主入口,"welcome"是flutter页面名
GeneratedPluginRegistrant.register(with: flutterEngine)

return true
}
}

这里是修改了FlutterEngine的run方法,FlutterViewController处不做修改。

3.2.修改FlutterViewController
1
2
3
4
5
6
7
@objc func handleClick(){
//跳转Flutter页面
let flutterController = FlutterViewController(
project: nil, initialRoute: "welcome", nibName: nil, bundle: nil)
navigationController?.setNavigationBarHidden(true, animated: true)
navigationController?.pushViewController(flutterController, animated: true)
}

这里是给FlutterViewController指定加载名字为welcome的flutter页面。AppDelegate的FlutterEngine处不作修改。

flutter欢迎页

以上就是在iOS工程中集成flutter页面的思路总结。完结撒花~


集成Flutter模块
https://davidlii.cn/2023/04/23/flutter-embed.html
作者
Davidli
发布于
2023年4月23日
许可协议