Flutter入坑实录(iOS篇)

入坑背景

作为一个从事p2p行业的iOS移动开发者,今年的行情可谓是灾难性的。公司裁员过半,在我极力要求被裁的前提下,我依然被留了下来,与公司共存亡。在这个大环境下,公司CTO提出了大前端。。。(人都没了还大前端~~)。大概就是搞跨平台开发,节约人员成本摆了。在RN,Weex,Flutter三者竞选中Flutter以大平台,高性能等优势胜出。下面进入正题。


安装Flutter

安装前建议你更新到最新的mac系统和xcode。
1.在MACOS操作系统下安装Flutter,这里直接跳转到官方下载页
你也可以去github下载
2.解压安装包到你想安装的目录,此路径后面会用到,此用用myPath代替
3.添加flutter相关工具到path中:

export PATH= myPath/bin:$PATH

4.运行 flutter doctor查看是否需要安装其它依赖项来完成安装::

flutter doctor

显示结果可能是这样的:


屏幕快照 2019-05-10 下午3.04.47.png

Android的可以先不管,按照给出的解决方法在终端输入命令。这个过程可能会遇到xcode,mac系统版本过低,你需要更新后重新操作。最终结果如下图:
屏幕快照 2019-05-13 下午5.41.56.png

这里是我安装了Android Studio等环境后的最终结果,后面编辑dart文件会用到Android Studio。你也可以用其他编辑器来做,但Android Studio编写更加友好。
到这里Flutter安装配置已经完成。


创建第一个Flutter工程

如果你还没安装Android Studio,你可以通过命令行来安装:
使用 flutter create 命令创建一个project:

flutter create myapp
cd myapp

上述命令创建一个项目名为myapp,其中包含一个使用Material 组件的简单演示应用程序。
此时你可以进入myapp内iOS文件下通过xcode打开Runner.xcworkspace文件。在 lib/main.dart文件下编写代码。
跑起来是这个样子:

屏幕快照 2019-05-13 下午6.07.45.png

体验热重载

Flutter 可以通过 热重载(hot reload) 实现快速的开发周期,热重载就是无需重启应用程序就能实时加载修改后的代码,并且不会丢失状态。Flutter暂时并不支持热更新可参考官方Flutter
1.打开文件lib/main.dart
2.将字符串
'You have pushed the button this many times:' 随便更改后,不要按“停止”按钮;,让您的应用继续运行.
3.要查看您的更改,请调用 Save (cmd-s / ctrl-s), 或者在Android Studio中点击 热重载按钮 (带有闪电图标的按钮).
你会立即在运行的应用程序中看到更新的字符串。
这样一个简单的Flutter工程就跑去来了。


Flutter混编

上面讲到的是从零开始如何创建Flutter工程。如果已经有了OC或者Swift写的工程后,如何集成进去Flutter进行混编呢?

创建Flutter module

首先我们要创建一个Flutter module(my_flutter)放在和你已存在工程的同级目录下:

 cd some/path/
 flutter create -t module my_flutter

这将创建一个带有Dart代码的Flutter模块项目,以及一个隐藏的.ios/ 子文件夹,该子文件夹包装了包含一些cocoapod和一个Ruby脚本的模块项目。
打开lib/main.dart文件如图:


屏幕快照 2019-05-14 上午9.33.21.png

这是自动帮我们生成的代码,应该和你上面提到Runner工程是一样的。下面我们会修改这个模版实行Native和Flutter的交互。


Native工程配置

集成Flutter module工程到Native需要Cocoapods依赖项管理器,请确保本地安装了cocoapods,如果未安装,可以参考:cocoapods.org/
默认你当前项目已经集成cocoapods,请将下列配置添加到工程的Podfile文件中。

flutter_application_path = 'some/path/my_flutter/'
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)

香这个样子:

platform :ios, '9.0'
use_frameworks!

target 'native_Project' do
  flutter_application_path = 'some/path/my_flutter/'
  eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)
end

配置完成后,执行:

pod install

确保Flutter.framework安装成功。
此时使用Xcode打开native_Project.xcworkspace文件。
进行如下配置:
1.禁用bitcode,因为Flutter现在不支持bitcode,需要禁用项目TARGETS的Build Settings-> Build Options-> Enable Bitcode部分中的ENABLE_BITCODE标志。

2.找到项目TARGETS的Build Phases,点击左上角+号选择New Run Script Phase添加Run Script,在Shell字段下添加下面两行脚本:


屏幕快照 2019-05-13 下午4.16.27.png

执行⌘B构建一下项目。你会在工程里看到多了个Development Pods文件:


屏幕快照 2019-05-13 下午4.20.40.png

到此为止项目配置搞定。

原生与Flutter交互

OC工程修改:

首先进入native_Project的AppDelegate.h,引入Flutter头文件,并把AppDelegate改为继承自FlutterAppDelegate。并在头文件中定义FlutterEngine变量供后续使用:

#import <UIKit/UIKit.h>
#import <Flutter/Flutter.h>

@interface AppDelegate : FlutterAppDelegate
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end

在AppDelegate.m文件中的完成应用启动的生命周期函数中实现flutterEngine

#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 {

/**
项目之前代码保留就行,(指定window根控制器等等。。)只需添加下面代码
*/
  self.flutterEngine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil];
  [self.flutterEngine runWithEntrypoint:nil];
  [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

@end

接下来你可以在原生任意一个地方定义一事件点击跳转到Flutter页面。点击事件实行如下:

- (void)jumpFlutterAction {
    FlutterEngine *flutterEngine = [(AppDelegate *)[[UIApplication sharedApplication] delegate] flutterEngine];
    FlutterViewController *flutterViewController = [[FlutterViewController alloc] initWithEngine:flutterEngine nibName:nil bundle:nil];
    [self presentViewController:flutterViewController animated:false completion:nil];
}

Swift工程修改:

修改AppDelegate.swift:

import UIKit
import Flutter
import FlutterPluginRegistrant // Only if you have Flutter Plugins.

@UIApplicationMain
class AppDelegate: FlutterAppDelegate {
  var flutterEngine : FlutterEngine?;
  // Only if you have Flutter plugins.
  override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    self.flutterEngine = FlutterEngine(name: "io.flutter", project: nil);
    self.flutterEngine?.run(withEntrypoint: nil);
    GeneratedPluginRegistrant.register(with: self.flutterEngine);
    return super.application(application, didFinishLaunchingWithOptions: launchOptions);
  }

}

点击跳转事件:

  @objc func handleButtonAction() {
    let flutterEngine = (UIApplication.shared.delegate as? AppDelegate)?.flutterEngine;
    let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)!;
    self.present(flutterViewController, animated: false, completion: nil)
  }

此时基本的原生跳转Flutter已经完成,你可通过点击事件跳转到Flutter默认界面,但是你会发现你无法返回到原生了,因为你还没做Flutter回调原生的交互。

设置route实现相互多样化

我们需要在原生工程指定route

  • OC:
[flutterViewController setInitialRoute:@"route1"];
  • Swift:
flutterViewController.setInitialRoute("route1")

在main.dart文件支持route:


屏幕快照 2019-05-14 上午10.29.32.png

你需要引入对应的头文件支持,通过指定route1或者route2来呈现不同的Flutter页面。
此时你运行后会发现跳转到了Unknown route黑屏页面,也就是进入了dart中的switch语句default内

default:
      return Center(
        child: Text('Unknown route: $route', textDirection: TextDirection.ltr)

官方解释如下:在AppDelegate初始化flutterEngine后,立即调用了[self.flutterEngine runWithEntrypoint:nil],这句代码是创建Flutter engine环境并启动引擎,这时候其实已经执行了main.dart中的main方法,此时window.defaultRouteName为空,所以展示了上面default分支的Widget,后边创建FlutterViewController后设置的routeName是起不到作用的。
解决如下:
我们可以使用FlutterViewController自己创建的FlutterEngine而不去自己创建,这样在按钮点击跳转事件处理时执行如下代码:

  • OC
    FlutterViewController *flutterViewController = [[FlutterViewController alloc] init];
    [flutterViewController setInitialRoute:@"route1"];
    FlutterMethodChannel* methodChannel = [FlutterMethodChannel
                                           methodChannelWithName:@"com.flutterbus/demo"
                                           binaryMessenger:flutterViewController];
    
    [methodChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
        if ([@"iOSFlutterMessage" isEqualToString:call.method]) {
            OCTMessageViewController *nativeViewController = [[OCTMessageViewController alloc] init];
            [flutterViewController.navigationController pushViewController:nativeViewController animated:NO];
            result(@YES);
        } if([@"iOSFlutter" isEqualToString:call.method]) {
            [flutterViewController.navigationController popViewControllerAnimated:NO];
            result(@YES);
        } else {
            result(FlutterMethodNotImplemented);
        }
    }];
    [self.navigationController pushViewController:flutterViewController animated:NO];
  • Swift
    @objc func btnAction(btn:UIButton){
        let flutterViewController = FlutterViewController();
        flutterViewController.hidesBottomBarWhenPushed = true
//设置首次进入Flutter背景色,否则会出现黑屏。
        flutterViewController.view.backgroundColor = UIColor.white
        flutterViewController.setInitialRoute("route1")
        let methodChannel = FlutterMethodChannel(name: "com.flutterbus/demo", binaryMessenger: flutterViewController)
        methodChannel.setMethodCallHandler { (call, result) in
            print(call.method)
            if ("iOSFlutterVideo" == call.method){
               let vide =  UIViewController()
                vide.view.backgroundColor = UIColor.red
flutterViewController.navigationController?.pushViewController(vide, animated: false)
            return
            }else if ("iOSFlutter" == call.method){
                flutterViewController.navigationController?.popViewController(animated: false)
                return
            }
        }
        self.rootVc?.navigationController?.pushViewController(flutterViewController, animated: true)


    }

其中FlutterMethodChannel是提供接受Flutter回调的信息处理Block,我们可以在这里完成Flutter和原生的交互。
对应的dart文件代码如下:

屏幕快照 2019-05-14 上午10.44.39.png

主要就是创建一个给native的MethodChannel (类似iOS的通知),标示为“com.flutterbus/demo”创建两个按钮FlatButton,实现按钮的点击事件_iOSPushToVC,_iOSPushToVC1,并传递方法名和参数供原生拦截使用。
目前简述不能上传视频,最终效果就没法演示了贴几张图吧:
CA923111-D4BA-4041-A6C7-F5D31AC6D958 2.png

点击进入镶嵌导航控制器的Flutter界面:
040D0B3B-1BE9-43D6-9479-02AA8C22BC28.png

点击回首页或去音频可以跳转到对应原生界面。
到期Flutter的基础学习基本完成,剩下的就是学习dart语法,写出漂亮的flutter界面了。

总结

经过学习Flutter进行混编,在编写dart文件时由于支持热更新,无需编译开发效率不错,但如果你修改了原生工程,再重新编译时明显发现集成如Flutter之后编译时间长了很多。而且发现由原生跳转Flutter页面时会出现明显的闪屏,过渡不是很友好目前还没得到解决。其次上面提供的混编教材来自官方,此方案有一巨大的缺点,就是在原生和Flutter页面叠加跳转时内存不断增大,因为FlutterView和FlutterViewController每次跳转都会新建一个对象,从而Embedder层的AndroidShellHolder和FlutterEngine都会创建新对象,UI Thread、IO Thread、GPU Thread和Shell都创建新的对象,唯独共享的只有DartVM对象,但是RootIsolate也是独立的,所以Flutter页面之前的数据不能共享,这样就很难将一些全局性的公用数据保存在Flutter中,所以这套方案比较适合开发不带有共享数据的独立页面,但是页面又不能太多,因为创建的Flutter页面越多内存就会暴增,尤其是在iOS上还有内存泄露的问题。因此我觉得Flutter还有很长的路要走,但毕竟时谷歌推崇的新星,我还是对Flutter充满了期待。作为一个iOS开发者,我总在幻想苹果何时能推出个跨平台方案来,我想这也就是幻想吧,毕竟苹果粑粑太“保守”了。但幻想还是要有的,万一实现了呢!!!
针对上面官方混编教材的缺陷,阿里团队提出了自己优化方案 单引擎的方案可以参考下。
参考文献:
官方Flutter
Add Flutter to existing apps
Flutter编程指南

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

推荐阅读更多精彩内容