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編程指南

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容