Flutter與Native混合開發(fā)-FlutterBoost集成應用和開發(fā)實踐(iOS)

借圖Flutter_Boost

背景:

現在Flutter火熱蚣常,各個公司都會嘗試使用Flutter開發(fā)抵蚊,一般采用的都會是混合形式的開發(fā)贞绳,混合開發(fā)目前Flutter官方提供的不太完善冈闭、iOS和Android有差異性萎攒、接口也不同意、很麻煩货矮,也有一些大廠為了實現高效率高可用自己實現一套混合開發(fā)技術,比如閑魚的flutter_boost和哈羅的flutter_thrio。兩者之間優(yōu)缺點及設計原理劫灶、架構等后續(xù)再來具體分析本昏。接下我們先說下flutter_boost在iOS項目中的接入混合實踐枪汪。

準備工作:

1.Flutter SDK

為了便于后續(xù)我們flutter sdk版本的快速切換宿稀,我們需要安裝管理工具祝沸,方便我們在后續(xù)開發(fā)或者調試過程中快速切換版本。

這里推薦使用的Flutter SDK管理工具fvm,能快速切換版本,具體不做介紹卤唉,直接去查看文檔就可以竭恬,很詳細萍聊。

安裝自己所需的flutter sdk版本(我這里安裝的是1.17.1版本,因為需要跟下邊的flutter_boost版本對應):


fvm-flutter

2.flutter_boost框架

了解混合框架flutter_boost,首先需要知道我們需要使用他們的那個版本及版本對應的flutter sdk版本亭螟,比如:flutter_boost:v1.17.1-hotfixes 對應的flutter sdk:1.17.1 预烙。

正式接入:

一扁掸、flutter_module工程

1.創(chuàng)建空文件夾后,使用命令:
flutter create -t module flutter_module

如果iOS使用的是Swift開發(fā)使用命令:

flutter create -i swift -t module flutter_module
2.flutter_module創(chuàng)建完成后镀脂,打開flutter_module文件夾下pubspec.yaml文件薄翅。如下:
pubspec.yaml
注意:這里我用的flutter_boost的SDK最新版本:v1.17.1-hotfixes
3.cd到flutter_module文件夾下翘魄,執(zhí)行命令:
flutter packages get

包下載完成后flutter_module工程基本配置已經完成斋射。

二绩鸣、iOS原生工程

1.創(chuàng)建iOS項目

注意:iOS工程根目錄與flutter_module根目錄平級呀闻。

2.cd到iOS工程目錄下創(chuàng)建Podfile文件,執(zhí)行命令:

touch Podfile 生成文件垒手,然后可以使用vim Podfile 編輯或者直接open Podfile 或者直接打開文本編輯也可以科贬。

在文件中添加

flutter_application_path = '../flutter_module'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
install_all_flutter_pods(flutter_application_path)

pod
3.執(zhí)行 pod install下載依賴庫榜掌。
pod

4.配置項目

如果是OC工程憎账,則不會要做什么額外處理胞皱,如果是Swift項目需要建個橋接文件FlutterHybridiOS-Bridging-Header.h雾鬼,同時Build Sttings中需要配置該文件路徑。

swift調用OC橋接文件
對應路徑
注意:

(1).看其他博客出現過森渐,查看Build Phases中是否存在Flutter Build Script的腳本竟块,如果不存在需要添加對應腳本,但是目前我沒出現缺失的情況埠况。

生成的Flutter Build Script
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build

"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed

三.代碼接入實踐

這塊可以直接下載flutter_boost官方例子OCflutter_boost官方例子Swift查看具體實踐苦囱,其中OC例子是最全的乾蓬,這里只已Swift項目為基礎介紹基礎使用矛紫。

<1>.iOS平臺
  • 1.創(chuàng)建實現flutter_boost路由類,實現FLBPlatform中的協(xié)議,和Flutter中也會有對應的方法來處理跳轉操作缓呛。(直接參考上邊官方的代碼類就行),即push模式跳轉痰憎、present模式跳轉铣耘、關閉頁面等方法蜗细。

import Foundation
//import flutter_boost

class PlatformRouterImp: NSObject, FLBPlatform {
   func open(_ url: String, urlParams: [AnyHashable : Any], exts: [AnyHashable : Any], completion: @escaping (Bool) -> Void) {
       //跳轉打開原生Native頁面
       if (url == "native") {
          self.openNativeVC(url, urlParams: urlParams, exts: exts)
          return
       }

       var animated = false;
       if exts["animated"] != nil{
           animated = exts["animated"] as! Bool;
       }
       let vc = FLBFlutterViewContainer.init();
       vc.setName(url, params: urlParams);
       self.navigationController().pushViewController(vc, animated: animated);
       completion(true);
   }
   
   func present(_ url: String, urlParams: [AnyHashable : Any], exts: [AnyHashable : Any], completion: @escaping (Bool) -> Void) {
       var animated = false;
       if exts["animated"] != nil{
           animated = exts["animated"] as! Bool;
       }
       let vc = FLBFlutterViewContainer.init();
       vc.setName(url, params: urlParams);
       navigationController().present(vc, animated: animated) {
           completion(true);
       };
   }
   
   func close(_ uid: String, result: [AnyHashable : Any], exts: [AnyHashable : Any], completion: @escaping (Bool) -> Void) {
       var animated = false;
       if exts["animated"] != nil{
           animated = exts["animated"] as! Bool;
       }
       let presentedVC = self.navigationController().presentedViewController;
       let vc = presentedVC as? FLBFlutterViewContainer;
       if vc?.uniqueIDString() == uid {
           vc?.dismiss(animated: animated, completion: {
               completion(true);
           });
       }else{
           self.navigationController().popViewController(animated: animated);
       }
   }
   
private func openNativeVC(
       _ name: String?,
       urlParams params: [AnyHashable : Any]?,
       exts: [AnyHashable : Any]?
   ) {
       let vc = UIViewController()
       let animated = (exts?["animated"] as? NSNumber)?.boolValue ?? false
       if (params?["present"] as? NSNumber)?.boolValue ?? false {
           self.navigationController().present(vc, animated: animated) {}
       } else {
     
           self.navigationController().pushViewController(vc, animated: animated)
       }
   }

   func navigationController() -> UINavigationController {
       let delegate = UIApplication.shared.delegate as! AppDelegate
       let navigationController = delegate.window?.rootViewController as! UINavigationController
       return navigationController;
   }
}

  • 2.flutter_boost初始化吊骤,即在啟動的時候初始化框架代碼白粉。
import UIKit

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {

   override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
          //路由創(chuàng)建
          let router = PlatformRouterImp.init();
          //FlutterBoost初始化
           FlutterBoostPlugin.sharedInstance().startFlutter(with: router, onStart: { (engine) in
            
           });
           
           self.window = UIWindow.init(frame: UIScreen.main.bounds)
           let viewController = ViewController.init()
           let navi = UINavigationController.init(rootViewController: viewController)
           self.window.rootViewController = navi
           self.window.makeKeyAndVisible()
           
        return true
    }

  • 3.具體使用(創(chuàng)建自己的ViewController,添加兩個按鈕進行測試跳轉Native)

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.white
        // Do any additional setup after loading the view.
        let btn = UIButton(type: .custom);
        btn.backgroundColor = UIColor.red
        btn.frame = CGRect(x: 10, y: 60, width: 60, height: 40)
        btn.setTitle("Push Flutter Page", for: .normal)
        self.view.addSubview(btn);
        btn.addTarget(self, action: #selector(onClickPushFlutterPage), for: .touchUpInside);
        
        let btn2 = UIButton(type: .custom);
        btn2.backgroundColor = UIColor.blue
        btn2.frame = CGRect(x: 10, y: 120, width: 60, height: 40)
        self.view.addSubview(btn2);
        btn2.setTitle("Present Flutter Page", for: .normal)
        btn2.addTarget(self, action: #selector(onClickPresentFlutterPage), for: .touchUpInside);

    }


    @objc func onClickPushFlutterPage(_ sender: UIButton, forEvent event: UIEvent){
//        self.navigationController?.navigationBar.isHidden = true
        //其中這里的first鹃祖,在Flutter也會對應初始化的時候注冊該標識對應的Widget。
         FlutterBoostPlugin.open("first", urlParams:[kPageCallBackId:"MycallbackId#1"], exts: ["animated":true], onPageFinished: { (_ result:Any?) in
             print(String(format:"call me when page finished, and your result is:%@", result as! CVarArg));
         }) { (f:Bool) in
             print(String(format:"page is opened\(f)"));
         }
     }
    @objc func onClickPresentFlutterPage(_ sender: UIButton, forEvent event: UIEvent){
         FlutterBoostPlugin.present("second", urlParams:[kPageCallBackId:"MycallbackId#2"], exts: ["animated":true], onPageFinished: { (_ result:Any?) in
             print(String(format:"call me when page finished, and your result is:%@", result as! CVarArg));
         }) { (f:Bool) in
             print(String(format:"page is presented"));
         }
     }
}

*注意點:

(1).調用FlutterBoostPlugin.open和present方法時候楷兽,傳遞的urlname(first、second)會在Flutter中注冊對應的Widget。

<2>.Flutter平臺
  • 1.main.dart (可以理解為入口類却特,類似iOS中的Appdelete類)筛圆,實現Flutter_boost初始化和注冊Native對應的widget太援。
    (1)初始化flutter_boost插件及注冊對應的widgets仙蛉。
    (2)初始化路由router。
import 'package:flutter/material.dart';
import 'package:flutter_boost/flutter_boost.dart';
import 'simple_page_widgets.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();

//(1)初始化注冊
    FlutterBoost.singleton.registerPageBuilders(<String, PageBuilder>{
      'first': (String pageName, Map<String, dynamic> params, String _) => FirstRouteWidget(),
      'firstFirst': (String pageName, Map<String, dynamic> params, String _) =>
          FirstFirstRouteWidget(),
      'second': (String pageName, Map<String, dynamic> params, String _) => SecondRouteWidget(),
      'secondStateful': (String pageName, Map<String, dynamic> params, String _) =>
          SecondStatefulRouteWidget(),
    });
    
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Boost example',
        //(2)初始化路由
        builder: FlutterBoost.init(postPush: _onRoutePushed),
        home: Container(color: Colors.white));
  }

//(2)
  void _onRoutePushed(
    String pageName,
    String uniqueId,
    Map<String, dynamic> params,
    Route<dynamic> route,
    Future<dynamic> _,
  ) {}
}
注意點:

(1)import 'simple_page_widgets.dart'這個dart類中是實現了很多widget哀墓,比如first對應的FirstRouteWidget麸祷,second對應的SecondRouteWidget等阶牍。但是在實際開發(fā)中不要把widget放到一個Dart類種實現星瘾,降低代碼冗余琳状、代碼復雜度念逞、降低維護成本等翎承。

  • 2.實現widget(具體可以查看官方類中的simple_page_widgets.dart)
    下邊只展示我們之前對應的first和second。
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_boost/flutter_boost.dart';
import 'package:flutter_module/platform_view.dart'; //封裝的view


/// *********************** FirstRouteWidget*/
class FirstRouteWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _FirstRouteWidgetState();
}

class _FirstRouteWidgetState extends State<FirstRouteWidget> {
  _FirstRouteWidgetState();

  // flutter 側MethodChannel配置啊胶,channel name需要和native側一致
// static const MethodChannel _methodChannel = MethodChannel('flutter_native_channel');
  // String _systemVersion = '';

  // Future<dynamic> _getPlatformVersion() async {

  //   try {
  //     final String result = await _methodChannel.invokeMethod('getPlatformVersion');
  //     print('getPlatformVersion:' + result);
  //     setState(() {
  //       _systemVersion = result;
  //     });
  //   } on PlatformException catch (e) {
  //     print(e.message);
  //   }

  // }

  @override
  void initState() {
    print('initState');
    super.initState();
  }

  @override
  void didChangeDependencies() {
    print('didChangeDependencies');
    super.didChangeDependencies();
  }

  @override
  void didUpdateWidget(FirstRouteWidget oldWidget) {
    print('didUpdateWidget');
    super.didUpdateWidget(oldWidget);
  }

  @override
  void deactivate() {
    print('deactivate');
    super.deactivate();
  }

  @override
  void dispose() {
    print('[XDEBUG] - FirstRouteWidget is disposing~');
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('First Route')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            RaisedButton(
              child: const Text('Open native page'),
              onPressed: () {
                print('open natve page!');
                FlutterBoost.singleton
                    .open('native')
                    .then((Map<dynamic, dynamic> value) {
                  print(
                      'call me when page is finished. did recieve native route result $value');
                });
              },
            ),
            RaisedButton(
              child: const Text('Open FF route'),
              onPressed: () {
                print('open FF page!');
                FlutterBoost.singleton
                    .open('firstFirst')
                    .then((Map<dynamic, dynamic> value) {
                  print(
                      'call me when page is finished. did recieve FF route result $value');
                });
              },
            ),
            RaisedButton(
              child: const Text('Open second route1'),
              onPressed: () {
                print('open second page!');
                FlutterBoost.singleton
                    .open('second')
                    .then((Map<dynamic, dynamic> value) {
                  print(
                      'call me when page is finished. did recieve second route result $value');
                });
              },
            ),
            RaisedButton(
              child: const Text('Present second stateful route'),
              onPressed: () {
                print('Present second stateful page!');
                FlutterBoost.singleton.open('secondStateful',
                    urlParams: <String, dynamic>{
                      'present': true
                    }).then((Map<dynamic, dynamic> value) {
                  print(
                      'call me when page is finished. did recieve second stateful route result $value');
                });
              },
            ),
            RaisedButton(
              child: const Text('Present second route'),
              onPressed: () {
                print('Present second page!');
                FlutterBoost.singleton.open('second',
                    urlParams: <String, dynamic>{
                      'present': true
                    }).then((Map<dynamic, dynamic> value) {
                  print(
                      'call me when page is finished. did recieve second route result $value');
                });
              },
            ),
            RaisedButton(
              child: Text('Get system version by method channel:' + _systemVersion),
              onPressed: () => _getPlatformVersion(),
            ),
          ],
        ),
      ),
    );
  }
}

/// *********************** SecondRouteWidget*/
class SecondRouteWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Second Route')),
      body: Center(
        child: RaisedButton(
          onPressed: () {
            // Navigate back to first route when tapped.
            final BoostContainerSettings settings =
                BoostContainer.of(context).settings;
            FlutterBoost.singleton.close(
              settings.uniqueId,
              result: <String, dynamic>{'result': 'data from second'},
            );
          },
          child: const Text('Go back with result!'),
        ),
      ),
    );
  }
}

注意點:

(1).狀態(tài)管理的處理某饰,如果細心地同學會發(fā)現露乏,上邊FirstRouteWidget和SecondRouteWidget的繼承類不同,一個是StatelessWidget(非動態(tài)的)比勉、StatefulWidget(動態(tài)的)兩種狀態(tài)管理浩聋。具體理解可以自己去查資料學習衣洁,后續(xù)也會專門講下這個坊夫。

場景:

1.原生-->>原生

這個不多說直接還是原生跳轉就行

2.原生-->>Flutter

調用PlatformRouterImp中的方法

3.Flutter-->>原生

需要Flutter代碼執(zhí)行

 FlutterBoost.singleton
                    .open('native')
                    .then((Map<dynamic, dynamic> value) {
                  print(
                      'call me when page is finished. did recieve native route result $value');
                });

4.Flutter-->>Flutter (走Native原生Controller容器跳轉)

 FlutterBoost.singleton
                    .open('firstFirst')
                    .then((Map<dynamic, dynamic> value) {
                  print(
                      'call me when page is finished. did recieve FF route result $value');
                });

5.Flutter-->>Flutter(走Flutter自己的Flutter Navigator 跳轉)


 Navigator.push<dynamic>(
                    context,
                    MaterialPageRoute<dynamic>(builder: (_) => PushWidget()),
                  );

這個自己去做處理,建議既然使用flutter_boost就都采用flutter_boost統(tǒng)一的路有跳轉放吩。

如果想實現只打開一個Flutter的原生控制器渡紫,其他走flutter內部跳轉router邏輯惕澎,則需要自己處理集灌,或者也可以看下flutter_thrio框架复哆。

到這里基本上全部工作都可以了梯找,Flutter和iOS都會關聯上锈锤。直接運行Xcode就可以了久免。

關于分離開發(fā):

需要注意的地方如果之后考慮分開開發(fā)阎姥,Flutter同學只開發(fā)Flutter的話呼巴,需要在flutter_module/.ios/下工程配置之前iOS端的東西即可衣赶,就可以使用flutter run直接跑起來府瞄,且也能使用flutter_boost路由碘箍。做到Flutter開發(fā)Flutter敲街,原生開發(fā)原生多艇。做到分離峻黍。

下一篇會寫下Flutter_Boost在Android中的集成應用和代碼實踐姆涩,Flutter與Native混合開發(fā)-FlutterBoost集成應用和開發(fā)實踐(Android)骨饿。
萌圖鎮(zhèn)樓
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末绒北,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子峻汉,更是在濱河造成了極大的恐慌休吠,老刑警劉巖业簿,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件梅尤,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機矾湃,發(fā)現死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門邀跃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蛙紫,“玉大人僵驰,你說我怎么就攤上這事唁毒〗鳎” “怎么了近零?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長窖杀。 經常有香客問我幌甘,道長痊项,這世上最難降的妖魔是什么皱埠? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮托修,結果婚禮上,老公的妹妹穿的比我還像新娘睦刃。我一直安慰自己砚嘴,他們只是感情好,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布际长。 她就那樣靜靜地躺著,像睡著了一般工育。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上搓彻,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機與錄音旭贬,去河邊找鬼。 笑死骑篙,一個胖子當著我的面吹牛,可吹牛的內容都是我干的靶端。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼吁断!你這毒婦竟也來了?” 一聲冷哼從身側響起又兵,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎逆皮,沒想到半個月后页屠,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡竹观,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年臭增,在試婚紗的時候發(fā)現自己被綠了列牺。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瞎领。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖殿遂,靈堂內的尸體忽然破棺而出墨礁,到底是詐尸還是另有隱情饵溅,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站唇牧,受9級特大地震影響丐重,放射性物質發(fā)生泄漏。R本人自食惡果不足惜崖蜜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一豫领、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧闪檬,春花似錦粗悯、人聲如沸样傍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至互例,卻和暖如春媳叨,著一層夾襖步出監(jiān)牢的瞬間糊秆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留觉增,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓腻扇,卻偏偏與公主長得像幼苛,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子配并,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345