原创:有趣知识点摸索型文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容
目录
- 一、FlutterBoost 的介绍
- 二、FlutterBoost 工程到目录结构
- 三、FlutterBoost 工程中 Dart 部分的配置代码
- 四、FlutterBoost 工程中 iOS 部分的配置代码
一、FlutterBoost 的介绍
随着 Flutter
的发展,国内越来越多的App开始使用 Flutter
。为了降低风险,大部分App采用渐进式方式引入 Flutter
,在App里选几个页面用 Flutter
来编写,但都碰到了相同的问题,在原生页面和 Flutter
页面共存的情况下,如何管理路由? 官方没有提供这样的解决方案,而FlutterBoost
就是为了解决这个问题而生。FlutterBoost
从开源后受到了社区开发者的欢迎,已经有很多App使用了 FlutterBoost
,社区开发者也很活跃,提了很多 issue
和 PR
。感谢开发者的一路支持和包容,无论是意见反馈还是吐槽,我们都会认真看,会持续关注 issue
。
1、FlutterBoost 的使命
FlutterBoost
的使命是让开发者非常简单的在原生App中开发 Flutter
页面。 FlutterBoost
作为 Flutter SDK
上层的解决方案,有一定的局限性,我们需要依赖 SDK
更多的开放能力。因此我们同时在做两件事情:
推动Flutter官方开放更多的底层接口
我们参与 Flutter 组织的的讨论。也多次发邮件给 Flutter
团队反馈 SDK
的 Bug 和一些无法支持的应用场景。 很欣慰的是在 Flutter 2.0 上看到混合开发的重大进展,Flutter 2.0 提供了 FlutterEngineGroup
。FlutterEngineGroup
创建一个新 Engine
,内存只增加180k,这个给我们提供了很多想象空间。但 FlutterEngineGroup
最大的问题是多 Engine
之间不是 isolate
(隔离) 层面的内存共享。 从目前看 FlutterBoost
这种单 Engine
内存共享的方式还不能被完全取代。
FlutterBoost 的升级
虽然开源社区很活跃,Star
很多,使用者也很多,但 FlutterBoost
离优秀的开源项目还很远。
2、FlutterBoost 的问题
- 社区的
issue
没有收敛的趋势。 - 设计过于复杂,概念太多。这让一个新手看
FlutterBoost
的代码很吃力。 - 稳定性。每次
Flutter
发布一个Stable
版本,开发者都会问,FlutterBoost
针对新版本适配了没有?他们准备升级新版本,需要FlutterBoost
能适配最新版本,所以每次都要针对新版本拉2个新分支(Androidx
和Support
分支)进行适配。 时间长了,会产生很多分支,这个给分支管理带来很大的成本,比如在某个分支上修复的issue
要同步到其他分支,一不小心就会遗漏同步。
针对上面的问题,我们做了几个事项:
- 不侵入引擎
- 不区分
Androidx
和Support
分支,兼容Flutter
的各种版本。Flutter SDK
的升级不需要再升级FlutterBoost
,极大降低升级成本 - 简化架构和接口,和
FlutterBoost2.0
比,代码减少了一半 - 双端统一,包括接口和设计上的统一。很多
Flutter
开发者只会一端,只会Android
或者只会 iOS,但他需要接入双端,所以双端统一能降低他的学习成本和接入成本 - 支持打开
Flutter
页面,不再打开容器场景。在很多场景下,Flutter
页面跳转Flutter
页面,这个时候可以不需要再打开容器。不打开容器,能节省内存开销。 最新版本上,打开容器和不打开容器的区别表现在用户接口上仅仅是withContainer
参数是否为true
就好。
3、FlutterBoost 的架构
FlutterBoost
插件分为平台和 Dart
两端,中间通过 Message Channel
连接。
平台侧:
- 提供了
Flutter
引擎的配置和管理 -
Native
容器的创建/销毁 - 页面可见性变化通知
- 以及
Flutter
页面的打开/关闭接口
Dart 侧:
- 提供类似原生
Navigator
的页面导航接口的能力 - 负责
Flutter
页面的路由管理
FlutterBoost
是采用单Engine
的方案,所以整个 App 是在同一个 isolate
下,内存共享,而FlutterEngineGroup
是采用多 Engine
方案,每个页面是一个 Engine
,或者一个页面内包含多个 Engine
,每个Engine
对应一个 isolate
,内存不共享。 从 FlutterEngineGroup
生成的FlutterEngine
,内存只增加180k。因为它对常用资源进行共享(例如 GPU 上下文、字体度量和隔离线程的快照),所以会加快首次渲染的速度、降低延迟并降低内存占用。
那是不是有了 FlutterEngineGroup
就不需要 FlutterBoost
了?从目前看 FlutterBoost
这种单Engine
的方案,有一定的合理性,还不能完全被替代。
二、FlutterBoost 工程到目录结构
我们新建一个文件夹 FlutterBoostExample
,这个文件夹下面放置另外三个文件夹。 另外三个分别是您的 Android
工程,iOS工程,以及需要接入的 flutter module
, 这个地方注意,flutter
一定是 module
,而不是工程项目,判断是不是 module
的方法就是看其是否有 android
和ios 文件夹, 如果没有,那就是 module
。
在这里我们命名为 BoostTestAndroid
,BoostTestIOS
以及 flutter_module
。注意这三个工程在同级目录下。BoostTestIOS
工程可以参考我们刚才上面介绍的步骤重新创建一个。这里需要通过 Android Studio
创建一个新的 BoostTestAndroid
工程。
三、FlutterBoost 工程中 Dart 部分的配置代码
1、配置依赖库文件
首先,需要添加 FlutterBoost
依赖到 yaml
文件,之后在 flutter
工程下运行 flutter pub get dart
端就集成完毕了。这个我们在之前的步骤中已经做过了。
flutter_boost:
git:
url: 'https://github.com/alibaba/flutter_boost.git'
ref: '4.2.0'
2、创建自定义的 Binding 接管 Flutter App 的生命周期
这里要特别注意,如果你的工程里已经有一个继承自 WidgetsFlutterBinding
的自定义 Binding
,则只需要将其 with
上 BoostFlutterBinding
。如果你的工程没有自定义的 Binding
,则可以参考这个CustomFlutterBinding
的做法。创建一个自定义的 Binding
,继承和with
的关系如下,里面什么都不用写。BoostFlutterBinding
用于接管 Flutter App
的生命周期,必须得接入的。
class CustomFlutterBinding extends WidgetsFlutterBinding with BoostFlutterBinding {}
在 main.dart
文件中的 main
入口方法中需要创建 CustomFlutterBinding
对象。这里的CustomFlutterBinding
调用务必不可缺少,用于控制Boost
状态的resume
和pause
。
void main() {
CustomFlutterBinding();
runApp(MyApp());
}
创建一个StatefulWidget
模版。
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
3、routerMap 变量的作用
在 _MyAppState
类中声明一个 routerMap
变量,对每一个页面的路由方式进行配置。如果想用类似iOS平台的跳转动画,那么只需要像下面这样写成 [CupertinoPageRoute]
即可。如果需要push
的时候,两个页面都需要动的话,就是像 iOS native
那样,在push
的时候,前面一个页面也会向左推一段距离。那么前后两个页面都必须是遵循CupertinoRouteTransitionMixin
的路由。简单来说,就两个页面都是CupertinoPageRoute
就好,如果用MaterialPageRoute
的话同理,MaterialPageRoute
是类似安卓平台从下往上滑出的跳转动画。
Map<String, FlutterBoostRouteFactory> routerMap = {
'mainPage': (settings, uniqueId) {
return CupertinoPageRoute(
settings: settings,
builder: (_) {
Map<String, dynamic>? map = settings.arguments as Map<String, dynamic>;
String? data = map['data'];
return MainPage(
data: data,
);
});
},
'simplePage': (settings, uniqueId) {
return CupertinoPageRoute(
settings: settings,
builder: (_) {
Map<String, dynamic>? map = settings.arguments as Map<String, dynamic>;
String? data = map['data'];
return SimplePage(
data: data,
);
});
},
};
这里的routerMap
中的String
是页面的名称,而FlutterBoostRouteFactory
是一个方法的重命名。这个方法的传入参数是路由的设置和uniqueId
,返回值是路有Route
类。
typedef FlutterBoostRouteFactory = Route<dynamic>? Function(
RouteSettings settings, String? uniqueId);
在routerMap
中我们看到的mainPage
字符串右边的模块其实就是该方法的具体实现,包括参数、函数体,返回的是一个路由CupertinoPageRoute
,其为Route
的子类。
uniqueId
其实在这里并不会使用到,我们真正关心的是settings
中的参数arguments
,其也为一个Map
类型。我们可以从中获取要打开的新页面所必需要使用的一些参数值,并传入。
以mainPage
为例,如下所示,其中MainPage
页面是我们自定义的一个页面。
'mainPage': (settings, uniqueId) {
return CupertinoPageRoute(
settings: settings,
builder: (_) {
Map<String, dynamic>? map = settings.arguments as Map<String, dynamic>;
String? data = map['data'];
return MainPage(
data: data,
);
});
},
4、routeFactory 方法的作用
要在build
方法中创建FlutterBoostApp
这样一个Widget
,那么就需要传入一个FlutterBoostRouteFactory
类型的参数。
@override
Widget build(BuildContext context) {
return FlutterBoostApp(
routeFactory,
appBuilder: appBuilder,
);
}
因为其初始化方法中需要这样一个入参,而FlutterBoostRouteFactory
类型正是刚才我们所说的重命名的函数类型,其可以通过在我们刚才生成的routerMap
中通过页面名称进行获取。
class FlutterBoostApp extends StatefulWidget {
FlutterBoostApp(
FlutterBoostRouteFactory routeFactory, {
Key? key,
FlutterBoostAppBuilder? 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);
}
5、appBuilder 方法的作用
构建FlutterBoostApp
还需要FlutterBoostAppBuilder
类型的参数,其也是方法的别名。
typedef FlutterBoostAppBuilder = Widget Function(Widget home);
所以这里也实现一个appBuilder
方法,参数和返回值同以上类型保持相同,可以直接将此方法当参数传入使用。appBuilder
方法构建了一个MaterialApp
类型的Widget
,注意这里必须加上builder
参数,否则showDialog
等会出问题。
Widget appBuilder(Widget home) {
return MaterialApp(
home: home,
debugShowCheckedModeBanner: true,
builder: (_, __) {
return home;
},
);
}
在重写的build
方法中,构建FlutterBoostApp
,将routeFactory
和appBuilder
这两个方法作为参数传入。
@override
Widget build(BuildContext context) {
return FlutterBoostApp(
routeFactory,
appBuilder: appBuilder,
);
}
6、创建自定义的 Flutter 页面
接着就是绘制我们的UI部分了。下面的代码创建了一个 SimplePage
页面。
class SimplePage extends StatelessWidget {
const SimplePage({Object? data});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: () {
BoostNavigator.instance.pop();
},
icon: Icon(Icons.arrow_back_ios_new)),
title: const Text("商品详情"),
),
body: Center(child: Text('寒蝉凄切,对长亭晚,骤雨初歇。都门帐饮无绪,留恋处,兰舟催发。执手相看泪眼,竟无语凝噎。')),
);
}
}
创建一个新的 main_page.dart
文件。建立一个命名为MainPage
的 Widget
。
class MainPage extends StatefulWidget {
final String? data;
const MainPage({Key? key, this.data}) : super(key: key);
@override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
分别实现其 initState
方法。
@override
void initState() {
super.initState();
print("进入到 Flutter 的商品列表页面");
}
还有 build
方法。
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: () {
BoostNavigator.instance.pop();
},
icon: Icon(Icons.arrow_back_ios_new)),
title: const Text("商品列表"),
),
body: Container(
color: Colors.grey.withOpacity(0.1),
width: double.infinity,
height: double.infinity,
child: Center(
child: Text(widget.data ?? ""),
),
),
);
}
四、FlutterBoost 工程中 iOS 部分的配置代码
1、配置 Podfile 文件
首先到自己的 iOS 目录下,打开Podfile
文件,添加以下代码。这段代码我们之前添加过了。
flutter_application_path = '../flutter_module'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
install_all_flutter_pods(flutter_application_path)
添加之后,您的Podfile
应该类似下面这样。然后再执行pod install
安装完成。
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
flutter_application_path = '../flutter_module'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
target 'BoostTestIOS' do
use_frameworks!
install_all_flutter_pods(flutter_application_path)
end
2、实现 FlutterBoostDelegate 委托方法
进行准备工作创建FlutterBoostDelegate
。 这里面的内容是完全可以自定义的,在您了解各个API
的含义时,你可以完全自定义这里面每个方法的代码,下面只是给出大多数场景的默认解法。
import UIKit
import flutter_boost
class BoostDelegate: NSObject, FlutterBoostDelegate {
/// 单例
public static let shared = BoostDelegate()
}
FlutterBoostDelegate
委托包括三个方法必须实现的方法:
-
pushNativeRoute:如果框架发现您输入的路由表在
flutter
里面注册的路由表中找不到,那么就会调用此方法来push
一个纯原生页面。 -
pushFlutterRoute:当框架的
withContainer
为true
的时候,会调用此方法来做原生的push
-
popRoute:当
pop
调用涉及到原生容器的时候,此方法将会被调用
@protocol FlutterBoostDelegate <NSObject>
- (void) pushNativeRoute:(NSString *) pageName arguments:(NSDictionary *) arguments;
- (void) pushFlutterRoute:(FlutterBoostRouteOptions *)options;
- (void) popRoute:(FlutterBoostRouteOptions *)options;
@end
声明属性navigationController
表示用来push
的导航栏。
var navigationController:UINavigationController?
用来存放 Flutter
页面返回原生页面时所执行的回调闭包。
var resultTable:Dictionary<String,([AnyHashable:Any]?)->Void> = [:]
3、从 Flutter 页面跳转到 iOS 原生页面
实现从 Flutter
页面跳转到 iOS 原生页面的方法 pushNativeRoute
。这里根据pageName
来判断生成哪个vc
。可以用参数来控制是否展示跳转动画以及进入下一个页面的方式。进入下一个页面的方式可以是 push
方式或者present
方式。
func pushNativeRoute(_ pageName: String!, arguments: [AnyHashable : Any]!) {
let isPresent = arguments["isPresent"] as? Bool ?? false
let isAnimated = arguments["isAnimated"] as? Bool ?? true
var targetViewController = UIViewController()
if (isPresent) {
self.navigationController?.present(targetViewController, animated: isAnimated, completion: nil)
} else {
self.navigationController?.pushViewController(targetViewController, animated: isAnimated)
}
}
上面暂时给了个默认的vc
,假如我们知道了某个vc
的名称的话就可以像下面这样接收传入的参数并打开这个页面。
if (pageName == "homePage") {
let data: String = arguments?["data"] as? String ?? ""
let homeVC = HomeViewController()
homeVC.dataString = data
targetViewController = homeVC
}
4、从 iOS 原生页面跳转到 Flutter 页面
实现从 iOS 原生页面跳转到 Flutter
页面的方法 pushFlutterRoute
。这里同上,也可以使用参数来控制是否展示跳转动画以及进入下一个页面的方式。如果是 present
模式或者透明模式,那么就需要以 present
模式打开新页面。
func pushFlutterRoute(_ options: FlutterBoostRouteOptions!) {
let vc: FBFlutterViewContainer = FBFlutterViewContainer()
vc.setName(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)
}
}
FlutterBoostRouteOptions
类表示的是路有参数配置,包含以下属性。
@interface FlutterBoostRouteOptions : NSObject
/// 页面在路由表中的名字
@property(nonatomic, strong) NSString* pageName;
/// 参数
@property(nonatomic, strong) NSDictionary* arguments;
/// 参数回传的回调闭包,仅在原生->flutter页面的时候有用
@property(nonatomic, strong) void(^onPageFinished)(NSDictionary*);
/// open方法完成后的回调,仅在原生->flutter页面的时候有用
@property(nonatomic, strong) void(^completion)(BOOL);
/// 代理内部会使用,原生往flutter open的时候此参数设为nil即可
@property(nonatomic, strong) NSString* uniqueId;
/// 这个页面是否透明 注意:default value = YES
@property(nonatomic,assign) BOOL opaque;
@end
5、从 Flutter 页面跳转回原生页面
实现关闭 Flutter
页面的popRoute
方法。
func popRoute(_ options: FlutterBoostRouteOptions!) {
}
如果当前被 present
的 vc
是 container
,那么就执行 dismiss
逻辑,否则直接执行pop
逻辑。这里分为两种情况,由于UIModalPresentationOverFullScreen
下,生命周期显示会有问题,所以需要手动调用的场景,从而使下面底部的vc
调用viewAppear
相关逻辑。这里手动调用 beginAppearanceTransition
触发页面生命周期。如果是正常场景的话直接 dismiss
即可。
if let vc = self.navigationController?.presentedViewController as? FBFlutterViewContainer, vc.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 {// pop
self.navigationController?.popViewController(animated: true)
}
从结果列表中取出onPageFinshed
回调。onPageFinished
是参数回传的回调闭包,从原生页面跳转 到flutter
页面,从 flutter
页面跳转回来之后,会回调 onPageFinished
,所以此处为其传入flutter
页面想要带给原生页面的参数,最后将已经 pop
的页面从结果表中移除。
if let onPageFinshed = resultTable[options.pageName] {
onPageFinshed(options.arguments)
resultTable.removeValue(forKey: options.pageName)
}
6、解决 Flutter 页面在销毁后内存不会被销毁的问题
Flutter
在内存方面最严重的两个点:一个是页面在销毁后内存不会销毁,另外一个是图片内存。关于页面销毁后内存不销毁的问题,主要原因在于引擎为了实现加载过的页面二次进入能达到秒加载,所以造成了内存不销毁。图片内存主要在于原生端如果加载过A
图片,Flutter
端如果也需要加载A
图片,默认情况下不会从原生端处获取图片缓存,而是在 Flutter
端产生一份新的图片缓存。
FlutterBoost
没有解决单引擎的通病——页面销毁内存不销毁。在翻看源码时发现页面销毁触发 dealloc
之后没有主动调用内存释放。在 iOS 端上的解决方案是通过原生 Controller
, 嵌入 FBFlutterViewContainer.view
, 继而由原生端的 Controller
实现 dealloc
时主动调用内存释放 API
。
FBFlutterViewContainer
继承自FlutterViewController
,最终继承自 ViewController
。
@interface FBFlutterViewContainer : FlutterViewController<FBFlutterContainer>
@interface FlutterViewController: UIViewController
为了解决上面所说的问题,我们新建了一个 CustomFlutterController
类,由其作为FBFlutterViewContainer
的容器类,接管FBFlutterViewContainer
相关操作。我们将FBFlutterViewContainer
的View
视图嵌入到这个新创建的类上,作为其子控制器而存在。
class CustomFlutterController: UIViewController {
lazy var flutterVC: FBFlutterViewContainer = FBFlutterViewContainer()
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
内存释放 API
。
deinit {
flutterVC.removeFromParent()
}
既然现在由CustomFlutterController
来接管FBFlutterViewContainer
,那么在 BoostDelegate
的 pushFlutterRoute
方法中便不能再使用FBFlutterViewContainer
了,而应该换成CustomFlutterController
。
let vc: FBFlutterViewContainer = FBFlutterViewContainer()
vc.setName(options.pageName, uniqueId: options.uniqueId, params: options.arguments, opaque: options.opaque)
将以上的代码修改为:
let vc = CustomFlutterController()
vc.configFlutter(name: options.pageName, uniqueId: options.uniqueId, params: options.arguments, opaque: options.opaque)
为了仍然能够调用 setName
方法,所以在 CustomFlutterController
中增加了一个桥接方法。
func configFlutter(name: String, uniqueId: String?, params: [AnyHashable : Any]?, opaque: Bool) {
flutterVC.setName(name, uniqueId: uniqueId, params: params, opaque: opaque)
}
同理,在popRoute
方法中也需要替换掉FBFlutterViewContainer
。
if let vc = self.navigationController?.presentedViewController as? FBFlutterViewContainer, vc.uniqueIDString() == options.uniqueId
修改为:
if let vc = self.navigationController?.presentedViewController as? CustomFlutterController, vc.flutterVC.uniqueIDString() == options.uniqueId
但是在popRoute
方法中pop
场景下要从Flutter
页面返回到原生页面的时候,遇到了一个问题。我们知道从原生页面进入到Flutter
页面的时候会带有一个我们为FBFlutterViewContainer
增加的容器Controller
。但是倘若点击返回按钮的时候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()
}
7、初始化 FlutterBoost
在AppDelegate
的didFinishLaunchingWithOptions
方法中创建代理,做初始化操作。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let delegate = BoostDelegate.shared
FlutterBoost.instance().setup(application, delegate: delegate) { engine in
print("")
}
return true
}
Demo 演示
来到ViewController
中,在viewDidLoad
方法中设置当前的导航控制器为BoostDelegate
用来push
的导航栏。
override func viewDidLoad() {
super.viewDidLoad()
BoostDelegate.shared.navigationController = self.navigationController
}
为什么要在这里设置self.navigationController
呢?APP
刚启动的时候进入到ViewController
这个页面,需要给BoostDelegate.shared.navigationController
一个初始值,否则点击“跳转到 Flutter” 按钮无法进行跳转。
因为当我们点击跳转按钮的时候会进入到pushFlutterRoute
方法之中来,在该方法中执行到navigationController?.pushViewController
的时候发现navigationController
为nil
,所以无法进行跳转到Flutter
页面,也就不会执行CustomFlutterController
页面的setupUI
方法了。
func pushFlutterRoute(_ options: FlutterBoostRouteOptions!) {
...
if(isPresent || !options.opaque){
self.navigationController?.present(vc, animated: isAnimated, completion: nil)
} else {
self.navigationController?.pushViewController(vc, animated: isAnimated)
}
}
我们注意到之前在
我们搭建一个基本的UI,提供一个跳转到 Flutter
页面的按钮。
在点击按钮跳转到 Flutter
页面之前,我们需要对路由参数进行配置,包括要打开哪一个页面,其所需的参数值,以及实现 Flutter
页面打开完成的回调和 Flutter
页面关闭返回到原生页面时的回调。
@IBAction func gotoFlutterPage(_ sender: UIButton) {
let options = FlutterBoostRouteOptions()
options.pageName = "mainPage"
options.arguments = ["data": "嗟乎!时运不齐,命途多舛。冯唐易老,李广难封。屈贾谊于长沙,非无圣主;窜梁鸿于海曲,岂乏明时?所赖君子见机,达人知命。老当益壮,宁移白首之心?穷且益坚,不坠青云之志。酌贪泉而觉爽,处涸辙以犹欢。北海虽赊,扶摇可接;东隅已逝,桑榆非晚。孟尝高洁,空余报国之情;阮籍猖狂,岂效穷途之哭!"]
options.opaque = true
options.completion = { completion in
print("打开 Flutter 页面的操作完成")
}
options.onPageFinished = { dict in
print("Flutter 页面关闭返回到原生页面时,参数值:\(String(describing: dict))")
}
FlutterBoost.instance().open(options)
}
当我们在 Android Studio
中修改完成 Dart
侧的代码后,需要在终端对 iOS 原生工程执行 pod install
命令后才能运行 iOS 原生工程,否则修改不会生效。运行起来看下效果。
可以看到成功实现了 Flutter
与 iOS
原生页面之间的跳转。
8、navigationController 的问题
我们将项目UI变成了下图所示。
在 Flutter
页面点击文本的时候会跳转到原生页面。
child: InkWell (
child: Text(widget.data ?? ""),
onTap: () {
BoostNavigator.instance.push("Article");
},
),
这里跳转到了新创建的一个原生页面。
func pushNativeRoute(_ pageName: String!, arguments: [AnyHashable : Any]!) {
let isPresent = arguments["isPresent"] as? Bool ?? false
let isAnimated = arguments["isAnimated"] as? Bool ?? true
var targetViewController = UIViewController()
targetViewController.title = "星期五放假"
targetViewController.view.backgroundColor = .white
if (isPresent) {
self.navigationController?.present(targetViewController, animated: isAnimated, completion: nil)
} else {
self.navigationController?.pushViewController(targetViewController, animated: isAnimated)
}
}
我们发现要想让跳转正常运行,必须在进入到原生页面之前更新其navigationController
的值,让BoostDelegate.shared.navigationController
的viewcontrollers
值同self.navigationController
的值保持一致,二者都是最新值。之前我们是在viewDidLoad
中对其进行赋值的,其只会被赋值一次,并不是最新,所以我们将其赋值放到了viewWillAppear
中,当然也可以放到tabbar
切换controller
的时候更新其值。
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
BoostDelegate.shared.navigationController = self.navigationController
}
我们也发现在进入到Flutter
页面之前也需要保持BoostDelegate.shared.navigationController
的viewcontrollers
值是最新的,否则在点击Flutter
页面的文本从Flutter
页面跳转回到原生页面的时候,也会出现异常,为了保证在进入到Flutter
页面的时候该值最新,所以我们在CustomFlutterController
的setupUI
方法中对其进行了赋值。
func setupUI() {
addChild(flutterVC)
view.addSubview(flutterVC.view)
flutterVC.view.frame = view.bounds
BoostDelegate.shared.navigationController = self.navigationController
}