iOS原生混编Flutter路由指南及解决Flutter首页闪白屏问题

前言

公司iOS项目自从20年从原生引入了Flutter以来,生产力来说不可谓提升不大,毕竟1个人就可以干两端,其他端的适配只需要简单的适配即可。从Flutter1.22.6开始一直适配到现在的2.10.5,期间大大小小产生的坑也不少。Flutter混编的路由方案我们采用的是阿里的flutter_boost方案,最近项目也是登录模块用Flutter进行了重构,和原先只在二级页面使用相比,应用冷启动就进入Flutter页面其实十分有挑战,毕竟引擎的启动要时间。这不,当你的启动图和登录界面使用了特殊的背景图就会有短暂的闪白屏的效果,如下,其实就是Flutter引擎还没渲染完毕的真空时间。如是就有了下面的解决方案。

20220729_104447.GIF

iOS接入Flutter及解决首页闪白屏全过程

一、创建flutter module项目

我们通过xcode新建一个demo ios项目,然后在项目目录下创建flutter module项目

//创建flutter module项目
flutter create -t module flutter_module

//pubspec文件引入flutter_boost,我这里采用本地引入方式

flutter_boost:
    path: flutter_boost-3.0-null-safety-release.2.1

初始化ios项目 Pod


pod init

Podfile配置代码如下


flutter_application_path = './flutter_module'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'NaviteMixinFlutterDemo' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!
  install_all_flutter_pods(flutter_application_path)
  # Pods for NaviteMixinFlutterDemo

end


然后pod install。这样子我们的混编flutter项目就构建完成了


pod install

二、iOS原生注册flutter引擎及实现flutter_boost路由

注册Flutter引擎,我们只需使用flutter_boost的方式在原生appDelegate注册即可。


func registerFlutter(application: UIApplication) {
        FlutterBoost.instance().setup(application, delegate: JFFlutterRoute.shareInstance) { engine in
            guard let _ = engine else { return }
            print("engine success")
            //you can register your channel in there.
        }
    }

实现flutter_boost路由跳转协议deleagte。获取顶层vc的代码,我往demo项目写了个扩展,这里不再赘述。


extension JFFlutterRoute: FlutterBoostDelegate {
    internal func pushNativeRoute(_ pageName: String!, arguments: [AnyHashable : Any]!) {
        switch pageName {
        case JFFluterRouteName.nativeMainPage:
            let vc = ViewController()
            let navi = JFNavigationViewController(rootViewController: vc)
            AppDelegate.switchRootVC(vc: navi)
            break
        case JFFluterRouteName.nativePage:
            let vc = ViewController()
            vc.title = "原生二级页面"
            UIApplication.shared.visibleNavigationController()?.pushViewController(vc, animated: true)
            break
        default:
            break
        }
    }
    
    internal func pushFlutterRoute(_ options: FlutterBoostRouteOptions!) {
        guard let vc = JFFlutterViewController() else { return }
        vc.setName(options.pageName, uniqueId: options.uniqueId, params: options.arguments, opaque: options.opaque)
        UIApplication.shared.visibleNavigationController()?.pushViewController(vc, animated: true)
    }
    
    internal func popRoute(_ options: FlutterBoostRouteOptions!) {
        //只演示push,pop.  present dismiss的处理 自己处理
        UIApplication.shared.visibleNavigationController()?.popViewController(animated: true)
    }
}

三、flutter端注册路由表


//MyApp Widget中注册

static Map<String, FlutterBoostRouteFactory> pageMap = {
    JFRoute.loginPage: (settings, uniqueId) {
      return CupertinoPageRoute(
        settings: settings,
        builder: (_) => const LoginPage(
        ),
      );
    },
    JFRoute.demoPage: (settings, uniqueId) {
      return CupertinoPageRoute(
        settings: settings,
        builder: (_) => const DemoPage(
        ),
      );
    },
  };

  Route<dynamic>? routeFactory(RouteSettings settings, String? uniqueId) {
    FlutterBoostRouteFactory? func = pageMap[settings.name!];
    if (func == null) {
      return null;
    }
    return func(settings, uniqueId ?? "");
  }


  Widget appBuilder(Widget home) {
    return MaterialApp(
      home: home,
      debugShowCheckedModeBanner: true,
      ///必须加上builder参数,否则showDialog等会出问题
      builder: (_, __) {
        return home;
      },
    );
  }

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return FlutterBoostApp(
        routeFactory,
        appBuilder: appBuilder,
      );
  }
  
 //简单封装一个路由类
 
 class JFRoute {
  
  static var loginPage = "jf://loginPage";
  static var nativeMainPage = "jf://nativeMainPage";
  static var nativePage = "jf://nativePage";
  static var demoPage = "jf://demoPage";

  static pushRoute(String url,
      {Map<String, dynamic>? urlParams,
      bool opque = true,
      bool withContainer = true,}) {
    withContainer = true;
    BoostNavigator.instance.push(
        url, //required
        withContainer: withContainer, //optional
        arguments: urlParams, //optional
        opaque: opque, //optional,default value is true
      );
  }

  static popRoute() {
    BoostNavigator.instance.pop();
  }
}

至此我们只要把启动根控制器换成flutter登录页面,我们一启动就会显示一个Flutter登录页面.


guard let scene = (scene as? UIWindowScene) else { return }
        self.window = UIWindow(windowScene: scene)
        let vc = JFLoginFluterViewController()
        vc.setName(JFFluterRouteName.loginPage, uniqueId: "", params: [:], opaque: true)
        self.window?.rootViewController = JFNavigationViewController(rootViewController: vc)
        self.window?.makeKeyAndVisible()

四、解决Flutter首页闪白屏问题

分析原因:由于引擎的启动要时间,当启动图和登录页面都用了同一个背景图时,会有一个白屏的闪缩大概(0.5-1s)左右,机型越好速度越快。那么我们要如何解决这个问题?

  • 方案一

我们可以用一个原生的vc带一个背景图,然后flutter vc当做child vc。 这种做法是可行的,但是每次修改都得做一次相同的操作,而且不灵活。 不太推荐。

  • 方案二

也是我目前实现的一个方案,由于UIColor自带一个api可以通过图片来渲染出一种特殊的颜色,我们只需要在flutter基类,判断需要修改背景色的路由,做一次UIImage渲染成颜色的动作即可。当然由于图片会拉伸,我们直接new一个image是不行的,我么需要用UIImageView来承载图片,让它自动撑满,再对图片截图然后缓存起来,这样渲染出来的图片就可以启动图一摸一样了。
代码如下,以及最终效果。

20220729_104243.GIF

 private func tryChangeLoginBgColorIfNeed() {
        guard JFFlutterRoute.needBgViewRoutes.contains(self.name) else { return }
        if let color = JFFlutterLoginBgColor {
            //use cache color
            print("use cache color")
            self.view.backgroundColor = color
            return
        }
        let screenSize = UIScreen.main.bounds.size
        let launchView = UIImageView(image: UIImage(named: "bg"))
        launchView.contentMode = .scaleToFill
        launchView.frame = CGRect(x: 0, y: 0, width: screenSize.width, height: screenSize.height)
        if let image = UIImage.jf_convertViewToImage(view: launchView) {
            let myColor = UIColor(patternImage: image)
            JFFlutterLoginBgColor = myColor
            self.view.backgroundColor = myColor
        }
    }

Demo地址

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

推荐阅读更多精彩内容