因为在移动端中启动Flutter页面会有短暂空白,虽然官方提供了引擎预热机制,但是需要提前将所有页面都进行预热,这样开发成本较高,在研究了闲鱼的FlutterBoost插件后,我看看能不能自己实现一个简单的快速启动框架。
这篇文章用到的知识点都在《flutter混合开发:native与flutter交互》中详细讲解了,大家可以先读一下这篇文章再来看本文。本文不再赘述这些内容,直接上干货。
创建一个Flutter Plugin项目,并添加git,然后编写三端代码:
首先是flutter端的代码
1)RouteManager
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_boot/BasePage.dart';
class RouteManager{
factory RouteManager() => _getInstance();
static RouteManager get instance => _getInstance();
static RouteManager _instance;
RouteManager._internal(){
}
static RouteManager _getInstance(){
if(_instance == null){
_instance = new RouteManager._internal();
}
return _instance;
}
Map<String, BasePage> routes = Map();
void registerRoute(String route, BasePage page){
routes[route] = page;
}
RouteFactory getRouteFactory(){
return getRoute;
}
MaterialPageRoute getRoute(RouteSettings settings){
if(routes.containsKey(settings.name)){
return MaterialPageRoute(builder: (BuildContext context) {
return routes[settings.name];
}, settings: settings);
}
else{
return MaterialPageRoute(builder: (BuildContext context) {
return PageNotFount();
});
}
}
BasePage getPage(String name){
if(routes.containsKey(name)) {
return routes[name];
}
else{
return PageNotFount();
}
}
}
class PageNotFount extends BasePage{
State<StatefulWidget> createState() {
return _PageNotFount();
}
}
class _PageNotFount extends BaseState<PageNotFount>{
Widget buildImpl(BuildContext context) {
return Scaffold(
body: Center(
child: Text("page not found"),
),
);
}
}
它的作用就是管理路由,是一个单例,用一个map来维护路由映射。其中三个函数比较重要:
这里getRouteFactory和getPage共用一个路由map,所以不论是页面内切换还是页面切换都保持统一。
2)BaseApp
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter_boot/RouteManager.dart';
abstract class BaseApp extends StatefulWidget{
State<StatefulWidget> createState() {
registerRoutes();
return _BaseApp(build);
}
Widget build(BuildContext context, Widget page);
void registerRoutes();
}
class _BaseApp extends State<BaseApp>{
Function buildImpl;
static const bootChannel = const BasicMessageChannel<String>("startPage", StringCodec());
Widget curPage = RouteManager.instance.getPage("");
_BaseApp(this.buildImpl){
bootChannel.setMessageHandler((message) async {
setState(() {
var json = jsonDecode(message);
var route = json["route"];
var page = RouteManager.instance.getPage(route);
page.args = json["params"];
curPage = page;
});
return "";
});
}
Widget build(BuildContext context) {
return buildImpl.call(context, curPage);
}
}
是一个抽象类,真正的flutter app需要继承它。主要是封装了一个BasicMessageChannel用来与android/ios交互,并根据收到的消息处理页面内的切换,实现快速启动。
继承它的子类需要实现registerRoutes函数,在这里使用RouteManager的registerRoute将每个页面注册一下即可。
3)BasePage
import 'package:flutter/material.dart';
abstract class BasePage extends StatefulWidget{
dynamic args;
}
abstract class BaseState<T extends BasePage> extends State<T>{
dynamic args;
Widget build(BuildContext context) {
if(ModalRoute.of(context).settings.arguments == null){
args = widget.args;
}
else{
args = ModalRoute.of(context).settings.arguments;
}
return buildImpl(context);
}
Widget buildImpl(BuildContext context);
}
同样是抽象类,每个flutter页面都需要继承它,它主要是处理两种启动方式传过来的参数,统一到args中,这样子类就可以直接使用而不需要考虑是如何启动的。
接下来是plugin中的android的代码
1)BootEngine
package com.bennu.flutter_boot
import android.app.Application
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.StringCodec
object BootEngine {
public var flutterBoot : BasicMessageChannel<String>? = null
fun init(context: Application){
var flutterEngine = FlutterEngine(context)
flutterEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
FlutterEngineCache.getInstance().put("main", flutterEngine)
flutterBoot = BasicMessageChannel<String>(flutterEngine.dartExecutor.binaryMessenger, "startPage", StringCodec.INSTANCE)
}
}
这个是单例,初始化并预热FlutterEngine,同时创建BasicMessageChannel用于后续交互。需要在Application的onCreate中调用它的init函数来初始化。
2)FlutterBootActivity
package com.bennu.flutter_boot
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.PersistableBundle
import io.flutter.embedding.android.FlutterActivity
import org.json.JSONObject
class FlutterBootActivity : FlutterActivity() {
companion object{
const val ROUTE_KEY = "flutter.route.key"
fun build(context: Context, routeName : String, params : Map<String, String>?) : Intent{
var intent = withCachedEngine("main").build(context)
intent.component = ComponentName(context, FlutterBootActivity::class.java)
var json = JSONObject()
json.put("route", routeName)
var paramsObj = JSONObject()
params?.let {
for(entry in it){
paramsObj.put(entry.key, entry.value)
}
}
json.put("params", paramsObj)
intent.putExtra(ROUTE_KEY, json.toString())
return intent
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
}
override fun onResume() {
super.onResume()
var route = intent.getStringExtra(ROUTE_KEY)
BootEngine.flutterBoot?.send(route)
}
override fun onDestroy() {
super.onDestroy()
}
}
继承FlutterActivity,提供一个build(context: Context, routeName : String, params : Map
ios与android类似
1)FlutterBootEngine
FlutterBootEngine.h
#ifndef FlutterBootEngine_h
#define FlutterBootEngine_h
#import <Foundation/Foundation.h>
#import <Flutter/Flutter.h>
@interface FlutterBootEngine : NSObject
+ (nonnull instancetype)sharedInstance;
- (FlutterBasicMessageChannel *)channel;
- (FlutterEngine *)engine;
- (void)initEngine;
@end
#endif /* FlutterBootEngine_h */
FlutterBootEngine.m
#import "FlutterBootEngine.h"
#import <Flutter/Flutter.h>
@implementation FlutterBootEngine
static FlutterBootEngine * instance = nil;
FlutterEngine * engine = nil;
FlutterBasicMessageChannel * channel = nil;
+(nonnull FlutterBootEngine *)sharedInstance{
if(instance == nil){
instance = [self.class new];
}
return instance;
}
+(id)allocWithZone:(struct _NSZone *)zone{
if(instance == nil){
instance = [[super allocWithZone:zone]init];
}
return instance;
}
- (id)copyWithZone:(NSZone *)zone{
return instance;
}
- (FlutterEngine *)engine{
return engine;
}
- (FlutterBasicMessageChannel *)channel{
return channel;
}
- (void)initEngine{
engine = [[FlutterEngine alloc]initWithName:@"flutter engine"];
channel = [FlutterBasicMessageChannel messageChannelWithName:@"startPage" binaryMessenger:engine.binaryMessenger codec:[FlutterStringCodec sharedInstance]];
[engine run];
}
@end
这是也是一个单例,初始化并启动FlutterEngine,并创建一个FlutterBasicMessageChannel与flutter交互。
需要在ios项目的AppDelegate初始化时调用它的initEngine函数。
2)FlutterBootViewController
FlutterBootViewController.h
#ifndef FlutterBootViewController_h
#define FlutterBootViewController_h
#import <Flutter/FlutterViewController.h>
@interface FlutterBootViewController : FlutterViewController
- (nonnull instancetype)initWithRoute:(nonnull NSString*)route
params:(nullable NSDictionary*)params;
@end
#endif /* FlutterBootViewController_h */
FlutterBootViewController.m
#import "FlutterBootViewController.h"
#import "FlutterBootEngine.h"
@implementation FlutterBootViewController
NSString * mRoute = nil;
NSDictionary * mParams = nil;
- (nonnull instancetype)initWithRoute:(nonnull NSString *)route params:(nullable NSDictionary *)params{
self = [super initWithEngine:FlutterBootEngine.sharedInstance.engine nibName:nil bundle:nil];
mRoute = route;
mParams = params;
return self;
}
//viewDidAppear时机有点晚,会先显示一下上一个页面才更新到新页面,所以换成viewWillAppear
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
if(mParams == nil){
mParams = [[NSDictionary alloc]init];
}
NSDictionary * dict = @{@"route" : mRoute, @"params" : mParams};
NSData * jsonData = [NSJSONSerialization dataWithJSONObject:dict options:0 error:NULL];
NSString * str = [[NSString alloc]initWithData:jsonData encoding:NSUTF8StringEncoding];
NSLog(@"%@", str);
[FlutterBootEngine.sharedInstance.channel sendMessage:str];
}
@end
同样新增一个使用路由名和参数的构造函数,然后在viewWillAppear时通知flutter。
注意这里如果改成viewDidAppear时机有点晚,会先显示一下上一个页面才更新到新页面,所以换成viewWillAppear。
3)FlutterBoot.h
#ifndef FlutterBoot_h
#define FlutterBoot_h
#import "FlutterBootEngine.h"
#import "FlutterBootViewController.h"
#endif /* FlutterBoot_h */
这个是swift的桥接文件,通过它swift就可以使用我们上面定义的类。
这样我们的plugin就开发完成了,可以发布到pub上。我这里是push到git仓库中,通过git的方式依赖使用。
创建一个flutter module,然后引入我们的plugin,在pubspec.yaml中:
dependencies:
flutter:
sdk: flutter
...
flutter_boot:
git: https://gitee.com/chzphoenix/flutter-boot.git
然后我们开发两个页面用于测试。
1)FirstPage.dart
import 'package:flutter/material.dart';
import 'package:flutter_boot/BasePage.dart';
class FirstPage extends BasePage{
State<StatefulWidget> createState() {
return _FirstPage();
}
}
class _FirstPage extends BaseState<FirstPage>{
void _goClick() {
Navigator.of(context).pushNamed("second", arguments: {"key":"123"});
}
Widget buildImpl(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Flutter Demo Home Page"),
),
body: Center(
child: ...,
),
floatingActionButton: FloatingActionButton(
onPressed: _goClick,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
继承BasePage和BaseState即可,点击按钮可以跳转到页面2
2)SecondPage.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_boot/BasePage.dart';
class SecondPage extends BasePage{
State<StatefulWidget> createState() {
return _SecondPage();
}
}
class _SecondPage extends BaseState<SecondPage>{
Widget buildImpl(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("test"),
),
body:Text("test:${args["key"]}")
);
}
}
这个页面获取传递过来的参数key,并展示。
3)main.dart
import 'package:flutter/material.dart';
import 'package:flutter_boot/BaseApp.dart';
import 'package:flutter_boot/RouteManager.dart';
import 'FirstPage.dart';
import 'SecondPage.dart';
void main() => runApp(MyApp());
class MyApp extends BaseApp {
Widget build(BuildContext context, Widget page) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: page,
onGenerateRoute: RouteManager.instance.getRouteFactory(),
);
}
void registerRoutes() {
RouteManager.instance.registerRoute("main", FirstPage());
RouteManager.instance.registerRoute("second", SecondPage());
}
}
入口继承BaseApp,并实现registerRoutes,注册这两个页面。
注意这里的onGenerateRoute使用RouteManager.instance.getRouteFactory(),这样一次注册就可以了,不必自己去实现。
module开发完后,就可以在andorid/ios上使用了。
在android上比较简单,在android项目中引入刚才的module即可,然后需要在android的主module(一般是app)的build.gradle中引入module和plugin,如下:
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
...
implementation project(path: ':flutter') //module
provided rootProject.findProject(":flutter_boot") //plugin
}
注意plugin的名称是之前在module中的pubspec.yaml定义的。
然后就可以在android中使用了,首先要初始化,如下:
import android.app.Application
import com.bennu.flutter_boot.BootEngine
public class App : Application() {
override fun onCreate() {
super.onCreate()
BootEngine.init(this)
...
}
}
然后合适的时候启动flutter页面即可,启动代码如下:
button.setOnClickListener {
startActivity(FlutterBootActivity.build(this, "main", null))
}
button2.setOnClickListener {
var params = HashMap<String, String>()
params.put("key", "123")
startActivity(FlutterBootActivity.build(this, "second", params))
}
一个启动无参的页面1,一个启动有参的页面2。
测试可以发现无论打开哪个页面都非常快,几乎没有加载时间。这样就实现了快速启动。
ios端稍微复杂一些,需要先了解一下ios如何加入flutter,见《flutter混合开发:在已有ios项目中引入flutter》
我选用的是framework的方式引入,所以在flutter module项目下通过命令编译打包framework
flutter build ios-framework --xcframework --no-universal --output=./Flutter/
然后引入到ios项目中,与上一篇文章不同的是,因为这个module中加入了plugin,所以framework产物是四个:
这四个都需要引入到ios项目中。
然后AppDelegate需要继承FlutterAppDelegate(如果无法继承,则需要处理每个生命周期,见https://flutter.cn/docs/development/add-to-app/ios/add-flutter-screen?tab=engine-swift-tab) 。
然后在AppDelegate中初始化,如下:
import UIKit
import Flutter
import flutter_boot
@UIApplicationMain
class AppDelegate: FlutterAppDelegate {
override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FlutterBootEngine.sharedInstance().initEngine()
return true
}
override func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
}
然后在合适的地方启动flutter页面即可,如下:
@objc func showMain() {
let flutterViewController =
FlutterBootViewController(route: "main", params: nil)
present(flutterViewController, animated: true, completion: nil)
}
@objc func showSecond() {
let params : Dictionary<String, String> = ["key" : "123"]
let flutterViewController =
FlutterBootViewController(route: "second", params: params)
present(flutterViewController, animated: true, completion: nil)
}
同样分别打开两个页面,可以看到启动几乎没有加载时间,同时参数也正确传递。