[Flutter Mixed Development] Develop a simple quick start framework

insert image description here

foreword

Because there will be a short blank when starting the Flutter page on the mobile terminal, although the official engine warm-up mechanism is provided, but all pages need to be warmed up in advance, so the development cost is high. After studying the FlutterBoost plug-in of Xianyu, I See if you can implement a simple quick start framework yourself.

The knowledge points used in this article are explained in detail in "flutter hybrid development: interaction between native and flutter" . You can read this article first and then read this article. This article will not repeat these contents, and go directly to the dry goods.

Start the plugin

Create a Flutter Plugin project, add git, and write three-terminal code:

Flutter code

The first is the code on the flutter side

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"),
      ),
    );
  }
}

Its role is to manage routing, which is a singleton, using a map to maintain routing mapping. Three of these functions are more important:

  • registerRoute: register route. Usually called at startup.
  • getRouteFactory: returns RouteFactory. Assign it to the onGenerateRoute field of MaterialApp
  • getPage: Returns the page widget by the route name.

Here getRouteFactory and getPage share a routing map, so whether it is switching within a page or switching between pages, it remains the same.

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);
  }

}

It is an abstract class, and the real flutter app needs to inherit it. It mainly encapsulates a BasicMessageChannel for interacting with android/ios, and processes the switching in the page according to the received message to realize quick startup.

Subclasses that inherit it need to implement the registerRoutes function. Here, use the registerRoute of RouteManager to register each page.

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);
}

It is also an abstract class, and every flutter page needs to inherit it. It mainly processes the parameters passed by the two startup methods and unifies them into args, so that subclasses can be used directly without considering how to start them.

android code

Next is the code for android in the plugin

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)
    }
}

This is a singleton, which initializes and warms up FlutterEngine, and creates a BasicMessageChannel for subsequent interactions. Need to call its init function in Application's onCreate to initialize.

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()
    }
}

Inherit FlutterActivity, provide a build(context: Context, routeName : String, params : Map<String, String>?) function to start, and pass the route name and parameters. During onResume, send these two data to flutter for processing through BasicMessageChannel.

iOS code

ios is similar to 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

This is also a singleton, initialize and start FlutterEngine, and create a FlutterBasicMessageChannel to interact with flutter.

Its initEngine function needs to be called when the AppDelegate of the ios project is initialized.

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

Also add a constructor using the route name and parameters, and then notify flutter when viewWillAppear.

Note that if it is a bit late to change to viewDidAppear here, the previous page will be displayed before updating to the new page, so change to viewWillAppear.

3)FlutterBoot.h

#ifndef FlutterBoot_h
#define FlutterBoot_h

#import "FlutterBootEngine.h"
#import "FlutterBootViewController.h"

#endif /* FlutterBoot_h */

This is a swift bridging file, through which swift can use the class we defined above.

In this way, our plugin is developed and can be published to pub. I am here to push to the git warehouse, and rely on the use of git.

start module

Create a flutter module, and then import our plugin, in pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  ...
  flutter_boot:
    git: https://gitee.com/chzphoenix/flutter-boot.git

Then we develop two pages for testing.

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.
    );
  }
}

Just inherit BasePage and BaseState, click the button to jump to page 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"]}")
    );
  }
}

This page gets the passed parameter key and displays it.

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());
  }
}

The entry inherits from BaseApp and implements registerRoutes to register these two pages.

Note that onGenerateRoute here uses RouteManager.instance.getRouteFactory(), so that one registration is enough, and you don't have to implement it yourself.

use

After the module is developed, it can be used on andorid/ios.

android

It is relatively simple on android, just import the module just now in the android project, and then you need to introduce the module and plugin in the build.gradle of the main module of android (usually app), as follows:

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    ...
    implementation project(path: ':flutter')  //module
    provided rootProject.findProject(":flutter_boot") //plugin
}

Note that the name of the plugin is previously defined in pubspec.yaml in the module.

Then it can be used in android. First, it must be initialized, as follows:

import android.app.Application
import com.bennu.flutter_boot.BootEngine

public class App : Application() {
    
    

    override fun onCreate() {
    
    
        super.onCreate()
        BootEngine.init(this)
        ...
    }
}

Then start the flutter page at the right time, the startup code is as follows:

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))
}

One starts page 1 without parameters, and one starts page 2 with parameters.

The test can find that no matter which page is opened, it is very fast, with almost no loading time. This enables a quick start.

ios side

The ios side is a little more complicated. You need to understand how to add flutter to ios first, see "flutter hybrid development: introducing flutter into existing ios projects"

I chose to introduce the framework, so compile and package the framework through the command under the flutter module project

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

Then introduce it into the ios project. The difference from the previous article is that because the plugin is added to this module, there are four framework products:

  • App.xcframework
  • flutter_boot.xcframework (this is the ios part of our plugin)
  • Flutter.xcframework
  • FlutterPluginRegistrant.xcframework

These four need to be introduced into the ios project.

Then AppDelegate needs to inherit FlutterAppDelegate (if it cannot be inherited, it needs to handle each life cycle, see https://flutter.cn/docs/development/add-to-app/ios/add-flutter-screen?tab=engine-swift -tab).

Then initialize in AppDelegate, as follows:

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)
    }
}

Then start the flutter page in a suitable place, as follows:

@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)
  }

Also open two pages separately, you can see that there is almost no loading time for startup, and the parameters are passed correctly at the same time.

Guess you like

Origin blog.csdn.net/chzphoenix/article/details/122688396