在上一篇iOS原生接入Flutter添坑记录-接入篇介绍了iOS端接入Flutter的方法,这篇将介绍iOS如何进行耦合,交互。
我只是个不合格的翻译,请看官方介绍.
一.Flutter原生方案
1.1FlutterAppDelegate
这个比较简单,只需在AppDelegate中继承FlutterAppDelegate即可
然后在AppDelegate.h文件中声明一个flutterEngine属性
#import <UIKit/UIKit.h>
#import <Flutter/Flutter.h>
@interface AppDelegate : FlutterAppDelegate
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end
在AppDelegate.m中
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h> // Only if you have Flutter Plugins
#import "AppDelegate.h"
@implementation AppDelegate
// This override can be omitted if you do not have any Flutter Plugins.
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.flutterEngine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil];
[self.flutterEngine runWithEntrypoint:nil];
[GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end
FlutterEngine 是用来管理FlutterViewController控制器的,但是FlutterEngine本身是一个比较重的对象,所以官方给出的代码中就是在AppDelegate中初始化一个engine,在其他地方需要使用时再取用,但实际上这里有个坑,这个我们后面1.3页面跳转中会说到。
1.2 FlutterAppLifeCycleProvider 实现协议来完成Flutter耦合
如果你的AppDelegate不幸的已经继承自其他类了,那你就需要使用这个接入方案。简单来说就是自己实现FlutterAppDelegate中实现的协议FlutterAppLifeCycleProvider。
同样的在AppDelegate.h中,声明一个FlutterEngine,同时遵循协议
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h> // Only if you have Flutter Plugins
@interface AppDelegate : UIResponder <UIApplicationDelegate, FlutterAppLifeCycleProvider>
@property (strong, nonatomic) UIWindow *window;
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end
在AppDelegate.m中
@implementation AppDelegate
{
FlutterPluginAppLifeCycleDelegate *_lifeCycleDelegate;
}
- (instancetype)init {
if (self = [super init]) {
_lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
}
return self;
}
- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
self.flutterEngine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil];
[self.flutterEngine runWithEntrypoint:nil];
[GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine]; // Only if you are using Flutter plugins.
return [_lifeCycleDelegate application:application didFinishLaunchingWithOptions:launchOptions];
}
// Returns the key window's rootViewController, if it's a FlutterViewController.
// Otherwise, returns nil.
- (FlutterViewController*)rootFlutterViewController {
UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
if ([viewController isKindOfClass:[FlutterViewController class]]) {
return (FlutterViewController*)viewController;
}
return nil;
}
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
[super touchesBegan:touches withEvent:event];
// Pass status bar taps to key window Flutter rootViewController.
if (self.rootFlutterViewController != nil) {
[self.rootFlutterViewController handleStatusBarTouches:event];
}
}
- (void)applicationDidEnterBackground:(UIApplication*)application {
[_lifeCycleDelegate applicationDidEnterBackground:application];
}
- (void)applicationWillEnterForeground:(UIApplication*)application {
[_lifeCycleDelegate applicationWillEnterForeground:application];
}
- (void)applicationWillResignActive:(UIApplication*)application {
[_lifeCycleDelegate applicationWillResignActive:application];
}
- (void)applicationDidBecomeActive:(UIApplication*)application {
[_lifeCycleDelegate applicationDidBecomeActive:application];
}
- (void)applicationWillTerminate:(UIApplication*)application {
[_lifeCycleDelegate applicationWillTerminate:application];
}
- (void)application:(UIApplication*)application
didRegisterUserNotificationSettings:(UIUserNotificationSettings*)notificationSettings {
[_lifeCycleDelegate application:application
didRegisterUserNotificationSettings:notificationSettings];
}
- (void)application:(UIApplication*)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken {
[_lifeCycleDelegate application:application
didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}
- (void)application:(UIApplication*)application
didReceiveRemoteNotification:(NSDictionary*)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
[_lifeCycleDelegate application:application
didReceiveRemoteNotification:userInfo
fetchCompletionHandler:completionHandler];
}
- (BOOL)application:(UIApplication*)application
openURL:(NSURL*)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options {
return [_lifeCycleDelegate application:application openURL:url options:options];
}
- (BOOL)application:(UIApplication*)application handleOpenURL:(NSURL*)url {
return [_lifeCycleDelegate application:application handleOpenURL:url];
}
- (BOOL)application:(UIApplication*)application
openURL:(NSURL*)url
sourceApplication:(NSString*)sourceApplication
annotation:(id)annotation {
return [_lifeCycleDelegate application:application
openURL:url
sourceApplication:sourceApplication
annotation:annotation];
}
- (void)application:(UIApplication*)application
performActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem
completionHandler:(void (^)(BOOL succeeded))completionHandler NS_AVAILABLE_IOS(9_0) {
[_lifeCycleDelegate application:application
performActionForShortcutItem:shortcutItem
completionHandler:completionHandler];
}
- (void)application:(UIApplication*)application
handleEventsForBackgroundURLSession:(nonnull NSString*)identifier
completionHandler:(nonnull void (^)(void))completionHandler {
[_lifeCycleDelegate application:application
handleEventsForBackgroundURLSession:identifier
completionHandler:completionHandler];
}
- (void)application:(UIApplication*)application
performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
[_lifeCycleDelegate application:application performFetchWithCompletionHandler:completionHandler];
}
- (void)addApplicationLifeCycleDelegate:(NSObject<FlutterPlugin>*)delegate {
[_lifeCycleDelegate addDelegate:delegate];
}
@end
并不是所有的协议都需要实现,各位可以根据自己工程都实际需要来实现协议方法即可
1.3 原生跳转Flutter 页面
#import <Flutter/Flutter.h>
#import "AppDelegate.h"
#import "ViewController.h"
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
// 点击事件
- (void)handleButtonAction {
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
FlutterViewController *flutterCon = [[FlutterViewController alloc] initWithEngine:appDelegate.flutterEngine nibName:nil bundle:nil];
[self.navigationController pushViewController:flutterCon animated:YES];
}
@end
上面的这段代码会可以将main.dart文件创建的视图绘制在flutterViewController中。
但是如果需要跳转到不同的页面该怎么办。
// 设置跳转的页面,实际上这个方法并不会生效
[flutterViewController setInitialRoute:@"route1"];
官方给的方式经过在下实践发现并不会生效,然后在Flutter的 issue#27216中找到了答案,虽然官方推荐在App中避免重复创建FlutterEngine来节省资源,但是目前Fluter对此方法并没有做出很好对支持,解决办法在issue#27216中也有提到,就是在每个ViewController中创建自己对Engine来进行管理。代码修正如下
- (void)handleButtonAction {
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
FlutterViewController *flutterCon = [[FlutterViewController alloc] initWithEngine:appDelegate.flutterEngine nibName:nil bundle:nil];
[GeneratedPluginRegistrant registerWithRegistry:flutterCon];
[flutterCon setInitialRoute:@"route1"];
[self.navigationController pushViewController:flutterCon animated:YES];
}
1.4 在main.dart中
在main.dart中我们只需稍作修改即可实现跳转不同对页面
import 'dart:ui';
import 'package:flutter/material.dart';
// window.defaulteRouteName 即为 setInitialRoute:传来对参数,只需要根据此参数来创建不同对widget即可
void main() => runApp(_widgetForRoute(window.defaultRouteName));
Widget _widgetForRoute(String route) {
switch (route) {
case 'route1':
return SomeWidget(...);
case 'route2':
return SomeOtherWidget(...);
default:
return Center(
child: Text('Unknown route: $route', textDirection: TextDirection.ltr),
);
}
}
至此已基本实现了iOS与Flutter的对接,但是flutter与native之间的页面跳转都由于FlutterEngine的存在遗留里一系列的问题,这方面闲鱼技术团队闲鱼开源 FlutterBoost:实现 Flutter 混合开发和京东团队 京东技术中台的 Flutter 实践之路都有比较深入的探究,在下暂且当个搬运工将闲鱼的FlutterBoost作为方案二搬运过来供各位借鉴一下。
二. FlutterBoost 闲鱼开源方案
FlutterBoost是闲鱼技术给出的原生接入Flutter的路由方案,总体思路是,引用原文“Native容器Container通过消息驱动Flutter页面容器Container,从而达到Native Container与Flutter Container的同步目的”。
下面通过代码来进一步理解这个过程。同样的也可以分为三步。
1.创建原生Router,暂且称为FLBRouter
#import <UIKit/UIKit.h>
#import <FLBPlatform.h>
NS_ASSUME_NONNULL_BEGIN
// 遵循协议FLBPlatform
@interface FLBRouter : NSObject<FLBPlatform>
@property (nonatomic, strong) UINavigationController *navigationController;
+ (instancetype)sharedRouter;
@end
#import "FLBRouter.h"
#import <FLBFlutterViewContainer.h>
@implementation FLBRouter
+ (instancetype)sharedRouter {
static FLBRouter *_shareRouter;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_shareRouter = [FLBRouter new];
});
return _shareRouter;
}
// 实现协议方法
- (void)close:(nonnull NSString *)uid result:(nonnull NSDictionary *)result exts:(nonnull NSDictionary *)exts completion:(nonnull void (^)(BOOL))completion {
FLBFlutterViewContainer *vc = (id)self.navigationController.presentedViewController;
if([vc isKindOfClass:FLBFlutterViewContainer.class] && [vc.uniqueIDString isEqual: uid]){
[vc dismissViewControllerAnimated:YES completion:^{}];
}else{
[self.navigationController popViewControllerAnimated:YES];
}
}
- (void)open:(nonnull NSString *)url urlParams:(nonnull NSDictionary *)urlParams exts:(nonnull NSDictionary *)exts completion:(nonnull void (^)(BOOL))completion {
if([urlParams[@"present"] boolValue]){
FLBFlutterViewContainer *vc = FLBFlutterViewContainer.new;
[vc setName:url params:urlParams];
[self.navigationController presentViewController:vc animated:YES completion:^{}];
}else{
FLBFlutterViewContainer *vc = FLBFlutterViewContainer.new;
[vc setName:url params:urlParams];
[self.navigationController pushViewController:vc animated:YES];
}
}
@end
2.在AppDelelgate中,直接继承FLBFlutterAppDelegate
#import <UIKit/UIKit.h>
#import <FLBFlutterAppDelegate.h>
@interface AppDelegate : FLBFlutterAppDelegate <UIApplicationDelegate>
@end
#import "AppDelegate.h"
#import "ViewController.h"
#import "FLBRouter.h"
#import <FlutterBoostPlugin.h>
@interface AppDelegate ()
@end
@implementation AppDelegate
// This override can be omitted if you do not have any Flutter Plugins.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.window.backgroundColor = [UIColor whiteColor];
UINavigationController *navigationCon = [[UINavigationController alloc] initWithRootViewController:[ViewController new]];
self.window.rootViewController = navigationCon;
[self.window makeKeyAndVisible];
FLBRouter *router = [FLBRouter sharedRouter];
// 这个navigationCon可以看做原生的Contianer
router.navigationController = navigationCon;
[FlutterBoostPlugin.sharedInstance startFlutterWithPlatform:router
onStart:^(id<FlutterBinaryMessenger,FlutterTextureRegistry,FlutterPluginRegistry> engine) {
}];
return YES;
}
// 这里省略了其他生命周期
...
@end
- 页面添加跳转方法
- (void)onClickAction {
[FLBRouter.sharedRouter open:@"first"
urlParams:@{@"present" : @(YES)}
exts:@{@"animated" : @(YES)}
completion:^(BOOL finished) {
}];
}
至此原生iOS端就可以进行跳转了,下面看Flutter怎样同步。
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
FlutterBoost.singleton.registerPageBuilders({
/// 根据不同的信息来创建不同的Wdiget
'first': (pageName, params, _) => _firstRouteWidgte(),
///可以在native层通过 getContainerParams 来传递参数
'flutterPage': (pageName, params, _) {
print("flutterPage params:$params");
return FlutterRouteWidget();
},
});
}
Widget _firstRouteWidgte() {
return Text('FirstPage');
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Boost example',
builder: FlutterBoost.init(postPush: _onRoutePushed),
home: Container());
}
void _onRoutePushed(
String pageName, String uniqueId, Map params, Route route, Future _) {
}
}
完成!