[混編] iOS原生專案- 接入Flutter

路過看風景發表於2021-03-26

1. 

  • Flutter Application: Flutter應用                 包含標準的Dart層與Native平臺層
  • Flutter Module :          Flutter與原生混合開發
  • Flutter Plugin:           Flutter外掛
  • Flutter Package:       純Dart元件

還是有點不夠清楚

Flutter Application  建立的是純flutterApp
Flutter Plugin          用於為flutterAPP建立三方或者工具外掛

Flutter Module 用於建立安卓ios平臺原生上的元件 flutter  Module

Flutter Package   純Dart元件 無平臺區分的

2. iOS原生App如何接入flutter作為部分功能模組「flutter官方方案」

簡單說兩種:

1. 建立flutter module,通過CocoaPods整合到xcode專案裡

    生成兩個framework,Flutter.framework 和 App.framework

2. 建立flutter module,編譯後 手動引入xcode改成 Framework的動態庫

一般專案都整合有cocoapods 所以都選擇1;

方式1具體如何
1. 生成flutter module

cd some/path/
flutter create --template module my_flutterm


目錄結構
複製程式碼

my_flutter/

├── .ios/
│   ├── Runner.xcworkspace
│   └── Flutter/podhelper.rb
├── lib/
│   └── main.dart
├── test/
└── pubspec.yaml
複製程式碼

2. 

  1. Add the following lines to your Podfile:

    content_copy

    flutter_application_path = '../my_flutter'   <-相對路徑具體,我是同級目錄
    load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
    複製程式碼
  2. For each Podfile target that needs to embed Flutter, call install_all_flutter_pods(flutter_application_path).

    content_copy

    target 'MyApp' do
      install_all_flutter_pods(flutter_application_path)
    end
    複製程式碼
  3. Run pod install.

3. 程式碼層面實現方式1 啟動即啟動FlutterEngine

import Flutterimport FlutterPluginRegistrant@main
//這裡FlutterAppDelegate 在 UIResponder <UIApplicationDelegate>前面加了一層 //用於自己的接收系統事件,完成自定實現 class AppDelegate: FlutterAppDelegate {    lazy var flutterEngine = FlutterEngine(name: "my flutter engine")    override func application(_ application: UIApplication, didFinishLaunchingWithOptions 
                launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {        flutterEngine.run();        GeneratedPluginRegistrant.register(with: self.flutterEngine);        return super.application(application, didFinishLaunchingWithOptions: launchOptions);    }}

@IBAction func openFlutterPage(_ sender: Any) { 
//展示flutter main 裡面的頁面  let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine  let flutterViewController =   FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)        present(flutterViewController, animated: true, completion: nil)    }
複製程式碼

注意:

UIApplicationDelegate subclass FlutterAppDelegate is recommended but not required
這裡的繼承是推薦的,但不是必須的。
make your app delegate implement the FlutterAppLifeCycleProvider protocol in order to make sure your plugins receive the necessary callbacks

讓AppDelegate 去實現這個協議, 在需要通知flutter的具體回撥裡傳送通知

**

實現方式2 作為前面示例的替代方案,您可以讓FlutterViewController隱式地建立自己的FlutterEngine,而不需要提前預熱。**

// Existing code omitted.
func showFlutter() {
  let flutterViewController = FlutterViewController(project: nil, nibName: nil, bundle: nil)
  present(flutterViewController, animated: true, completion: nil)
}
複製程式碼

FlutterEngine, by default, runs the main()Dart function of your lib/main.dart file.

You can also run a different entrypoint function by using runWithEntrypoint with an NSString specifying a different Dart function.

引擎預設開啟的是main.dart的page,你也可以通過 [runWithEntrypoint](https://api.flutter.dev/objcdoc/Classes/FlutterEngine.html#/c:objc(cs)FlutterEngine(im)runWithEntrypoint:)  開啟一個自定義新的入口頁面

Note: Dart entrypoint functions other than main() must be annotated with the following in order to not be tree-shaken away when compiling:

  @pragma('vm:entry-point')
  void myOtherEntrypoint() { ... };

使用
flutterEngine.run(withEntrypoint: "myOtherEntrypoint", 
                       libraryURI: "other_file.dart")
複製程式碼

Route路由

let flutterEngine = FlutterEngine()
// FlutterDefaultDartEntrypoint is the same as nil, which will run main().
engine.run(
  withEntrypoint: FlutterDefaultDartEntrypoint, initialRoute: "/onboarding")

這話的意思是,讓Dart:UI 的windows.defaultRouteName 變成自定義/onboadrding 而不是 /
複製程式碼

Alternatively, to construct a FlutterViewController directly without pre-warming a FlutterEngine.  或者 構建FlutterViewController直接跳過預熱flutter引擎

FlutterViewController* flutterViewController =
      [[FlutterViewController alloc] initWithProject:nil
                                        initialRoute:@"/onboarding"
                                             nibName:nil
                                              bundle:nil];
複製程式碼

4. 第二種方案: 將flutter以framework的形式通過Cocoapods引入iOS工程

這也是我們本篇的主要內容 其中 Cocoapods引入也分為兩種方式:

  1. pod的本地路徑化引入

  2. pod通過遠端Git倉庫引入 我們先來介紹本地化引入

一、 pod的本地化引入
$ cd ~/Desktop/FlutterForFW/iOSProject
$ pod init
$ pod install
複製程式碼

1.2. 接下來建立名字為‘ MyFlutterPod’的Pod庫

$ cd ~/Desktop/FlutterForFW
$ pod lib create MyFlutterPod
複製程式碼

終端依次輸入所需型別:

xingkunkun:FlutterForFW admin$ pod lib create MyFlutterPod
Cloning `https://github.com/CocoaPods/pod-template.git` into `MyFlutterPod`.
Configuring MyFlutterPod template.
------------------------------
To get you started we need to ask a few questions, this should only take a minute.

What platform do you want to use?? [ iOS / macOS ]
 > ios
What language do you want to use?? [ Swift / ObjC ]
 > objc
Would you like to include a demo application with your library? [ Yes / No ]
 > no
Which testing frameworks will you use? [ Specta / Kiwi / None ]
 > none
Would you like to do view based testing? [ Yes / No ]
 > no
What is your class prefix?
 > Kevin

Running pod install on your new library.
複製程式碼

建立完成之後會有一個工程自動開啟,此工程為Pod工程,在Example->MyFlutterPod.xcworkspace開啟後可以作為獨立專案在此編碼iOS程式碼之類的,暫時先不在此進行編寫原生程式碼,關閉退出。

1.3. 在MyFlutterPod目錄下建立 Flutter Module模組

$ cd ~/Desktop/FlutterForFW/MyFlutterPod
$ flutter create -t module flutter_module_for_ios
複製程式碼

命令執行完後,目錄資料夾下會多出一個名為flutter_module_for_ios的flutter模板專案

該專案模板包含有flutter程式碼模組+隱藏.ios檔案。同時選中三個鍵可以使隱藏檔案顯示

command + shift + .
複製程式碼

在當前flutter_module_for_ios檔案lib中可以編碼flutter相關程式碼,考慮到可能會在flutter專案中使用到相關外掛,我們可以在pubspec.yaml中新增一個外掛

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^0.1.2
  #新增 資料持久化外掛  https://pub.flutter-io.cn/packages/shared_preferences
  shared_preferences: ^0.5.4+3
複製程式碼

1.4、在flutter_module_for_ios專案中執行安裝外掛操作

$ cd ~/Desktop/FlutterForFW/MyFlutterPod/flutter_module_for_ios
$ flutter pub get
複製程式碼

可以看到在.ios資料夾下自動生成出來一個Podfile檔案

1.5、執行編譯該flutter_module_for_ios專案

編譯後會生成Flutter所依賴的相關的庫檔案。我們在當前先編譯出debug版本的庫檔案方便我們後續除錯

$ flutter build ios --debug      //編譯debug產物
或者
$ flutter build ios --release --no-codesign //編譯release產物(選擇不需要證書)複製程式碼
複製程式碼

觀察專案中的變化,可發現有多出編譯產物

我們所需要的就是這些生成出來的framework庫

build目錄下

ios->Debug-iphoneos-> FlutterPluginRegistrant.framework

ios->Debug-iphoneos-> shared_preferences.framework

.ios目錄下

Flutter-->App.framework Flutter-->engine-->Flutter.framework

當前生成的庫都是debug版本庫檔案。 需要注意的是,後續若想編譯出release版本的framework庫,修改下面的指令碼檔案根據註釋提示修改。因為在build生成產物之前會先重置檔案為初始狀態

接下來iOS工程通過Pod把這些庫引入到自己的工程中了。為了方便集中快速管理操作我們可以通過建立指令碼的方式對其進行管理(思路就是通過指令碼建立一個資料夾,將這些散亂在各檔案的庫統一拷貝進來)

2.1、在flutter_module_for_ios下建立指令碼檔案

$ cd ../flutter_module_for_ios
$ touch move_file.sh   //1. 建立指令碼檔案
$ open move_file.sh    //2. 開啟指令碼檔案複製程式碼
複製程式碼

新增以下指令碼程式碼

if [ -z $out ]; then
    out='ios_frameworks'
fi

echo "準備輸出所有檔案到目錄: $out"

echo "清除所有已編譯檔案"
find . -d -name build | xargs rm -rf
flutter clean
rm -rf $out
rm -rf build

flutter packages get

addFlag(){
    cat .ios/Podfile > tmp1.txt
    echo "use_frameworks!" >> tmp2.txt
    cat tmp1.txt >> tmp2.txt
    cat tmp2.txt > .ios/Podfile
    rm tmp1.txt tmp2.txt
}

echo "檢查 .ios/Podfile檔案狀態"
a=$(cat .ios/Podfile)
if [[ $a == use* ]]; then
    echo '已經新增use_frameworks, 不再新增'
else
    echo '未新增use_frameworks,準備新增'
    addFlag
    echo "新增use_frameworks 完成"
fi

echo "編譯flutter"
flutter build ios --debug
#release下放開下一行註釋,註釋掉上一行程式碼
#flutter build ios --release --no-codesign
echo "編譯flutter完成"
mkdir $out
cp -r build/ios/Debug-iphoneos/*/*.framework $out
#release下放開下一行註釋,註釋掉上一行程式碼
#cp -r build/ios/Release-iphoneos/*/*.framework $out
cp -r .ios/Flutter/App.framework $out
cp -r .ios/Flutter/engine/Flutter.framework $out

echo "複製framework庫到臨時資料夾: $out"

libpath='../'

rm -rf "$libpath/ios_frameworks"
mkdir $libpath
cp -r $out $libpath

echo "複製庫檔案到: $libpath"複製程式碼
複製程式碼

注意觀察指令碼檔案中的程式碼意思:將編譯生成的debug版本的所需.framework庫檔案拷貝至ios_frameworks檔案下並複製一份到MyFlutterPod目錄下,後續若想編譯生成release版本庫檔案時還需修改指令碼檔案查詢對應上release標識

2.2、執行指令碼檔案

$ sh move_file.sh      //3. 執行指令碼檔案複製程式碼
複製程式碼

此時的ios_frameworks檔案已經生成拷貝

裡面包含有我們前面提到所需要的.framework所有庫檔案

接下來我們就要通過MyFlutterPod庫的podspec來建立依賴匯出

3.1、編輯podspec檔案

開啟podspec檔案在end前一行新增以下命令

  s.static_framework = true
  p = Dir::open("ios_frameworks")
  arr = Array.new
  arr.push('ios_frameworks/*.framework')
  s.ios.vendored_frameworks = arr複製程式碼
複製程式碼

新增之後檔案整體長這樣

3.2、在iOSProject專案的podfile檔案中執行pod引用

在iOSProject工程下的podfile檔案中新增

# Uncomment the next line to define a global platform for your project
platform :ios, '8.0'

target 'iOSProject' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for iOSProject
   pod 'MyFlutterPod', :path => '../MyFlutterPod'

end複製程式碼
複製程式碼

之後執行

$ pod install
複製程式碼

可以看到終端提示安裝MyFlutterPod庫成功

其中MyFlutterPod庫裡就包含有我們所需的上述提到的framework庫

OK下面我們來試一下如何在iOS專案中跳轉進flutter介面,也就是我們提到的混合開發的程式碼測試,基本上也就是按照官方提供的模板寫

4.1、AppDelegate.h中修改

//  AppDelegate.h
//  iOSProject


#import 
#import 

@interface AppDelegate : FlutterAppDelegate
@property (nonatomic,strong) UIWindow *window;

@end複製程式碼
複製程式碼

4.2、AppDelegate.m中修改

//  AppDelegate.m
//  FlutterPodTest

#import "AppDelegate.h"
#import "ViewController.h"
#import 

@implementation AppDelegate


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    if (@available(iOS 13.0, *)) {
        
    } else {
        self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
        [self.window setBackgroundColor:[UIColor whiteColor]];
        ViewController *con = [[ViewController alloc] init];
        UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:con];
        [self.window setRootViewController:nav];
        [self.window makeKeyAndVisible];
        
    }
       [GeneratedPluginRegistrant registerWithRegistry:self];
    
    return YES;
}複製程式碼
複製程式碼

4.3、SceneDelegate.m

#import "SceneDelegate.h"
#import "ViewController.h"

@implementation SceneDelegate

- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
       //在這裡手動建立新的window
        if (@available(iOS 13.0, *)) {
            UIWindowScene *windowScene = (UIWindowScene *)scene;
            self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
            [self.window setWindowScene:windowScene];
            [self.window setBackgroundColor:[UIColor whiteColor]];
            
            ViewController *con = [[ViewController alloc] init];
            UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:con];
            [self.window setRootViewController:nav];
            [self.window makeKeyAndVisible];
        }
}複製程式碼
複製程式碼

4.4、ViewController.m

//
//  ViewController.m
//  iOSProject


#import "ViewController.h"
#import "AppDelegate.h"

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button setFrame:CGRectMake(100, 100, 200, 50)];
    [button setBackgroundColor:[UIColor lightGrayColor]];
    [button setTitle:@"ClickMePushToFlutterVC" forState:UIControlStateNormal];
    [button addTarget:self action:@selector(btn_click) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
    
}

- (void)btn_click {
    
    FlutterViewController *flutterViewController = [[FlutterViewController alloc] init];
    [self.navigationController pushViewController:flutterViewController animated:YES];

    /* 方式 2
     
    FlutterViewController *fluvc = [[FlutterViewController alloc]init];
    [self addChildViewController:fluvc];
    fluvc.view.frame = self.view.bounds;
    [fluvc didMoveToParentViewController:self];
    [self.view addSubview:fluvc.view];
    [self.navigationController pushViewController:fluvc animated:YES];
     
     */
}複製程式碼
複製程式碼

整合程式碼較官方方式有部分不同,這裡沒有通過 [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil]; 這種方式去初始化引擎,是因為FlutterViewContorller在new的時候會自動的建立一個引擎。而通過官方的方式去初始化引擎則需將該引擎設定成一個全域性單例去使用

至此。第一種形式的pod本地化引入工程就已經完成。但是我們發現一個問題那就是目前感覺好像還是沒有能完全剝離一臺電腦上沒有flutter環境配置的情況下如何去引入flutter.framework等庫檔案,難道要手動拷貝麼,這樣也不是很符合開發的初衷,接下來我會給大家介紹一下如何將建立好的私有庫上傳至git去託管,然後其他開發同學直接通過Git命令去引入包,這樣也就從根源上解決了模組化的剝離,更為乾淨利落

一、 pod通過遠端Git倉庫引入,這裡我選擇了GitLab

1.1、遠端建立倉庫MyFlutterPod

1.2、在MyFlutterPod專案中與遠端建立連線

$ cd ../MyFlutterPod
$ git remote add origin https://gitlab.com/OmgKevin/myflutterpod.git複製程式碼
複製程式碼

為了防止上傳檔案過大的限制,可以選擇在.gitignore檔案中選擇不上傳flutter_module_for_ios程式碼,只將ios_frameworks檔案中的庫檔案上傳就好

1.2.1、gitignore檔案

$ git add .
$ git commit -m "Initial commit"
$ git push -u origin master
// 給當前程式碼設定tag版本
$ git tag -m "first demo" 0.1.0
$ git push --tags複製程式碼
複製程式碼

可能會有上傳檔案大小限制,解除具體可以參考這篇文章

www.jianshu.com/p/3b86486bc…

1.3、修改MyFlutterPod.podspec檔案

需要注意的地方時你自己建立的gitlab地址與管理員郵箱及tag版本一一對應上

將此修改的檔案推至遠端倉庫

$ git status
$ git add MyFlutterPod.podspec
$ git commit -m "修改檔案"
$ git push origin master複製程式碼
複製程式碼

1.4、驗證一下Pod庫檔案是否可行

$ pod spec lint MyFlutterPod.podspec --verbose複製程式碼
複製程式碼

1.5、在iOSProject檔案中進行新增程式碼

如果在此之前做過本地化載入pod庫,要先解除安裝掉之前安裝過的檔案 --1 註釋掉podfile檔案中的程式碼 pod 'MyFlutterPod', :path => '../MyFlutterPod' --2執行一下 pod install 可以看到之前安裝過得庫已經從專案中移除

修改podfile檔案

# Uncomment the next line to define a global platform for your project
platform :ios, '8.0'

target 'iOSProject' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for iOSProject
#   pod 'MyFlutterPod', :path => '../MyFlutterPod'
   pod 'MyFlutterPod',:git=>'https://gitlab.com/OmgKevin/myflutterpod.git'

end複製程式碼
複製程式碼

安裝過程可能會比較慢,這跟網路有關

1.6、下載完畢的專案目錄下可以看到新增進的framework庫檔案

2.1、可以試一下按照方式一中的程式碼切換進flutter頁面,這裡就不貼程式碼了

至此,通過Git遠端管理的flutter模組整合進iOS專案已經完成了,以後每次flutter模組程式碼有更新時,直接推向遠端,iOS開發同學直接在podfile檔案中進行拉取,後續可以考慮加上tag標識來進行拉取

優點: 對 Flutter 自身的構建流程改動較少並且較徹底第解決了本地耦合的問題; 解決了元件式開發的痛點,各自開發各自的程式碼,也不用要求每臺電腦上都配置flutter環境

缺點: 整合方式上變得貌似更加繁瑣,Flutter 內容的變動需要先同步到遠端倉庫再 同步到 Standalone 模式方能生效;且要各自打包維護iOS安卓的兩套程式碼倉庫供不同平臺去拉取呼叫

PS. 閒魚APP 最終選擇了這個策略。

相關文章