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工程的根目录:
在根目录中创建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 │ └── 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
目录:
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
|
在原生项目根目录下执行命令行:
这里 podhelper.rb 脚本会把Flutter模块中的插件、Flutter.framework、App.framework 嵌入到我们的原生工程里。其中:
- Flutter.framework 是 Flutter engine 所在的目录;
- App.framework 是 Flutter 子项目编译后的 Dart 代码所在目录;
注意,通过CocoaPods这种方式集成时,需要本地安装好Flutter SDK
。
如果 pubspec.yaml 中更新了依赖的插件,则需要在Flutter模块的目录/flutters
中执行以下代码来更新 podhelper.rb 要用到的插件:
接着在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); @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();
static const platform = MethodChannel('com.Hello.flutters');
void _handleLogin() async {
String username = _usernameController.text; String password = _passwordController.text; String response;
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工程需要FlutterEngine
和FlutterViewController
。FlutterEngine
充当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 { 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(){ 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)
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工程中了~
6.修改启动配置
上面集成flutter页面时使用的是默认的启动配置,即加载main()
入口,展示其home
:登录页。
有时我们需要自定义Dart入口,或者加载home
以外的其他flutter页面。这时我们就需要修改对应的启动配置。
1.修改入口
使用lib/main.dart
文件中main()
以外的函数作为主入口时,需要对此函数做特殊标记:
1 2 3 4
|
@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);
@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欢迎页了。
第一是种修改FlutterEngine
的run
方法;
第二是种修改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 { flutterEngine.run(withEntrypoint: "main", initialRoute: "welcome") GeneratedPluginRegistrant.register(with: flutterEngine) return true } }
|
这里是修改了FlutterEngine的run
方法,FlutterViewController
处不做修改。
3.2.修改FlutterViewController
1 2 3 4 5 6 7
| @objc func handleClick(){ 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处不作修改。
以上就是在iOS工程中集成flutter页面的思路总结。完结撒花~