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

4,684 阅读4分钟

前言

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

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

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来承载图片,让它自动撑满,再对图片截图然后缓存起来,这样渲染出来的图片就可以启动图一摸一样了。

代码如下,以及最终效果。


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地址

末尾

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿