Flutter && Flutter_Boost 之 iOS 混编开发

Mac系统Flutter环境集成

使用镜像

由于在国内访问Flutter有时可能会受到限制,Flutter官方为中国开发者搭建了临时镜像,可以将如下环境变量加入到用户环境变量中:

export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

获取Flutter SDK

官网下载安装包:https://flutter.io/sdk-archive/#macos
解压后拷贝安装到想安装的目录(安装到哪里都可以,但是后面需要将这个安装路径添加到环境变量中)
我的安装目录(装在这里不是很好,但是添加了环境变量,就先不动了):

/Users/HUANGXIAO/flutter

添加flutter相关工具到环境变量中:

cd /Users/HUANGXIAO/flutter
export PATH=`pwd`/flutter/bin:$PATH

现在只是设置了临时环境变量,长期使用需要将其设置为永久的环境变量。
在家目录打开 .bash_profile 文件:

cd ~
open -e .bash_profile

配置环境变量:
添加flutter安装目录到path中,使用命令添加或者直接编辑.bash_profile

export PATH='你的安装目录'/bin:$PATH

我的环境变量如下

export JAVA_HOME=$(/usr/libexec/java_home)

export ANDROID_HOME="/Users/HUANGXIAO/Library/Android/sdk"
export PATH=${PATH}:${ANDROID_HOME}/tools
export PATH=${PATH}:${ANDROID_HOME}/platform-tools

export PUB_HOSTED_URL=https://pub.flutter-io.cn 
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn 
export PATH=/Users/HUANGXIAO/flutter/bin:$PATH

[[ -s "$HOME/.rvm/scripts/rvm" ]] && source "$HOME/.rvm/scripts/rvm" # Load RVM into a shell session *as a function*

更新环境变量:

source .bash_profile

验证目录是否在已经在PATH中:

echo $PATH
1563505094101.jpg

到此Flutter 已安装好,并导入到环境变量中。

安装Flutter依赖

终端输入:

flutter doctor

根据提示安装相应的依赖软件,比如 Android Studio、XCode、VSCode等,并安装相应的Flutter插件。
可参照Flutter中文网教程安装:https://flutterchina.club/setup-macos/

这一步可能会遇到一些问题,这篇文章总结得比较全面,可以参考,在此感谢!
https://www.jianshu.com/p/603649a02956

最终安装完后会全部是√,说明环境和依赖已经OK了,接下来就可以进行开发了。


image2019-4-10_16-44-52.png

iOS 项目集成Flutter编译环境

Flutter 与 原生项目 混编有两种方案:

1. 自动创建Android和iOS项目

如果项目之初就已经决定使用Flutter与Native混编方案,那么可以直接用Flutter生成项目,其中会自动生成iOS和Android相对应的原生项目。这也是比较简单而且高效的混编方案。
Andriod Studio 创建:
File → New → New Flutter Project
注意选择混编是Android和iOS的开发语言:


image.png

命令创建:
同样注意选择混编是Android和iOS的开发语言:

flutter create -i swift -a kotlin Name

创建好后的目录结构:
android为Android项目文件,ios为iOS项目文件。


image.png

这样就创建好混编项目,以iOS为例,用Xcode打开Runner.xcworkspace项目,可以看到:


image.png

Flutter已自动将Flutter与iOS代码集成好了。可以根据需要修改项目配置,如Display Name, Bundle Id 等。
iOS项目自动采用cocopod集成第三方库。

2. iOS老项目集成Flutter

官方指导:https://github.com/flutter/flutter/wiki/Add-Flutter-to-existing-apps

如果已经有了iOS项目,需要Flutter与iOS混编。
创建Flutter module,请确保安卓、iOS、Flutter三个项目的根目录必须在同一目录下:

flutter create -t module flutter_module

如果iOS使用swift语言,请加上 -i swift
安卓、iOS、Flutter三个项目的根目录必须在同一目录下:


image.png
  • iOS项目使用cocoapods管理第三方依赖包,并在Podfile加入如图以下代码,注意Flutter项目路径:
flutter_application_path = 'path/to/my_flutter/'
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)
image.png

打开Flutter项目 , 获取包:

flutter packages get

然后,将Flutter跑一遍,已确保Flutter生成iOS项目集成需要的依赖产物:

flutter build ios

Flutter项目运行成功后,iOS项目执行:

pod install

成功后可以在pods - Development Pods下看到Flutter 安装的包:


image.png
  • iOS项目Enable Bitcode改为NO


    image.png
  • iOS项目添加脚本,注意脚本位置要放在check pods manifest.lock之后:
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
image.png

如果一切顺利,编译iOS项目⌘B编译应该会成功。此时iOS项目Flutter编译环境已集成完成。

iOS Flutter混编

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

}

Native跳转Flutter:

官方方法:

import UIKit
import Flutter

class ViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    let button = UIButton(type:UIButtonType.custom)
    button.addTarget(self, action: #selector(handleButtonAction), for: .touchUpInside)
    button.setTitle("Press me", for: UIControlState.normal)
    button.frame = CGRect(x: 80.0, y: 210.0, width: 160.0, height: 40.0)
    button.backgroundColor = UIColor.blue
    self.view.addSubview(button)
  }

  @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路由:

flutterViewController.setInitialRoute("route1")

在我实际集成的时候,使用flutterEngine是没办法指定跳转的路由的,也就是flutterViewController.setInitialRoute("route1")无效!无效!无效!
如果不使用flutterEngine,直接初始化FlutterViewController并指定路由跳转是能够成功跳转相应路由的!但是,不使用flutterEngine会导致FlutterViewController()关闭是内存无法释放!内存无法释放!内存无法释放!

let flutterViewController = FlutterViewController()
flutterViewController.setInitialRoute("route1")
self.present(flutterViewController, animated: false, completion: nil)

这是Flutter官方最大的bug,也是最大的坑!
只有等待谷歌官方后续解决吧。

寻求解决办法:

1.使用消息传递机制跳转页面
iOS代码:

// 带默认返回操作Fluttervc
    class func createFluttervcWithDefaultHandler(paramStr:String) -> (BaseFlutterViewController) {
        let engine = HX_AppDelegate.flutterEngine
        let messageChannel = HX_AppDelegate.messageChannel
        
        let flutterVC = BaseFlutterViewController(engine: engine, nibName: nil, bundle: nil)!
        let channelName = "com.novasoftware.ShoppingMall.address"
        let channel = FlutterMethodChannel(name: channelName, binaryMessenger: flutterVC)
        channel.setMethodCallHandler { (call: FlutterMethodCall, result: FlutterResult) in
            print(call.method)
            if call.method == "back" {
                flutterVC.dismiss(animated: true, completion: nil)
            }
        }
        print("Native:" + paramStr)
        engine!.navigationChannel.invokeMethod("", arguments: paramStr)
        messageChannel!.sendMessage(paramStr)  // 发送消息

        return flutterVC
    }
    
    // 自定义handler的Fluttervc
    class func createFluttervcWithHandler(paramStr:String, handler:@escaping FlutterMethodCallHandler) -> (BaseFlutterViewController) {
        let engine = HX_AppDelegate.flutterEngine
        let messageChannel = HX_AppDelegate.messageChannel
        
        let flutterVC = BaseFlutterViewController(engine: engine, nibName: nil, bundle: nil)!
        let channelName = "com.novasoftware.ShoppingMall.address"
        let channel = FlutterMethodChannel(name: channelName, binaryMessenger: flutterVC)
        channel.setMethodCallHandler(handler)
        print("Native:" + paramStr)
        engine!.navigationChannel.invokeMethod("", arguments: paramStr)
        messageChannel!.sendMessage(paramStr)    // 发送消息
        return flutterVC
    }

Native页面跳转Flutter指定页面,并带参数传递:

        var flutterVC = BaseFlutterViewController()
        let handler : FlutterMethodCallHandler = { (call: FlutterMethodCall, result: FlutterResult) in
            print(call.method)
            if call.method == "back" {
                // 消息交互
                flutterVC.dismiss(animated: true, completion: nil)
            }
        }

        let paramStr = "MyCoupon?" + (SM_token ?? "")
        flutterVC = HXHelper.createFluttervcWithHandler(paramStr: paramStr, handler: handler)
        self.viewContainingController()?.present(flutterVC, animated: true, completion: nil)

Flutter代码:

const String _kReloadChannelName = 'reload';
const BasicMessageChannel<String> _kReloadChannel =
    BasicMessageChannel<String>(_kReloadChannelName, StringCodec());

void main() {
  _kReloadChannel.setMessageHandler(run);
  print("Flutter---" + ui.window.defaultRouteName);
  run(ui.window.defaultRouteName);
}

Future<String> run(String route) async {
  print("Flutter---" + route);
  switch (_getPageName(route)) {
    case 'address':
      String param = _getPageParamJsonStr(route);
      AddressParamEntity entity =
          AddressParamEntity.fromJson(json.decode(param));
      runApp(AddressPage1(
        entity: entity,
      ));
      break;
    case 'ReceiveCoupon':
      runApp(CouponPage(token: _getPageParamJsonStr(route)));
      break;
    case 'MyCoupon':
      runApp(MyCouponPage(token: _getPageParamJsonStr(route)));
      break;
    default:

      break;
  }
  return '';
}

String _getPageName(String s) {
  if (s.indexOf("?") == -1) {
    return s;
  } else {
    return s.substring(0, s.indexOf("?"));
  }
}

String _getPageParamJsonStr(String s) {
  if (s.indexOf("?") == -1) {
    return "";
  } else {
    return s.substring(s.indexOf("?") + 1);
  }
}

int getAddressId(String s) {
  return int.parse(s.substring(0, s.indexOf(",")));
}

String getAddressToken(String s) {
  return s.substring(s.indexOf(",") + 1);
}

这样能使Native跳转到Flutter相应的页面,并传递参数。
并且可以使用消息传递机制做一些交互:

static const  platform = const MethodChannel('com.novasoftware.ShoppingMall.address');
  Future<Null> back() async {
    try {
      await platform.invokeMethod('back');
    } on PlatformException catch (e) {
    }
  }
var flutterVC = BaseFlutterViewController()
let handler : FlutterMethodCallHandler = { (call: FlutterMethodCall, result: FlutterResult) in
    print(call.method)
    if call.method == "back" {
        flutterVC.dismiss(animated: true, completion: nil)
    }
}

let paramStr = "ReceiveCoupon?" + (SM_token ?? "")
flutterVC = HXHelper.createFluttervcWithHandler(paramStr: paramStr, handler: handler)
self.present(flutterVC, animated: true, completion: nil)

这样测试了一段时间,本以为就此搞定。没想到后面页面出现乱码,视图出现马赛克,等等一些问题。
可以确定,是Flutter内存泄漏导致的,这样做还是有个问题,同一个页面关闭后内存并没有释放,只是第二次打开这个页面的时候,不会重新申请内存新建页面,也就是同一个Flutter页面不会多次创建,但是也不会释放。比如,第二次打开这个页面,上次在输入框输入的文本并没有清空,还显示在那里,而且,连光标都没有释放!

这种办法行不通,另外寻求解决办法。
后面发现阿里巴巴的闲鱼团队在使用Flutter框架,并且有了很多成功的案例。
最重要的,他们还开源了一套Flutter Native混合开发的框架Flutter Boost!

集成Flutter Boost

在对应的pubspec.yaml文件中加入依赖

flutter_boost: ^0.0.415
image.png

之后调用:

flutter packages get

执行:

flutter build ios

在iOS的根目录下执行:

pod install

使iOS和flutter都添加FlutterBoost插件。


image.png
  • Dart 代码集成
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({
      'Address': (pageName, params, _) {
        print(params);
        AddressParamEntity entity =
        AddressParamEntity.fromJson(json.decode(params["object"]));
        return AddressPage1(
            entity: entity
        );
      }, // 页面1

      'ReceiveCoupon': (pageName, params, _) {
        print(params);
        return CouponPage(
            token: params["token"]
        );
      }, // 页面2

      'MyCoupon': (pageName, params, _) {
        print(params);
        return MyCouponPage(
            token: params["token"]
        );
      }// 页面3
    );
    FlutterBoost.handleOnStartPage();
  }

  Map<String, WidgetBuilder> routes = {
    "second": (BuildContext context) =>
        MyCoupon(token: "")
  };

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Boost example',
        builder: FlutterBoost.init(postPush: _onRoutePushed),
        routes: routes,
        home: Container());
  }

  void _onRoutePushed(
      String pageName, String uniqueId, Map params, Route route, Future _) {
  }

}
  • iOS代码集成
    需要 将libc++ 加入 "Linked Frameworks and Libraries"
    这个主要是项目的General 的Linked Frameworks and Libraries 栏下,点击加号(+)搜索libc++,找到libc++.tbd即可。


    image.png

修改AppDelegate.swift

import UIKit
import CoreData
import Flutter
import FlutterPluginRegistrant
import flutter_boost


@UIApplicationMain
class AppDelegate: FLBFlutterAppDelegate {

    var flutterEngine : FlutterEngine?
    var messageChannel : FlutterBasicMessageChannel?
    
    override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.

        self.setupIQKeyBoardManager()
        
        if (HX_Defaults_Standard.string(forKey: DEFAULT_TOKEN) == nil) {
            let login = R.storyboard.login().instantiateInitialViewController()
            self.window?.rootViewController = login
        } else {
            let tabbar = BaseTabBarController()
            self.window?.rootViewController = tabbar
        }
        
        // ------------------------
        let router = HXFlutterRouter.sharedRouter
        router.navigationController = self.window?.rootViewController?.navigationController
        // 初始化FlutterBoost,也可以在其他地方做初始化
        FlutterBoostPlugin.sharedInstance()?.startFlutter(with: router, onStart: { (flutterVC) in
            // 或许这里需要些什么代码,让Flutter能跳转Native页面,还需要研究
        })
        // ------------------------
        
        self.window?.backgroundColor = UIColor.white
        self.window?.makeKeyAndVisible()

        return super.application(application, didFinishLaunchingWithOptions: launchOptions);
        
    }
    ....
}

实现FLBPlatform协议

import UIKit

class HXFlutterRouter : NSObject, FLBPlatform{
    
    var navigationController: UINavigationController?
    static let sharedRouter = HXFlutterRouter()
    let accessibilityEnable = true
    
    func openPage(_ name: String, params: [AnyHashable : Any], animated: Bool, completion: @escaping (Bool) -> Void) {
        if let present = params["present"] as? Bool {
            if present {
                let vc = FLBFlutterViewContainer()
                vc.setName(name, params: params)
                navigationController?.present(vc, animated: animated, completion: {
                    completion(true)
                })
                return
            }
        }
       
        let vc = FLBFlutterViewContainer()
        vc.setName(name, params: params)
        navigationController?.pushViewController(vc, animated: animated)
        completion(true)
    }
    
    func closePage(_ uid: String, animated: Bool, params: [AnyHashable : Any], completion: @escaping (Bool) -> Void) {
        if let vc = navigationController?.presentedViewController as? FLBFlutterViewContainer {
            if vc.isKind(of: FLBFlutterViewContainer.self) && vc.uniqueIDString == uid {
                vc.dismiss(animated: animated) { }
                return
            }
        }
        navigationController?.popViewController(animated: animated)
    }
    
    func flutterCanPop(_ canpop: Bool) {
        navigationController?.interactivePopGestureRecognizer?.isEnabled = canpop
    }
    
}

其中的openPage 方法会接收来至flutter-->native以及native-->flutter的页面跳转,可以根据需求书写。

Native 跳转 Dart

let paramsTem : [String : Any] = ["id": self.entity.OrderId,
                                              "orderNumber": self.entity.CustomOrderNumber ?? "",
                                              "token": SM_token as Any]
//根据与Dart代码约定的参数传递方式,转成json字符串,当然也可以约定直接用Dictionary。
let params = ["object": HXHelper.convertDictionaryToString(dict: paramsTem)] 
            
HXFlutterRouter.sharedRouter.navigationController = self.viewContainingController()!.navigationController
let flutterVC = FLBFlutterViewContainer()
flutterVC.setName("AfterSellPage", params: params)
self.viewContainingController()!.present(flutterVC, animated: true, completion: nil)

Dart 与 Native 交互

同样可以使用消息传递机制使Dart与Native交互

static const  platform = const MethodChannel('com.novasoftware.ShoppingMall.address');
  Future<Null> use() async {
    try {
      // Dart 传递“Use”消息给 Native
      await platform.invokeMethod('use');
    } on PlatformException catch (e) {
    }
  }
HXFlutterRouter.sharedRouter.navigationController = self.navigationController
let flutterVC = FLBFlutterViewContainer()
flutterVC.setName("MyCoupon", params: ["token": SM_token as Any])

if let flutterVc = FlutterBoostPlugin.sharedInstance()?.currentViewController() {
    let channelName = "com.novasoftware.ShoppingMall.address"
    let channel = FlutterMethodChannel(name: channelName, binaryMessenger:flutterVc)
    channel.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: FlutterResult) in
        print(call.method)
        if call.method == "use" {
            // Native接收消息
            flutterVC.dismiss(animated: true, completion: {
                self?.tabBarController?.selectedIndex = 0
            })
        }
    }
}
self.present(flutterVC, animated: true, completion: nil)

问题:

  1. 打开一个dart页面后,侧滑返回手势失效

打开一个dart页面返回后。发现侧滑返回手势失效了。
打断点调试,发现打push一个Dart页面后,

interactivePopGestureRecognizer?.isEnabled == false.
image

应该是flutter boost打开一个Dart页面后,将其设置未false了。检测发现FLBPlatform协议有控制方法。

image

实现该方法,返回true即可。

funcflutterCanPop(_canpop:Bool) {
    navigationController?.interactivePopGestureRecognizer?.isEnabled = true
}

总结:

到此Flutter Boost初步集成。中间踩了太多坑,当然还有很多坑还没跳出来。
目前使用发现还是有内存泄露,不过已经比直接使用好用很多。
还是非常感谢闲鱼团队。

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

推荐阅读更多精彩内容