Flutter混编(三)

Flutter 混编-iOS

配置

一、 cd APP/ (进入workspace所在目录)
二、 flutter create -t module my_flutter (my_flutter为flutter模块文件夹名,可自行定义,在后边配置flutter路径的时候需要用到)
三、 在 podfile 中添加如下代码,将 flutter 模块引入到工程

flutter_application_path = 'path/to/flutter_app/'
  eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)
注意:正确填写路径
番外:
1、podfile 由 Ruby 编写。
2、上述代码的作用,是在当前上下文中,执行引入文件中的 Ruby 代码
3、引入的 Flutter 模块先的 podhelper.rb 文件,在
   每次 执行 flutter packages get 之后,都会被重写。
4、podhelper.rb 文件文件中包含如下代码,如果在当前 iOS 工程 podfile 中包含 post_install 会导致 duplicate 错误,由于上述第三条原因,最佳的解决方案,是将 podhelper.rb 文件相关核心代码合并到当前文件。 
post_install do |installer|
    installer.pods_project.targets.each do |target|
        target.build_configurations.each do |config|
            config.build_settings['ENABLE_BITCODE'] = 'NO'
            xcconfig_path = config.base_configuration_reference.real_path
            File.open(xcconfig_path, 'a+') do |file|
                file.puts "#include \"#{File.realpath(File.join(framework_dir, 'Generated.xcconfig'))}\""
            end
        end
    end
end

四、完成上述配置后,执行 pod install

注意:每次修改完 APP/my_flutter/pubspec.yaml 文件后
需要执行 flutter packages get ,Flutter 模块会更新依赖的模块,
这个时候你需要重新执行 pod install 来同步 Flutter 的更改。

五、为 Dart code 添加 build phase

TARGET->Build Phases->左上“+”->New Run Script Phase
添加如下脚本
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
具体位置如下图示

六、command + B build工程,完成后,Flutter 和 iOS工程混编的基础配置就完成了。
七、如果混编工程中,Flutter 模块用的了其他 plugin ,需要在添加如下代码
Objective-C

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

@interface AppDelegate : FlutterAppDelegate // Flutter 模块需要 hook 比如进程什么周期的方法
@end
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h> // Only if you have Flutter Plugins

#include "AppDelegate.h"

@implementation AppDelegate

// This override can be omitted if you do not have any Flutter Plugins.
- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  [GeneratedPluginRegistrant registerWithRegistry:self];
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

@end

Swift

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

@UIApplicationMain
class AppDelegate: FlutterAppDelegate {

  // Only if you have Flutter plugins.
  override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    GeneratedPluginRegistrant.register(with: self);
    return super.application(application, didFinishLaunchingWithOptions: launchOptions);
  }

}

八、示例
完成以上配置之后你就可以实现 iOS 和 Flutter 的混编了。
Objective-C

#import <Flutter/Flutter.h>
#import "ViewController.h"

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button addTarget:self
               action:@selector(handleButtonAction)
     forControlEvents:UIControlEventTouchUpInside];
    [button setTitle:@"Press me" forState:UIControlStateNormal];
    [button setBackgroundColor:[UIColor blueColor]];
    button.frame = CGRectMake(80.0, 210.0, 160.0, 40.0);
    [self.view addSubview:button];
}

- (void)handleButtonAction {
    FlutterViewController* flutterViewController = [[FlutterViewController alloc] init];
    [self presentViewController:flutterViewController animated:false completion:nil];
}
@end

Swift

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 flutterViewController = FlutterViewController()
    self.present(flutterViewController, animated: false, completion: nil)
  }
}

FlutterViewController,是 Flutter 模块的容器,同时也是一个完整的 UIController ,你可以在项目中任意组合,需要注意的是,用 Flutter 展示不同页面的时候,需要配置好路由。
Objective-C

[flutterViewController setInitialRoute:@"route1"];

Swift

flutterViewController.setInitialRoute("route1")

官方文档

Flutter 混编-Android

一、 cd APP/
二、 flutter create -t module my_flutter (my_flutter为flutter模块文件夹名,可自行定义,在后边配置flutter路径的时候需要用到)
三、在建立的Android工程的settings.gradle中加入以下代码:

setBinding(new Binding([gradle: this]))                                 // new
evaluate(new File(                                                      // new
  settingsDir.parentFile,                                               // new
  'my_flutter/.android/include_flutter.groovy'                          // new
))                                                                      // new
注意:settingsDir.parentFile表示当前目录的父级目录,
my_flutter是前面所建立的Flutter Module目录。

四、Sync now,会创建一个Flutter的library module
五、在Application Module的Build.gradle中依赖刚刚引入的library

implementation project(':flutter')

注意:如果由于 gradle 版本问题,导致某些方法找不到,请参考如下链接
[gradle 版本太低 issue]
(https://blog.csdn.net/qq_15653601/article/details/80236728)
六、至此,Android 和 Flutter 的混编配置,就完成了。
配置参考文档
七、 混编示例
在Android项目默认生成的MainActivity中,我们来展示一个页面,随后你可以在 main.dart 中修改页面布局。

        flutterView = Flutter.createView(
                MainActivity.this,
                getLifecycle(),
                "route"//传给 flutter 的参数,可用于打开指定页面
        );
        FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(500,800);
        layout.leftMargin = 100;
        layout.topMargin = 100;

        addContentView(flutterView, layout);

FlutterView不是唯一的使用方式,还有一种通过FlutterFragment来调用Flutter的代码方式,如下代码所示

FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.fl_flutter_view, Flutter.createFragment("route"));
fragmentTransaction.commit();
<FrameLayout
    android:id="@+id/fl_flutter_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

混编模式下的热重载

$ cd some/App/my_flutter
$ flutter attach
Waiting for a connection from Flutter on iPhone X...

执行上述命令后,打开安装在手机上的混编 APP ,进入到项目中的 Flutter 页面,即可与调试器建立连接,终端会输出如下信息

Done.
Syncing files to device iPhone X...                          4.7s

🔥  To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".
An Observatory debugger and profiler on iPhone X is available at: http://127.0.0.1:54741/
For a more detailed help message, press "h". To quit, press "q".

然后,你就可以编写 Dart 代码,使用热重载来调试 UI 了。

Flutter 与 Native 通信工具 —— Channel

关于 channel 的详细解析,请参考以下链接
闲鱼-深入理解Flutter Platform Channel
官方文档
闲鱼大佬对 Flutter Channel 做了深入剖析,我针对实战中的几个关键点做个简单总结。

Channel-Native 通信示意

Channel Name

​ 一个Flutter应用中可能存在多个Channel,每个Channel在创建时必须指定一个独一无二的name,用来绑定对应的Channel,Flutter和Native通信的时候,根据其传递过来的channel name找到该Channel对应的Handler。

Channel 架构

​ BinaryMessenger是Native端与Flutter端通信的接口对象,在Android端是一个接口,其具体实现为FlutterNativeView。而其在iOS端是一个协议,名称为FlutterBinaryMessenger,FlutterViewController遵循了它。

​ Binarymessenger并不知道Channel的存在,它只和BinaryMessageHandler打交道。而Channel和BinaryMessageHandler则是一一对应的。

Platform Channel是否线程安全

​ Platform Channel并非是线程安全的,这一点在官方的文档也有提及。Flutter Engine中多个组件是非线程安全的,故跟Flutter Engine的所有交互(接口调用)必须发生在Platform Thread。故我们在将Platform端的消息处理结果回传到Flutter端时,需要确保回调函数是在Platform Thread(也就是Android和iOS的主线程)中执行的。

是否支持大内存数据块的传递

​ Platform Channel实际上是支持大内存数据块的传递,当需要传递大内存数据块时,需要使用BasicMessageChannel以及BinaryCodec。而整个数据传递的过程中,唯一可能出现数据拷贝的位置为native二进制数据转化为Dart语言二进制数据。若二进制数据大于阈值时(目前阈值为1000byte)则不会拷贝数据,直接转化,否则拷贝一份再转化。

Channel 示例

官方示例

官方示例核心代码分析

Dart部分

// 方法 Channel ,平台侧会立即收到通道同步的信息,可用作方法调用
static const MethodChannel methodChannel =
      MethodChannel('samples.flutter.io/battery');
// 事件 Channel ,可类比为通知对象,可用于监听
  static const EventChannel eventChannel =
      EventChannel('samples.flutter.io/charging');

  // Dart 方法,触发methodChannel想Native同步信息
  Future<void> _getBatteryLevel() async {
    String batteryLevel;
    try {
      final int result = await methodChannel.invokeMethod('getBatteryLevel');
      batteryLevel = 'Battery level: $result%.';
    } on PlatformException {
      batteryLevel = 'Failed to get battery level.';
    }
    // 更新 UI
    setState(() {
      _batteryLevel = batteryLevel;
    });
  }

  @override
  void initState() {
    super.initState();
    // 注册 eventChannel
    eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError);
  }
  // eventChannel回调
  void _onEvent(Object event) {
    setState(() { /*更新 UI*/ });
  }
  // eventChannel回调
  void _onError(Object error) {
    setState(() { /*更新 UI*/ });
  }

Native(Swift)

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
     // FlutterMethodChannel
    let batteryChannel = FlutterMethodChannel(name: "samples.flutter.io/battery",
                                              binaryMessenger: controller)
    batteryChannel.setMethodCallHandler({
      (call: FlutterMethodCall, result: FlutterResult) -> Void in
          guard call.method == "getBatteryLevel" else {
          // 给Flutter端的回调
          result(FlutterMethodNotImplemented)
          return
        }
        self.receiveBatteryLevel(result: result)
    })
      // FlutterEventChannel
     let chargingChannel = FlutterEventChannel(name: ChannelName.charging,
                                                  binaryMessenger: binaryMessenger)
     chargingChannel.setStreamHandler(self)
  }
    // 监听方法
    public func onListen(withArguments arguments: Any?,
                         eventSink: @escaping FlutterEventSink) -> FlutterError? {
        self.eventSink = eventSink
        UIDevice.current.isBatteryMonitoringEnabled = true
        sendBatteryStateEvent()
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(self.onBatteryStateDidChange),
            name: NSNotification.Name.UIDeviceBatteryStateDidChange,
            object: nil)
        return nil
    }
     // 监听方法
    public func onCancel(withArguments arguments: Any?) -> FlutterError? {
        NotificationCenter.default.removeObserver(self)
        eventSink = nil
        return nil
    }

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

推荐阅读更多精彩内容