iOS接入Flutter填坑记录-代码篇

在上一篇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
  1. 页面添加跳转方法
- (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 _) {
  }
}

完成!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容