Flutter:通过 Flutter Boost 实现 Flutter 页面与原生页面之间的跳转

原创:有趣知识点摸索型文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、FlutterBoost 的介绍
  • 二、FlutterBoost 工程到目录结构
  • 三、FlutterBoost 工程中 Dart 部分的配置代码
  • 四、FlutterBoost 工程中 iOS 部分的配置代码

点击下载 Demo


一、FlutterBoost 的介绍

随着 Flutter 的发展,国内越来越多的App开始使用 Flutter。为了降低风险,大部分App采用渐进式方式引入 Flutter,在App里选几个页面用 Flutter 来编写,但都碰到了相同的问题,在原生页面和 Flutter 页面共存的情况下,如何管理路由? 官方没有提供这样的解决方案,而FlutterBoost 就是为了解决这个问题而生。FlutterBoost 从开源后受到了社区开发者的欢迎,已经有很多App使用了 FlutterBoost,社区开发者也很活跃,提了很多 issuePR。感谢开发者的一路支持和包容,无论是意见反馈还是吐槽,我们都会认真看,会持续关注 issue

1、FlutterBoost 的使命

FlutterBoost 的使命是让开发者非常简单的在原生App中开发 Flutter 页面。 FlutterBoost 作为 Flutter SDK 上层的解决方案,有一定的局限性,我们需要依赖 SDK 更多的开放能力。因此我们同时在做两件事情:

推动Flutter官方开放更多的底层接口

我们参与 Flutter 组织的的讨论。也多次发邮件给 Flutter 团队反馈 SDK 的 Bug 和一些无法支持的应用场景。 很欣慰的是在 Flutter 2.0 上看到混合开发的重大进展,Flutter 2.0 提供了 FlutterEngineGroupFlutterEngineGroup 创建一个新 Engine,内存只增加180k,这个给我们提供了很多想象空间。但 FlutterEngineGroup 最大的问题是多 Engine 之间不是 isolate(隔离) 层面的内存共享。 从目前看 FlutterBoost 这种单 Engine 内存共享的方式还不能被完全取代。

FlutterBoost 的升级

虽然开源社区很活跃,Star 很多,使用者也很多,但 FlutterBoost 离优秀的开源项目还很远。


2、FlutterBoost 的问题

  • 社区的 issue没有收敛的趋势。
  • 设计过于复杂,概念太多。这让一个新手看 FlutterBoost 的代码很吃力。
  • 稳定性。每次 Flutter 发布一个 Stable版本,开发者都会问,FlutterBoost 针对新版本适配了没有?他们准备升级新版本,需要 FlutterBoost 能适配最新版本,所以每次都要针对新版本拉2个新分支(AndroidxSupport 分支)进行适配。 时间长了,会产生很多分支,这个给分支管理带来很大的成本,比如在某个分支上修复的 issue 要同步到其他分支,一不小心就会遗漏同步。

针对上面的问题,我们做了几个事项:

  • 不侵入引擎
  • 不区分 AndroidxSupport 分支,兼容 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

在这里我们命名为 BoostTestAndroidBoostTestIOS 以及 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,则只需要将其 withBoostFlutterBinding。如果你的工程没有自定义的 Binding,则可以参考这个CustomFlutterBinding 的做法。创建一个自定义的 Binding,继承和with的关系如下,里面什么都不用写。BoostFlutterBinding 用于接管 Flutter App 的生命周期,必须得接入的。

class CustomFlutterBinding extends WidgetsFlutterBinding with BoostFlutterBinding {}

main.dart 文件中的 main 入口方法中需要创建 CustomFlutterBinding 对象。这里的CustomFlutterBinding 调用务必不可缺少,用于控制Boost状态的resumepause

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,将routeFactoryappBuilder这两个方法作为参数传入。

@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 文件。建立一个命名为MainPageWidget

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:当框架的withContainertrue的时候,会调用此方法来做原生的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!) {
    
}

如果当前被 presentvccontainer,那么就执行 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相关操作。我们将FBFlutterViewContainerView视图嵌入到这个新创建的类上,作为其子控制器而存在。

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,那么在 BoostDelegatepushFlutterRoute 方法中便不能再使用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

AppDelegatedidFinishLaunchingWithOptions方法中创建代理,做初始化操作。

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的时候发现navigationControllernil,所以无法进行跳转到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 原生工程,否则修改不会生效。运行起来看下效果。

可以看到成功实现了 FlutteriOS原生页面之间的跳转。


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.navigationControllerviewcontrollers值同self.navigationController的值保持一致,二者都是最新值。之前我们是在viewDidLoad中对其进行赋值的,其只会被赋值一次,并不是最新,所以我们将其赋值放到了viewWillAppear中,当然也可以放到tabbar切换controller的时候更新其值。

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    BoostDelegate.shared.navigationController = self.navigationController
}

我们也发现在进入到Flutter页面之前也需要保持BoostDelegate.shared.navigationControllerviewcontrollers值是最新的,否则在点击Flutter页面的文本从Flutter页面跳转回到原生页面的时候,也会出现异常,为了保证在进入到Flutter页面的时候该值最新,所以我们在CustomFlutterControllersetupUI方法中对其进行了赋值。

func setupUI() {
    addChild(flutterVC)
    view.addSubview(flutterVC.view)
    flutterVC.view.frame = view.bounds
    
    BoostDelegate.shared.navigationController = self.navigationController
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容