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

Posted 谷歌开发者

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter 混合开发: 开发一个简单的快速启动框架 | 开发者说·DTalk相关的知识,希望对你有一定的参考价值。

本文原作者: 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 函数,在这里使用 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;


  @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.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<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.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 端稍微复杂一些,需要先了解一下 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 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)
  

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


长按右侧二维码

查看更多开发者精彩分享

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

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


以上是关于Flutter 混合开发: 开发一个简单的快速启动框架 | 开发者说·DTalk的主要内容,如果未能解决你的问题,请参考以下文章

Flutter混合开发:Android中如何启动Flutter

Flutter混合开发:Android中如何启动Flutter

Flutter和iOS混合开发iOS14启动闪退

FlutterFlutter 混合开发 ( 关联 Android 工程与 Flutter 工程 | 安卓页面中嵌入 Flutter 页面 | 安卓中启动 Flutter 页面 )

北海(Kraken)构建大前端混合渲染技术体系 —— Web 与 Flutter Widget 混合渲染方案

FlutterFlutter 混合开发 ( 安卓端向 Flutter 传递数据 | FlutterFragment 数据传递 | FlutterActivity 数据传递 )