Flutter 混合开发: 开发一个简单的快速启动框架 | 开发者说·DTalk

2022 年 6 月 24 日 谷歌开发者
本文原作者: BennuC 原文 发布于: BennuCTech


在移动端中启动 Flutter 页面会有短暂空白,虽然官方提供了引擎预热机制,但是需要提前将所有页面都进行预热,这样开发成本较高,在研究了闲鱼的 FlutterBoost 插件后,看看能不能自己实现一个简单的快速启动框架。



开发启动框架 plugin


创建一个 Flutter Plugin 项目,并添加 git,然后编写三端代码: 


Flutter 代码
首先是 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{
@override State<StatefulWidget> createState() { return _PageNotFount(); }
}
class _PageNotFount extends BaseState<PageNotFount>{
@override Widget buildImpl(BuildContext context) { return Scaffold( body: Center( child: Text("page not found"), ), ); }}


它的作用就是管理路由,是一个单例,用一个 map 来维护路由映射。其中三个函数比较重要: 
  • registerRoute : 注册路由,一般在启动时调用;
  • getRouteFactory : 返回 RouteFactory ,将它赋值给 MaterialApp onGenerateRoute 字段;
  • getPage : 通过 route 名称返回页面 widget。

这里 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{
@override 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 ""; }); }
@override Widget build(BuildContext context) { return buildImpl.call(context, curPage); }
}

是一个抽象类,真正的 Flutter app 需要继承它。主要是封装了一个 BasicMessageChannel 用来与 Android/iOS 交互,并根据收到的消息处理页面内的切换,实现快速启动。


继承它的子类需要实现 registerRoutes 函数,在这里使用 RouteManagerregisterRoute 将每个页面注册一下即可。


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;
@override 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 中,这样子类就可以直接使用而不需要考虑是如何启动的。


Android 代码
接下来是 plugin 中的 Android 的代码

1. BootEngine

package com.bennu.flutter_boot
import android.app.Applicationimport io.flutter.embedding.engine.FlutterEngineimport io.flutter.embedding.engine.FlutterEngineCacheimport io.flutter.embedding.engine.dart.DartExecutorimport io.flutter.plugin.common.BasicMessageChannelimport 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.ComponentNameimport android.content.Contextimport android.content.Intentimport android.os.Bundleimport android.os.PersistableBundleimport io.flutter.embedding.android.FlutterActivityimport 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<String, String>?) 函数来启动,传递路由名称和参数。在 onResume 的时候通过 BasicMessageChannel 将这两个数据 send 给 Flutter 处理。

iOS

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 


创建一个 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{
@override State<StatefulWidget> createState() { return _FirstPage(); }}
class _FirstPage extends BaseState<FirstPage>{
void _goClick() { Navigator.of(context).pushNamed("second", arguments: {"key":"123"}); }
@override 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{
@override State<StatefulWidget> createState() { return _SecondPage(); }
}
class _SecondPage extends BaseState<SecondPage>{
@override 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 { @override Widget build(BuildContext context, Widget page) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: page, onGenerateRoute: RouteManager.instance.getRouteFactory(), ); }
@override void registerRoutes() { RouteManager.instance.registerRoute("main", FirstPage()); RouteManager.instance.registerRoute("second", SecondPage()); }}


入口继承 BaseApp,并实现 registerRoutes,注册这两个页面。


注意这里的 onGenerateRoute 使用 RouteManager.instance.getRouteFactory (),这样一次注册就可以了,不必自己去实现。



引入移动端


Module 开发完后,就可以在 Android/iOS 上使用了。

Android 端

在 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.Applicationimport 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 端稍微复杂一些,需要先了解一下 iOS 如何加入 Flutter。


我选用的是 framework 的方式引入,所以在 Flutter module 项目下通过命令编译打包 framework。


flutter build ios-framework --xcframework --no-universal --output=./Flutter/

然后引入到 iOS 项目中,与上一篇文章不同的是,因为这个 module 中加入了 plugin,所以 framework 产物是四个: 
  • App.xcframework
  • flutter_boot.xcframework (这个就是我们的 plugin 中的 iOS 部分)
  • Flutter.xcframework
  • FlutterPluginRegistrant.xcframework

这四个都需要引入到 iOS 项目中。


然后 AppDelegate 需要继承 FlutterAppDelegate (如果无法继承,则需要处理每个生命周期,您可以查看: https://flutter.cn/docs/development/add-to-app/ios/add-flutter-screen?tab=engine-swift-tab)。


然后在 AppDelegate 中初始化,如下: 
import UIKitimport Flutterimport flutter_boot
@UIApplicationMainclass 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) }


同样分别打开两个页面,可以看到启动几乎没有加载时间,同时参数也正确传递。




长按右侧二维码

查看更多开发者精彩分享




"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。



 

 点击屏末 |  | 即刻报名参与 "开发者说·DTalk" 

 



登录查看更多
0

相关内容

Google 发布的面向结构化 web 应用的开语言。 dartlang.org
2021年中国云原生AI开发平台白皮书
专知会员服务
54+阅读 · 2021年12月4日
2021年中国AI开发平台市场报告
专知会员服务
72+阅读 · 2021年10月26日
TensorFlowLite:端侧机器学习框架
专知会员服务
32+阅读 · 2020年8月27日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
22+阅读 · 2019年11月7日
Keras François Chollet 《Deep Learning with Python 》, 386页pdf
专知会员服务
152+阅读 · 2019年10月12日
带您了解最全面的 Flutter Web | 开发者说·DTalk
谷歌开发者
0+阅读 · 2022年7月8日
一起看 I/O | Flutter 休闲游戏工具包发布
谷歌开发者
0+阅读 · 2022年5月19日
我们为什么选择了Flutter Desktop | 开发者说·DTalk
谷歌开发者
0+阅读 · 2022年3月10日
如何理解 Flutter 路由源码设计?| 开发者说·DTalk
谷歌开发者
1+阅读 · 2022年1月28日
Flutter 如何与 Native (Android) 进行交互 | 开发者说·DTalk
Flutter 之美 | 开发者说·DTalk
谷歌开发者
1+阅读 · 2021年12月23日
国家自然科学基金
0+阅读 · 2015年12月31日
国家自然科学基金
0+阅读 · 2014年12月31日
国家自然科学基金
1+阅读 · 2014年12月31日
国家自然科学基金
1+阅读 · 2013年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2011年12月31日
国家自然科学基金
0+阅读 · 2011年12月31日
国家自然科学基金
1+阅读 · 2008年12月31日
Output-Oblivious Stochastic Chemical Reaction Networks
Object Detection in 20 Years: A Survey
Arxiv
48+阅读 · 2019年5月13日
VIP会员
相关资讯
带您了解最全面的 Flutter Web | 开发者说·DTalk
谷歌开发者
0+阅读 · 2022年7月8日
一起看 I/O | Flutter 休闲游戏工具包发布
谷歌开发者
0+阅读 · 2022年5月19日
我们为什么选择了Flutter Desktop | 开发者说·DTalk
谷歌开发者
0+阅读 · 2022年3月10日
如何理解 Flutter 路由源码设计?| 开发者说·DTalk
谷歌开发者
1+阅读 · 2022年1月28日
Flutter 如何与 Native (Android) 进行交互 | 开发者说·DTalk
Flutter 之美 | 开发者说·DTalk
谷歌开发者
1+阅读 · 2021年12月23日
相关基金
国家自然科学基金
0+阅读 · 2015年12月31日
国家自然科学基金
0+阅读 · 2014年12月31日
国家自然科学基金
1+阅读 · 2014年12月31日
国家自然科学基金
1+阅读 · 2013年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
0+阅读 · 2011年12月31日
国家自然科学基金
0+阅读 · 2011年12月31日
国家自然科学基金
1+阅读 · 2008年12月31日
Top
微信扫码咨询专知VIP会员