Flutter's next-generation hybrid stack management framework (adapted to HarmonyOS Next)
2024-01-30 7,685 words Read for 10 minutes
Brief introduction
Fusion is a next-generation hybrid stack management framework for unified management of Flutter and native pages and supports functions such as page communication and page lifecycle listening. Fusion is designed to help developers use Flutter and Native for hybrid development without feeling the gap between the two, and improve the development experience. In addition, Fusion completely solves the problems of black screen, white screen, and flickering screen that are common in the hybrid development process, making it more suitable for apps that value user experience .fusion
Starting from 4.0, Fusion has completed the adaptation of the pure HarmonyOS platform (HarmonyOS Next/OpenHarmony, hereinafter referred to as HarmonyOS), so that developers can get a completely consistent experience on Android, iOS, and HarmonyOS. (The Flutter SDK for HarmonyOS is available here)
THE Android | iOS | HarmonyOS SDK5.0(21)+11.0+4.1(11)+
Fusion uses an engine reuse solution, so that when Flutter and Native pages jump multiple times, the app always has only one instance of FlutterEngine, so it has better performance and lower memory footprint.
Fusion is also currently the only mix-stack framework that supports the normal recovery of all Flutter pages after the application is recycled by the system in the background during hybrid development.
Get started
0. Preparation
Before you start, you need to follow the official Flutter documentation to connect the Flutter Module project to the Android, iOS, and HarmonyOS projects.。
1. Initialization
Flutter side
Use FusionApp to replace the previously used App Widget and pass in the required route table, the default route table and the custom route table can be set separately or at the same time.
dart
Code interpretation
Copy code
void main() { runApp(FusionApp( // Default routing table routeMap: routeMap, // Custom routing table customRouteMap: customRouteMap, )); } //Default routing table, use the default PageRoute // Use a unified routing animationfinal Map<String, FusionPageFactory> routeMap = { '/test': (arguments) => TestPage(arguments: arguments), kUnknownRoute: (arguments) => UnknownPage(arguments: arguments), }; // Customize the routing table, you can customize PageRoute // For example: some pages need specific routing animations, you can use this routing table final Map<String, FusionPageCustomFactory> customRouteMap = { '/mine': (settings) => PageRouteBuilder( opaque: false, settings: settings, pageBuilder: (_, __, ___) => MinePage( arguments: settings.arguments as Map<String, dynamic>?)), };
P.S: Indicates that the route is not definedkUnknownRoute
Note: If the project uses , it needs to be called before runApp, and if it is not used, this step is not required.flutter_screenutilFusion.instance.install()flutter_screenutil
dart
Code interpretation
Copy code
void main() { Fusion.instance.install(); runApp(FusionApp( // Default routing table routeMap: routeMap, // Custom routing table customRouteMap: customRouteMap, )); }
Android side
Initialize in the Application and implement the FusionRouteDelegate interface
kotlin
Code interpretation
Copy code
class MyApplication : Application(), FusionRouteDelegate { override fun onCreate() { super.onCreate() Fusion.install(this, this) } override fun pushNativeRoute(name: String?, arguments: Map<String, Any>?) { // Jump to Native page based on route name } override fun pushFlutterRoute(name: String?, arguments: Map<String, Any>?) { // Jump to Flutter page based on route name // arguments can be used to determine if a transparent page needs to be opened. } }
iOS side
Initialize in AppDelegate and implement the FusionRouteDelegate proxy
swift
Code interpretation
Copy code
@UIApplicationMain @objc class AppDelegate: UIResponder, UIApplicationDelegate, FusionRouteDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { ... Fusion.instance.install(self) ... return true } func pushNativeRoute(name: String?, arguments: Dictionary<String, Any>?) { // According to the route name Jump corresponding Native page } func pushFlutterRoute(name: String?, arguments: Dictionary<String, Any>?) { // According to the route name Jump corresponding Flutter page // Available at arguments Store the parameters to determine whether you need to open the transparent page // Available at arguments The judgment of the stored parameters is push still present } }
HarmonyOS side
Initialize in UIAbility and implement the FusionRouteDelegate proxy
typescript
Code interpretation
Copy code
export default class EntryAbility extends UIAbility implements FusionRouteDelegate { private static TAG = 'EntryAbility' private mainWindow: window.Window | null = null private windowStage: window.WindowStage | null = null override async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> { await Fusion.instance.install(this.context, this) GeneratedPluginRegistrant.registerWith(Fusion.instance.defaultEngine!) } pushNativeRoute(name: string, args: Map<string, Object> | null): void { // Jump to Native page based on route name } pushFlutterRoute(name: string, args: Map<string, Object> | null): void { // Jump to Flutter page based on route name // Can be stored in arguments whether a transparent page needs to be opened } }
2. Flutter container
Normal page mode
Android side
To create a Flutter container (or its subclass), you need to start the container using the method provided by Fusion, where the parameter needs to be set to false. Its XML configuration reference is as follows (if used, it is not configured):FusionActivitybuildFusionIntenttransparentFusionActivity
xml
Code interpretation
Copy code
<activity android:name=".CustomFusionActivity" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:exported="false" android:hardwareAccelerated="true" android:launchMode="standard" android:theme="@style/FusionNormalTheme" android:windowSoftInputMode="adjustResize" />
iOS side
Create a Flutter container through (or its subclasses), and both support. FusionViewController Hidden by default UINavigationController。FusionViewControllerpushpresent
In iOS, you need to deal with the problem of native swipe right exit gesture and Flutter gesture conflict, and the solution is also very simple: just implement it in the custom Flutter container and enable or disable the native gesture in the corresponding method, so that if there are multiple Flutter pages in the current Flutter container, the right swipe gesture is to exit the Flutter page, and when there is only one Flutter page, swipe right to exit the Flutter container.FusionPopGestureHandler
swift
Code interpretation
Copy code
// Enable native gesture func enablePopGesture() { // The following code is for demo only. navigationController?.interactivePopGestureRecognizer?.isEnabled = true } // Disable native gesture func disablePopGesture() { // The following code is for demo only. navigationController?.interactivePopGestureRecognizer?.isEnabled = true } true } // Disable native gestures func disablePopGesture() { // The following code is for demo only, do not copy it directly. navigationController?.interactivePopGestureRecognizer?.isEnabled = false } isEnabled = false }
HarmonyOS side
Creating a Flutter container (or its subclass) requires the Fusion method to start the container, or it can be used directly. Default full-screen mode.FusionEntrybuildFusionParamsFusionPage
typescript
Code interpretation
Copy code
const params = buildFusionParams(name, args, false, backgroundColor) this.mainLocalStorage?.setOrCreate('params', params) router.pushNamedRoute({name: FusionConstant.FUSION_ROUTE_NAME})
Transparent page mode
Android side
The usage is similar to the normal page pattern, except that the method's parameters need to be set to true, and its xml configuration reference is as follows:buildFusionIntenttransparent
xml
Code interpretation
Copy code
<activity android:name=".TransparentFusionActivity" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:exported="false" android:hardwareAccelerated="true" android:launchMode="standard" android:theme="@style/FusionTransparentTheme" android:windowSoftInputMode="adjustResize" />
iOS side
It works in a similar way to normal page mode:
swift
Code interpretation
Copy code
let fusionVc = CustomViewController(routeName: name, routeArguments: arguments, transparent: true) navController?.present(fusionVc, animated: false)
HarmonyOS side
It works in a similar way to normal page mode:
typescript
Code interpretation
Copy code
const params = buildFusionParams(name, args, true, backgroundColor) this.windowStage?.createSubWindow(FusionConstant.TRANSPARENT_WINDOW, (_, win) => { const record: Record<string, Object> = { 'params': params } win.loadContentByName(FusionConstant.FUSION_ROUTE_NAME, new LocalStorage(record)) win.showWindow() })
Flutter side
At the same time, the background of the Flutter page needs to be set to transparent
Subpage pattern
Sub-page mode refers to one or more Flutter Scenes where pages are embedded in the Native container at the same time, such as: Use Tab to switch between Flutter and native pages, Fusion supports multiple Flutter pages to be embedded in the same Native container.
Android side
To use FusionFragment to support the subpage pattern, creating a FusionFragment object requires a methodbuildFusionFragment
iOS side
Use the FusionViewController as in the page mode
HarmonyOS side
Use FusionEntry as in the page mode, with the method configuration parametersbuildFusionParams
Customize the container background color
By default, the background of the container is white, this is because the vast majority of pages use a white background, but if the background of the first Flutter page opened is a different color, such as a dark gray page in night mode, in this case, for better visual effects, you can customize the background color of the container to match the background color of the first Flutter page.
Android side
In the and methods, the parameter is set to the desired background colorbuildFusionIntentbuildFusionFragmentbackgroundColor
iOS side
When you create a FusionViewController (or its subclass) object, set the parameter to the desired background colorbackgroundColor
HarmonyOS side
In the Method, the parameter is set to the desired background colorbuildFusionParamsbackgroundColor
3、routingAPI(FusionNavigator)
push:Put the corresponding route into the stack,Navigator.pushNamed Equivalent to it, according to FusionRouteType, it is divided into the following ways:
flutter mode: Put the Flutter page corresponding to the specified route into the stack in the current Flutter container, and if not, jump to the Flutter page corresponding to kUnknownRoute
flutterWithContainer mode: create a new Flutter container, and will specify the route corresponding to the Flutter page into the stack, if not, then jump kUnknownRoute corresponding Flutter page. That is, execute pushFlutterRoute of FusionRouteDelegate.
native mode: put the Native page of the specified route on the stack, i.e. execute pushNativeRoute of FusionRouteDelegate.
Adaptive mode: Adaptive mode, default type. First, determine whether the route is a Flutter route, if not, enter native mode, if so, then determine whether the current page is a Flutter container, if so, enter flutter mode, if not, enter flutterWithContainer mode
pop:Route the top of the stack out of the stack in the current Flutter container,Navigator.pop Equivalent to
maybePop: Route the top of the stack out of the stack in the current Flutter container, which can be WillPopScopeintercept
replace:Replace the top-of-stack route with the corresponding route in the current Flutter container,Navigator.pushReplacementNamed Equivalent to
remove:Remove the corresponding route from the current Flutter container
You can use both the API for operations such as route redirection and closure as well as the corresponding API in (only the ones mentioned above)FusionNavigatorNavigator
4、Flutter Plugin register
On Android and iOS, the plugin is automatically registered inside the framework and does not need to be registered manually, but HarmonyOS must manually call the method.GeneratedPluginRegistrant.registerWith
5、customize Channel
If you need Native to communicate with Flutter, you need to create a Channel by yourself in the following way (take MethodChannel as an example):
Android side
(1) Methods that have nothing to do with containers
Register in Application
kotlin
Code interpretation
Copy code
val channel = Fusion.defaultEngine?.dartExecutor?.binaryMessenger?.let { MethodChannel( it, "custom_channel" ) } channel?.setMethodCallHandler { call, result -> }
(2) Methods related to containers
Implement the FusionMessengerHandler interface on the self-implemented FusionActivity, FusionFragmentActivity, and FusionFragment, create the Channel in configureFlutterChannel, and release the Channel in the releaseFlutterChannel
kotlin
Code interpretation
Copy code
class CustomActivity : FusionActivity(), FusionMessengerHandler { override fun configureFlutterChannel(binaryMessenger: BinaryMessenger) { val channel = MethodChannel(binaryMessenger, "custom_channel") channel.setMethodCallHandler { call, result -> } } override fun releaseFlutterChannel() { channel?.setMethodCallHandler(null) channel = null } }
iOS side
(1) Methods that have nothing to do with containers
Register in AppDelegate
swift
Code interpretation
Copy code
var channel: FlutterMethodChannel? = nil if let binaryMessenger = Fusion.instance.defaultEngine?.binaryMessenger { channel = FlutterMethodChannel(name: "custom_channel", binaryMessenger: binaryMessenger) } channel?.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in }
(2) Methods related to containers
In self-realization FusionViewController On realization FusionMessengerHandler Agreement, created in the agreement method Channel
swift
Code interpretation
Copy code
class CustomViewController : FusionViewController, FusionMessengerHandler { func configureFlutterChannel(binaryMessenger: FlutterBinaryMessenger) { channel = FlutterMethodChannel(name: "custom_channel", binaryMessenger: binaryMessenger) channel?.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in } } func releaseFlutterChannel() { channel?.setMethodCallHandler(nil) channel = nil } }
HarmonyOS side
(1) Methods that have nothing to do with containers
Register in UIAbility
typescript
Code interpretation
Copy code
const binaryMessenger = Fusion.instance.defaultEngine?.dartExecutor.getBinaryMessenger() const channel = new MethodChannel(binaryMessenger!, 'custom_channel') channel.setMethodCallHandler({ onMethodCall(call: MethodCall, result: MethodResult): void { } })
(2) Methods related to containers
Implement the FusionMessengerHandler interface on the self-implemented FusionEntry, in configure FlutterChannel Create Channel ,in releaseFlutterChannel release Channel
typescript
Code interpretation
Copy code
export default class CustomFusionEntry extends FusionEntry implements FusionMessengerHandler, MethodCallHandler { private channel: MethodChannel | null = null configureFlutterChannel(binaryMessenger: BinaryMessenger): void { this.channel = new MethodChannel(binaryMessenger, 'custom_channel') this.channel.setMethodCallHandler(this) } onMethodCall(call: MethodCall, result: MethodResult): void { result.success(`Custom Channel:${this}_${call.method}`) } releaseFlutterChannel(): void { this.channel?.setMethodCallHandler(null) this.channel = null } }
BasicMessageChannelIt is similar to the use of EventChannel
P.S.: Container-related methods are tied to the container lifecycle, and if the container is not visible or destroyed, you will not be able to receive channel messages.
6. Life cycle
Application Lifecycle Listener:
(1)、You can register and monitor anywhere on the Flutter side, and FusionAppLifecycleListener
implements
(2) Decide whether to cancel the monitor according to the actual situation
dart
Code interpretation
Copy code
void main() { ... FusionAppLifecycleBinding.instance.register(MyAppLifecycleListener()); runApp(const MyApp()); } class MyAppLifecycleListener implements FusionAppLifecycleListener { @override void onBackground() { print('onBackground'); } @override void onForeground() { print('onForeground'); } }
FusionAppLifecycleListener Life cycle callback function:
onForeground: The app will be called when it enters the foreground (it will not be called when it is launched for the first time, Android and iOS are the same)
onBackground: The app will be called when it is withdrawn to the background
Page Lifecycle Listener:
(1)、When you need to listen to the life cycle page State 中 FusionPageLifecycleListener
implements
(2) Register a listener in didChangeDependencies
(3) Cancel the listener in dispose
dart
Code interpretation
Copy code
class LifecyclePage extends StatefulWidget { const LifecyclePage({Key? key}) : super(key: key); @override State<LifecyclePage> createState() => _LifecyclePageState(); } class _LifecyclePageState extends State<LifecyclePage> implements FusionPageLifecycleListener { @override Widget build(BuildContext context) { return Container(); } @override void didChangeDependencies() { super.didChangeDependencies(); FusionPageLifecycleBinding.instance.register(this); } @override void onPageVisible() {} @override void onPageInvisible() {} @override void onForeground() {} @override void onBackground() {} @override void dispose() { super.dispose(); FusionPageLifecycleBinding.instance.unregister(this); } }
PageLifecycleListener Life cycle callback function:
onForeground: The application will be called when it enters the foreground, and all pages that have registered for lifecycle listening will be received
onBackground: The app will be called to retreat to the background, and all pages that have registered for lifecycle listeners will be received
onPageVisible: Called when the Flutter page is visible, e.g. when the Flutter page is accessed from a native page or another Flutter page; from a Native page or another Flutter page to that Flutter page; The app is also called when it comes to the foreground.
pushpop
onPageInvisible: Called when the Flutter page is not visible, such as when going from the Flutter page to the Native page or other Flutter page; such as when going from the Flutter page to the Native page or other Flutter page; it will also be called when the application returns to the background.pushpop
7. Global communication
Support the delivery of messages in the app, you can specify Native or Flutter or global receive and send.
Register for a message listener
Flutter side
(1) In the class that needs to listen to the message, FusionNotificationListener, and override the method, which can receive the sent message
implementsonReceive
(2) Register for monitoring at an appropriate time
(3) Cancel the monitor at the appropriate time
dart
Code interpretation
Copy code
class TestPage extends StatefulWidget { @override State<TestPage> createState() => _TestPageState(); } class _TestPageState extends State<TestPage> implements FusionNotificationListener { @override void onReceive(String name, Map<String, dynamic>? body) { } @override void didChangeDependencies() { super.didChangeDependencies(); FusionNotificationBinding.instance.register(this); } @override void dispose() { super.dispose(); FusionNotificationBinding.instance.unregister(this); } }
Native side
(1) Implement the FusionNotificationListener interface in the class that needs to listen to the message, and copy the method, which can receive the sent message
onReceive
(2) The method used at the appropriate time to register the listener
FusionNotificationBindingregister
(3) The method used at the appropriate time to cancel the listener
FusionNotificationBindingunregister
Send a message
The methods that can be used on all three sides to send messages have different effects depending on the type of FusionNotificationType used:FusionNavigatorsendMessage
flutter: Only Flutter can receive
native: Only Native can receive
global (default): Both Flutter and Native can be received
8. Return to intercept
In pure Flutter development, you can use components to intercept return operations, and Fusion fully supports this feature in exactly the same way as in pure Flutter development, and the operations used can also be intercepted by components.WillPopScopeFusionNavigator.maybePopWillPopScope
9. Status restoration
Fusion supports the recovery of Flutter routes after APPS on Android and iOS platforms are recycled.