Flutter混合开发:开发一个简单的快速启动框架

·  阅读 1513

我正在参加跨端技术专题征文活动,详情查看:juejin.cn/post/710123…

前言

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

这篇文章用到的知识点都在《flutter混合开发:native与flutter交互》中详细讲解了,大家可以先读一下这篇文章再来看本文。本文不再赘述这些内容,直接上干货。

开发启动框架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开发完后,就可以在andorid/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,见《flutter混合开发:在已有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(如果无法继承,则需要处理每个生命周期,见flutter.cn/docs/develo…

然后在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)
  }
复制代码

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

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改