iOS原生混編Flutter路由指南及解決Flutter首頁閃白屏問題

語言: CN / TW / HK

前言

我正在參與掘金技術社區創作者簽約計劃招募活動,點擊鏈接報名投稿

公司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 pageMap = {

JFRoute.loginPage: (settings, uniqueId) {

return CupertinoPageRoute(

settings: settings,

builder: (_) => const LoginPage(

),

);

},

JFRoute.demoPage: (settings, uniqueId) {

return CupertinoPageRoute(

settings: settings,

builder: (_) => const DemoPage(

),

);

},

};

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

末尾

我正在參與掘金技術社區創作者簽約計劃招募活動,點擊鏈接報名投稿