序文
モバイル端末でFlutterページを起動する際に短い空白が生じるため、公式のエンジンウォームアップ機構は提供されていますが、事前にすべてのページをウォームアップする必要があるため、開発コストが高くなります。 Xianyu のプラグインを使用して、簡単なクイック スタート フレームワークを自分で実装できるかどうかを確認します。
この記事で使用するナレッジポイントは、「 Flutter ハイブリッド開発: ネイティブと Flutter の相互作用」で詳しく説明されています。最初にこの記事を読んでから、この記事を読むこともできます。この記事ではこれらの内容を繰り返すことはせず、乾物について直接説明します。
プラグインを起動する
Flutter Plugin プロジェクトを作成し、git を追加して、3 ターミナル コードを作成します。
フラッターコード
1つ目はフラッター側のコードです
1)ルートマネージャー
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"),
),
);
}
}
その役割は、ルーティング マッピングを維持するためのマップを使用して、シングルトンであるルーティングを管理することです。これらの関数のうち 3 つはより重要です。
- registerRoute: ルートを登録します。通常、起動時に呼び出されます。
- getRouteFactory: RouteFactory を返します。これを、MaterialApp の onGenerateRoute フィールドに割り当てます。
- getPage: ルート名によってページ ウィジェットを返します。
ここで、getRouteFactory と getPage はルーティング マップを共有しているため、ページ内で切り替えても、ページ間で切り替えても、同じままです。
2)ベースアプリ
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 アプリはそれを継承する必要があります。主にandroid/iosとやり取りするためのBasicMessageChannelをカプセル化し、受信したメッセージに応じてページ内の切り替え処理を行うことで高速起動を実現します。
それを継承するサブクラスでは registerRoutes 関数を実装する必要がありますが、ここでは RouteManager の registerRoute を利用して各ページを登録します。
3) ベースページ
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);
}
こちらも抽象クラスであり、フラッターページごとに継承する必要があり、主に2つの起動メソッドから渡されるパラメータを処理して引数に統一することで、サブクラスの起動方法を意識せずに直接利用できるようにしています。
アンドロイドコード
次はプラグイン内の Android 用のコードです
1)ブートエンジン
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 を作成します。初期化するには、アプリケーションの onCreate で init 関数を呼び出す必要があります。
2)FlutterBootアクティビティ
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 中に、これら 2 つのデータを BasicMessageChannel 経由で処理するために Flutter に送信します。
iOSコード
iOSはAndroidに似ています
1)フラッターブートエンジン
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 を初期化して起動し、Flutter と対話するための FlutterBasicMessageChannel を作成します。
その initEngine 関数は、ios プロジェクトの AppDelegate が初期化されるときに呼び出す必要があります。
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 は上で定義したクラスを使用できます。
このようにして、プラグインが開発され、pub に公開できるようになります。ここでは git ウェアハウスにプッシュし、git の使用に依存しています。
起動モジュール
Flutter モジュールを作成し、プラグインを pubspec.yaml にインポートします。
dependencies:
flutter:
sdk: flutter
...
flutter_boot:
git: https://gitee.com/chzphoenix/flutter-boot.git
次に、テスト用に 2 ページを作成します。
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"]}")
);
}
}
このページでは、渡されたパラメータ キーを取得して表示します。
3)メイン.ダーツ
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 から継承し、これら 2 つのページを登録するために registerRoutes を実装します。
ここでの onGenerateRoute は RouteManager.instance.getRouteFactory() を使用しているため、1 回の登録で十分であり、自分で実装する必要がないことに注意してください。
使用
モジュールを開発すると、andorid/ios上で利用できるようになります。
アンドロイド
Android では比較的簡単で、Android プロジェクトにモジュールをインポートするだけです。次に、次のように、Android のメイン モジュール (通常はアプリ) の build.gradle にモジュールとプラグインを導入する必要があります。
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
...
implementation project(path: ':flutter') //module
provided rootProject.findProject(":flutter_boot") //plugin
}
プラグインの名前はモジュールの 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)
...
}
}
次に、適切なタイミングでフラッター ページを開始します。スタートアップ コードは次のとおりです。
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 つはパラメーターなしでページ 1 を開始し、もう 1 つはパラメーターを使用してページ 2 を開始します。
このテストでは、どのページを開いても、読み込み時間がほとんどなく、非常に高速であることがわかりました。これにより素早いスタートが可能になります。
iOS側
ios 側はもう少し複雑です。最初に ios にフラッターを追加する方法を理解する必要があります。「フラッター ハイブリッド開発: 既存の ios プロジェクトへのフラッターの導入」を参照してください。
フレームワークを導入することにしたので、flutter module プロジェクトの下のコマンドを使用してフレームワークをコンパイルしてパッケージ化します。
flutter build ios-framework --xcframework --no-universal --output=./Flutter/
続いて ios プロジェクトに導入します 前回の記事との違いは、このモジュールにプラグインを追加するため、フレームワーク製品が 4 つあることです。
- App.xcframework
- flutter_boot.xcframework (これはプラグインの iOS 部分です)
- Flutter.xcframework
- FlutterPluginRegistrant.xcframework
これら 4 つは ios プロジェクトに導入する必要があります。
次に、AppDelegate は FlutterAppDelegate を継承する必要があります (継承できない場合は、各ライフサイクルを処理する必要があります。https://flutter.cn/docs/development/add-to-app/ios/add-flutter-screen?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)
}
}
次に、次のように適切な場所でフラッター ページを開始します。
@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)
}
また、2つのページを別々に開くと、起動時のロード時間がほとんどなく、同時にパラメータが正しく渡されていることがわかります。