目的: 在iOS原生项目中,嵌套Flutter页面,实现原生与Flutter的通信。
实现方案:为了把Flutter项目引入到原生工程,我们需要将Flutter工程改造为原生工程的一个组件依赖,并以组件化的方式管理不同平台的Flutter构建产物,即iOS平台通过pod进行依赖管理,安卓平台使用aar。这样我们就可以在iOS工程中使用FlutterViewController,安卓使用FlutterView。为Flutter搭建应用入口,实现Flutter与原生的混合开发。
最终实现效果如下:
- 首页是原生页面,顶部定义Label,用于传值到Flutter页面。下面定义了两个按钮,都用于跳转到Flutter页面。
- 第一个按钮,Push到Flutter页面后,显示原生的导航条,Flutter页面显示原生传过来的内容。
- 第二个按钮,Push到Flutter页面后,显示Flutter的导航条,Flutter页面显示原生传过来的内容。再从Flutter页面跳转到原生页面,原生页面显示Flutter页面传过来的内容。
具体实现如下:
一:创建Flutter module工程
1: 为了让Flutter工程能够通过pod进行管理,首先要创建一个Flutter module项目:
Project location:选择和原生工程同一级目录,
Project type:选择Module,
原生语言和平台选择对应的即可,如下图所示:
2: Flutter module创建后,应该和iOS原生工程放在同一目录(方便管理),如下图所示:
二:FlutterBoost介绍与引入到工程
1: FlutterBoost是一个Flutter插件,它是新一代Flutter-Native混合解决方案 ,它可以轻松地为现有原生应用程序提供Flutter混合集成方案。FlutterBoost的理念是将Flutter在原生工程中像Webview那样来使用。我们知道在现有应用程序中同时管理Native页面和Flutter页面并非易事, 因此FlutterBoost帮你处理页面的映射和跳转,你只需关心页面的名字和参数即可(通常可以是URL)。
FlutterBoost的基本功能:
- 可复用的通用型混合开发方案
- 支持更加复杂的混合模式,比如支持tab切换的场景
- 无侵入性方案,使用时不再依赖修改Flutter的方案
- 支持页面生命周期统一的管理。
- iOS和安卓双端统一,具有统一明确的设计概念。
2:在flutter_module
中引入FlutterBoost 插件(版本号:5.0.1
)
1: 在flutter_module
项目中找到pubspec.yaml
文件,在dependencies
中配置flutter_boost
:
version: 1.0.0+1
environment:
sdk: '>=3.2.3 <4.0.0'
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
flutter_boost:
git:
url: 'https://github.com/alibaba/flutter_boost.git'
ref: '5.0.1'
2: 终端执行flutter pub get
命令,安装插件,安装完成后,就可以使用该插件了。
3:FlutterBoost基础配置
1: 在main.dart文件中,导入flutter_boost库
import 'package:flutter_boost/flutter_boost.dart';
2: 如果你的工程里已经有一个继承自WidgetsFlutterBinding
的自定义Binding
,则只需要将其with
上BoostFlutterBinding
,如果你的工程没有自定义的Binding
,则可以参考CustomFlutterBinding
的做法 ,BoostFlutterBinding
用于接管Flutter页面的生命周期,必须得接入的。
创建一个自定义的Binding
,继承和with
的关系如下,里面什么都不用写
class CustomFlutterBinding extends WidgetsFlutterBinding with BoostFlutterBinding {}
3: 在void main
方法中调用CustomFlutterBinding()
void main() {
/// 这里的CustomFlutterBinding调用务必不可缺少,用于控制Boost状态的resume和pause
CustomFlutterBinding();
runApp(const MyApp());
}
4: 创建一个有状态
的MyApp模版类,在类中配置flutterBoost的路由,路由主要是定义页面名称和参数,从而根据路由实现Flutter和原生的通信。
在_MyAppState
类中声明一个routerMap
变量,对每一个页面的路由方式进行配置。如果想用类似iOS平台的跳转动画(默认跳转动画是安卓样式),那么只需要像下面这样写成CupertinoPageRoute
即可。
boostRouterMap变量的作用:
boostRouterMap
中的string是页面的名称,下面中的homepage
就是我们自定义的一个页面,而FlutterBoostRouteFactory
是一个方法的重命名。这个方法的传入参数是路由的设置和uniqueID
, 返回值是路由Route
类。
在routerMap
中我们看到的homepage
字符串右边的模块其实就是该方法的具体实现,包括参数、函数体,返回的是一个路由的CupertinoPageRoute
,其为Route
的子类。
uniqueId
其实在这里并不会用到,我们真正关心的是Settings
中的参数arguments
,其也为一个Map
类型。我们可以从中获取要打开的新页面所必须使用的一些参数,并传入。
class _MyAppState extends State<MyApp> {
Map<String, FlutterBoostRouteFactory> routerMap = {
'homepage': (settings, uniqueId) {
return CupertinoPageRoute( // CupertinoPageRoute:类似iOS页面的Push效果
settings: settings,
builder: (_) {
Map<String, dynamic>? map = settings.arguments as Map<String,
dynamic>;
String data = map['data'] as String;
return Homepage(
data: data,
);
});
},
}
}
routeFactory方法的作用:
要在build
方法中创建FlutterBoostApp
这样一个Widget,那么就需要传入一个FlutterBoostRouteFactory
类型的参数。
// 在重写的build方法中,构建FlutterBoostApp,将routeFactory和appBuilder这两个方法作为参数传入。
Widget build(BuildContext context) {
return FlutterBoostApp(
routeFactory,
appBuilder: appBuilder,
);
}
我们实现一个routeFactory
方法,该方法的参数和返回值均同FlutterBoostRouteFactory
类型相同,所以可以直接当作参数值传入到FlutterBoostApp
的初始化方法中,routeFactory
方法用于通过传入的页面名称从routerMap
获取到对应的路由配置方法,并传入所需参数进行调用。
Route<dynamic>? routeFactory(RouteSettings settings, String? uniqueId) {
FlutterBoostRouteFactory? func = routerMap[settings.name!];
if (func == null) {
return null;
}
return func(settings, uniqueId);
}
appBuilder方法的作用:
构建FlutterBoostApp还需要FlutterBoostAppBuilder类型的参数,其也是方法的别名。
typedef FlutterBoostAppBuilder = Widget Function(Widget home);
所以这里也实现一个appBuilder
方法,参数和返回值同以上类型保持相同,可以直接将此方法当参数传入使用。appBuilder
方法构建了一个MaterialApp
类型的Widget
,注意这里必须加上builder
参数,否则showDialog
等会出问题。
Widget appBuilder(Widget home) {
return MaterialApp(
home: home,
debugShowCheckedModeBanner: false, // 是否显示debug模式
/// 必须加上builder参数,否则showDialog等会出问题
builder: (_, __) {
// return const Homepage();
return home;
},
);
5: FlutterBoost基础配置完整代码如下:
class _MyAppState extends State<MyApp> {
// 配置页面路由
Map<String, FlutterBoostRouteFactory> routerMap {
'homepage': (settings, uniqueId) {
return CupertinoPageRoute( // CupertinoPageRoute:类似iOS页面的Push效果
settings: settings,
builder: (_) {
Map<String, dynamic>? map = settings.arguments as Map<String,dynamic>;
String data = map['data'] as String;
return Homepage(
data: data,
);
});
},
};
// routeFactory方法用于通过传入的页面名称从routeMap获取到对应的路由配置方法,并传入所需参数进行调用
Route<dynamic>? routeFactory(RouteSettings settings, String? uniqueId) {
FlutterBoostRouteFactory? func = routerMap[settings.name!];
if (func == null) {
return null;
}
return func(settings, uniqueId);
}
Widget appBuilder(Widget home) {
return MaterialApp(
home: home,
debugShowCheckedModeBanner: false,
/// 必须加上builder参数,否则showDialog等会出问题
builder: (_, __) {
// return const Homepage();
return home;
},
);
}
@override
// 在重写的build方法中,构建FlutterBoostApp,将routeFactory和appBuilder这两个方法作为参数传入。
Widget build(BuildContext context) {
return FlutterBoostApp(
routeFactory,
appBuilder: appBuilder,
);
}
}
三: 创建自定义的Flutter页面
1: 自定义Flutter的homepage页面,该页面用于原生页面跳转到Flutter页面。
在Flutter Module项目的lib
目录下,新建一个homepage.dart
文件,在文件中新建一个有状态的Homepage
类,并做简单布局。
在类中定义一个String
类型的常量data
,data
用于接收从原生页面传过来的参数。
在布局的Container容器的Text文本中,调用widget.data
, 实现数据的渲染。
具体代码如下所示:
import 'package:flutter/material.dart';
import 'package:flutter_boost/flutter_boost.dart';
class Homepage extends StatefulWidget{
final String? data;
const Homepage({Key? key, this.data}) : super(key: key);
@override
State<Homepage> createState() => _HomepageState();
}
class _HomepageState extends State<Homepage> {
@override
void initState() {
// TODO: implement initState
super.initState();
print("进入到Flutter页面");
}
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Container(
margin: const EdgeInsets.only(top: 120),
alignment: Alignment.center,
child: const Text("这是一个Flutter页面",
style: TextStyle(
fontSize: 22,
color: Colors.blue
),
),
),
SizedBox(height: 30,),
Container(
alignment: Alignment.center,
child: const Text("下面内容是原生传过来的值",
style: TextStyle(
fontSize: 18,
color: Colors.black
),
),
),
const SizedBox(height: 20,),
Container(
color: Colors.lime,
margin:const EdgeInsets.only(left: 60,right: 60),
child:Text(widget.data ?? "",
style:const TextStyle(
fontSize: 16,
color: Colors.black
),),
),
],
),
);
}
}
2: 自定义Flutter的simple_page页面,该页面用于Flutter页面跳转原生页面。
新建一个simple_page.dart
文件,在文件中新建一个有状态的SimplePage
类,并做简单布局。
在类中定义一个String类型的textStr
,textStr
用于将该字符串从Flutter页面传到原生页面。
在ElevatedButton的点击事件中,调用flutterBoost提供的路由进行跳转并传参:
定义要跳转到原生页面的路由名称为NewsVC
,在arguments
中传递要传入的参数,代码如下所示:
BoostNavigator.instance.push(
"NewsVC",
arguments: {"data": textStr},
);
SimplePage
类的具体代码如下所示:
class _SimplePageState extends State<SimplePage> with PageVisibilityObserver{
String textStr = "转朱阁,低绮户,照无眠。不应有恨,何事长向别时圆?人有悲欢离合,月有阴晴圆缺,此事古难全。但愿人长久,千里共婵娟。";
@override
void initState() {
// TODO: implement initState
super.initState();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.blue,
title: const Text(
"这是一个Flutter页面",
style: TextStyle(color: Colors.white),
),
leading: IconButton(
onPressed: () {
Map<String, Object> result = {'data': '返回页面时传参'};
BoostNavigator.instance.pop(result);
},
icon: const Icon(Icons.arrow_back_ios),
color: Colors.white,
),
),
body: Center(
child: Column(
children: [
Container(
color: Colors.lime,
margin: const EdgeInsets.only(left: 60, right: 60, top: 80),
child: Text(textStr,
style: const TextStyle(
fontSize: 16,
color: Colors.black,
)),
),
const SizedBox(
height: 30,
),
ElevatedButton(
onPressed: () {
BoostNavigator.instance.push(
"NewsVC",
arguments: {"data": textStr},
);
},
style: ButtonStyle(
backgroundColor: const MaterialStatePropertyAll(Colors.blue),
foregroundColor: const MaterialStatePropertyAll(Colors.white),
shape: MaterialStatePropertyAll(RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
)),
),
child: const Text(
"跳转到原生页面",
style: TextStyle(
fontSize: 18,
),
),
)
],
),
),
);
}
}
四: 配置iOS工程中的pod
1: 如果iOS工程未配置pod,需要先进入pod配置,配置过程可自行查找资料,这里不进行细诉。如果工程已经配置pod,则打开podfile文件,在文件中添加如下Flutter的相关配置。
target 'iOSDemo' do
use_frameworks!
# 配置flutter相关path
flutter_application_path = '../flutter-demo'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
# 集成flutter的PodFile target,执行如下命令
install_all_flutter_pods(flutter_application_path)
2: 在 Podfile 的 post_install 部分,调用 flutter_post_install(installer)
post_install do |installer|
flutter_post_install(installer) if defined?(flutter_post_install)
end
3: 最后在终端执行 pod install
五:在iOS原生项目中,配置FlutterBoost
1: 进行准备工作创建 BoostDelegate
单例类:
import UIKit
import flutter_boost
class BoostDelegate: NSObject, FlutterBoostDelegate {
/// 单例
public static let shared = BoostDelegate()
}
2: 实现 FlutterBoostDelegate 委托方法
FlutterBoostDelegate委托包括三个必须实现的方法:
- pushNativeRoute:如果框架发现您输入的路由表在flutter里面注册的路由表中找不到,那么就会调用此方法来push一个纯原生页面。
- pushFlutterRoute:当框架的withContainer为true的时候,会调用此方法来做原生的push。
- popRoute:当pop调用涉及到原生容器的时候,此方法将会被调用。
@protocol FlutterBoostDelegate <NSObject>
// 如果框架发现您输入的路由表在flutter里面注册的路由表中找不到,那么就会调用此方法来push一个纯原生页面
- (void) pushNativeRoute:(NSString *) pageName arguments:(NSDictionary *) arguments;
// 当框架的withContainer为true的时候,会调用此方法来做原生的push
- (void) pushFlutterRoute:(FlutterBoostRouteOptions *)options;
// 当pop调用涉及到原生容器的时候,此方法将会被调用
- (void) popRoute:(FlutterBoostRouteOptions *)options;
@end
3: BoostDelegate单例类的具体实现:
- 通过实现pushNativeRoute方法,实现从 Flutter 页面跳转到原生页面。
- 通过实现pushFlutterRoute方法,实现从原生页面跳转到 Flutter 页面。
- 通过实现popRoute方法,完成从Flutter页面返回到原生页面。
- 引入CustomFlutterController类,用于替换FBFlutterViewController,CustomFlutterController的作用后面会说明。
具体代码实现如下:
class BoostDelegate: NSObject, FlutterBoostDelegate {
/// 单例
public static let shared = BoostDelegate()
/// 用来 push 的导航栏
var navigationController:UINavigationController?
/// 用来存放 Flutter 页面返回原生页面时所执行的回调闭包
var resultTable:Dictionary<String,([AnyHashable:Any]?)->Void> = [:]
/// 从 Flutter 页面跳转到 iOS 原生页面
func pushNativeRoute(_ pageName: String!, arguments: [AnyHashable : Any]!) {
let isPresent = arguments["isPresent"] as? Bool ?? false
let isAnimated = arguments["isAnimated"] as? Bool ?? true
let targetViewController = openNativePage(pageName, arguments: arguments)
// 可以用参数来控制跳转方式
if (isPresent) {
self.navigationController?.present(targetViewController, animated: isAnimated, completion: nil)
} else {
self.navigationController?.pushViewController(targetViewController, animated: isAnimated)
}
}
/// 打开原生页面
/// 根据 pageName 来判断生成哪个 VC
private func openNativePage(_ pageName: String, arguments: [AnyHashable : Any]) -> UIViewController {
// 根据定义的路由名称跳转到相应的界面
if (pageName == "NewsVC") {
let newsVC = NewsViewController()
newsVC.arguments = arguments as NSDictionary
return newsVC
}
return UIViewController()
}
/// 从 iOS 原生页面跳转到 Flutter 页面
func pushFlutterRoute(_ options: FlutterBoostRouteOptions!) {
let vc = CustomFlutterController()
// 跳转时,是否隐藏原生导航
let isBarHidden = (options.arguments?["isBarHidden"] as? Bool) ?? false
vc.isBarHidden = isBarHidden
vc.configFlutter(name: options.pageName, uniqueId: options.uniqueId, params: options.arguments, opaque: options.opaque)
let isPresent = (options.arguments?["isPresent"] as? Bool) ?? false
let isAnimated = (options.arguments?["isAnimated"] as? Bool) ?? true
// 对这个页面设置结果
resultTable[options.pageName] = options.onPageFinished;
if (isPresent || !options.opaque) {
self.navigationController?.present(vc, animated: isAnimated, completion: nil)
} else {
self.navigationController?.pushViewController(vc, animated: isAnimated)
}
}
/// 退出页面
func popRoute(_ options: FlutterBoostRouteOptions!) {
if let vc = self.navigationController?.presentedViewController as? CustomFlutterController, vc.flutterVC.uniqueIDString() == options.uniqueId {// dismiss
if vc.modalPresentationStyle == .overFullScreen {// 手动调用
self.navigationController?.topViewController?.beginAppearanceTransition(true, animated: false)
vc.dismiss(animated: true) {
self.navigationController?.topViewController?.endAppearanceTransition()
}
} else {
vc.dismiss(animated: true, completion: nil)
}
} else {
/*
从Flutter页面返回到原生页面,如果Flutter页面并不是当前的顶级控制器,这时候就不能通过导航栏来返回到原生页面,而是应该移除掉容器Controller。
*/
guard let viewControllers = self.navigationController?.viewControllers else { return }
var containerToRemove: CustomFlutterController?
for item in viewControllers.reversed() {
if let container = item as? CustomFlutterController, container.flutterVC.uniqueIDString() == options.uniqueId {
containerToRemove = container
break
}
}
if (containerToRemove == nil) {
fatalError("uniqueId is wrong!!!")
}
if self.navigationController?.topViewController == containerToRemove {
self.navigationController?.popViewController(animated: true)
} else {
containerToRemove?.removeFromParent()
}
}
if let onPageFinshed = resultTable[options.pageName] {
onPageFinshed(options.arguments)
resultTable.removeValue(forKey: options.pageName)
}
}
}
4: 解决 Flutter 页面在销毁后内存不会被销毁的问题
Flutter 在内存方面最严重的两个点,一个是页面在销毁后内存不会销毁,另外一个是图片内存。关于页面销毁后内存不销毁的问题,主要原因在于引擎为了实现加载过的页面二次进入能达到秒加载,所以造成了内存不销毁。
FlutterBoost
没有解决单引擎的通病——页面销毁内存不销毁。在 iOS 端上的解决方案是通过原生Controller
,嵌入FBFlutterViewContainer.view
,继而由原生端的 Controller
实现 dealloc
时主动调用内存释放。
FBFlutterViewContainer
继承自FlutterViewController
,最终继承自ViewController
。
@interface FBFlutterViewContainer: FlutterViewController<FBFlutterContainer>\
@interface FlutterViewController: UIViewController
为了解决上面所说的问题,需要新建一个CustomFlutterController
类,由其作为FBFlutterViewContainer
的容器类,接管FBFlutterViewContainer
相关操作。我们将FBFlutterViewContainer
的View视图嵌入到这个新创建的类上,作为其子控制器而存在。
import UIKit
import flutter_boost
class CustomFlutterController: UIViewController {
lazy var flutterVC: FBFlutterViewContainer = FBFlutterViewContainer()
var isBarHidden : Bool?
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
func setupUI() {
addChild(flutterVC)
view.addSubview(flutterVC.view)
flutterVC.view.frame = view.bounds
BoostDelegate.shared.navigationController = self.navigationController
}
}
这样我们就可以由新创建的容器Controller实现在dealloc
时,主动调用FBFlutterViewContainer
的内存释放。
deinit {
flutterVC.removeFromParent()
}
为了能在BoostDelegate
中调用configFlutter
方法,需要在CustomFlutterController
中新增一个桥接方法。
func configFlutter(name: String, uniqueId: String?, params: [AnyHashable : Any]?, opaque: Bool) {
flutterVC.setName(name, uniqueId: uniqueId, params: params, opaque: opaque)
}
至止,BoostDelegate
单例类已配置完成。
六: 在AppDelegate中初始化FlutterBoost
在AppDelegate
的didFinishLaunchingWithOptions
方法中创建代理,做初始化操作
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let boostDelegate = BoostDelegate.shared;
FlutterBoost.instance().setup(application, delegate: boostDelegate) { engine in
print("")
}
return true
}
七: 创建iOS原生页面,初始化UI
1: 在控制器的viewDidLoad
方法中设置当前的导航控制器为BoostDelegate
用来push
的导航栏。
override func viewDidLoad() {
super.viewDidLoad()
BoostDelegate.shared.navigationController = self.navigationController;
}
2: 创建一个按钮,用于跳转到Flutter页面
let nativePushFlutterButton = UIButton()
nativePushFlutterButton.setTitle("原生页面跳转Flutter页面", for: UIControl.State.normal)
nativePushFlutterButton.setTitleColor(.white, for: UIControl.State.normal)
nativePushFlutterButton.titleLabel?.textAlignment = .center
nativePushFlutterButton.titleLabel?.numberOfLines = 0;
nativePushFlutterButton.backgroundColor = .systemOrange
nativePushFlutterButton.addTarget(self, action: #selector(onClickNativePushFlutterButton), for: UIControl.Event.touchUpInside)
self.view.addSubview(nativePushFlutterButton)
八:实现从iOS原生页面跳转到Flutter页面
在点击按钮跳转到Flutter页面之前,我们需要对路由参数进行相关配置。
@objc func onClickNativePushFlutterButton(){
// 路由参数配置
let options = FlutterBoostRouteOptions()
// 路由的名称
options.pageName = "homepage"
// 传递的参数
options.arguments = ["data" : textLabel.text as Any]
// 页面是否透明
options.opaque = true
options.completion = {completion in
print("打开Flutter页面的操作完成")
}
options.onPageFinished = { dict in
print("Flutter 页面关闭返回到原生页面时,参数值:\(String(describing: dict))")
}
// 执行open()会调用"BoostDelegate"中的"pushFlutterRoute"方法
FlutterBoost.instance().open(options)
}
九:实现从Flutter页面跳转到原生页面
flutter页面跳转到原生页面,在flutter_module
中的SimplePage
类中已经说明:
- 定义要跳转到原生的路由名称。
- arguments:传递到原生页面的参数。
BoostNavigator.instance.push(
"NewsVC",
arguments: {"data": textStr},
);
十:实现从Flutter页面返回到原生页面
调用flutter_boost提供的:BoostNavigator.instance.pop()
方法即可。
onpressed: () { Map<String, Object> result = {'data': '%E8%BF%94%E5%9B%9E%E9%A1%B5%E9%9D%A2%E6%97%B6%E4%BC%A0%E5%8F%82'}; BoostNavigator.instance.pop(result);}
参考文档:
flutter_boost:https://github.com/alibaba/flutter_boost
flutter_boost和iOS原生的通信:https://www.jianshu.com/p/086d0ad44261
Demo下载:
https://github.com/zhwIdea/iOSAndFlutterExample/tree/main
如果此文档对你有用,请点赞关注支持一下,谢谢。