做者 | 稻子原文連接:https://mp.weixin.qq.com/s/m0...git
Flutter 在咱們團隊的起步算是比較晚的,直到 Flutter 要出 1.0 版本前夕纔開始實踐。github
大概的時間線以下:面試
做爲一個創新業務的團隊,要作一門全新技術棧的技術儲備面臨如下幾個問題:架構
這三個問題都是很是現實的問題,若是沒有明確的路線規劃盲目的引入 Flutter 的,踩坑過多最終會導入投入產出比過低而在業務上沒法接受。異步
我把實踐路線主要分一下四個階段:ide
下面介紹在每一個階段咱們作了哪些事以及得到的成果和經驗。模塊化
目標設定:提高人效 50% ~ 100%組件化
關鍵行動:佈局
一樣是手機開發,爲何要分IOS和Android?若是分IOS&Android,那麼,它必定是由於價格不一樣。Ios手機5K以上,Android基本在500~5000。可是對於應用開發了說,一個應用,須要開發兩套,一套IOS,一套Android,那麼,能不能只開發一套,讓他們運行在兩個平臺上呢?答案是確定的,那就是混合開發(Hybrid)Flutter計算。
須要的夥伴能夠post
若是你正在尋找 Flutter 的學習資源,下面我整理了一些關於 Flutter的資料,須要的私信( Flutter)我分享給你。這份資料能夠幫助新手開始 Flutter 的旅程,也能夠幫已經瞭解過這方面的朋友更進一步。但願能幫到大家。
有須要的朋友迎加入羣聊:875911285(記得備註思否)到管理員處領取,或者 點擊下面連接哦,有不對的地方也歡迎指出,一塊兒交流共同進步。
若是你有其餘須要的話,也能夠在 GitHub 上查看,下面的資料也會陸續上傳到Github
Flutter核心進階學習資料
Flutter基礎篇
Flutter項目實戰
demo 驗證
在技術儲備階段,主要是準備最小可驗證的 demo,驗證如下幾點:
創建規範
沒有規範,會增長後續人員的入門成本:
人員準備
團隊分紅兩組,前後入坑 Flutter,主要作如下準備:
降級方案
雖然咱們是創新業務,但出於對線上敬畏之心,咱們依然準備了降級的方案,一旦 Flutter 上線以後影響到 App 的穩定性,能夠隨時降級。
因此咱們選擇了既有的模塊,將這些模塊用 Flutter 從新開發一遍。同時也爲後續的人效對比提供數據支撐。
代碼量減小
僅供參考,咱們 Flutter 的代碼量實踐下來會比任何一端的代碼量都少一些,相對於 iOS,咱們通常是純代碼佈局,代碼量減小更多。
更少的代碼,必定程度上表示更少的 bug,更少的 bug 表示花在修復 bug 上的時間也減小了。
多端一致性
Flutter 渲染的多端一致性,讓咱們在 UI 佈局上所花費的時間更少了。固然早期的 Flutter SDK 在處理字體、光標等方面略有差別,甚至有 bug,但都不是很大的問題。
人效提高
僅供參考,畢竟每一個團隊的狀況不盡相同,業務複雜度也不盡相同。
這裏給出咱們早期的三個數據的對比,19 年咱們下半年的時間基本上進入了純 Flutter 開發的階段,但 iOS 和 Android 兩端仍是須要分別打包、測試、上線,這會必定程度上下降人效提高的百分比,因此咱們綜合的人效提高會在 90% 左右。
業務價值
經過引入 Flutter,咱們在業務上能更快的進行迭代,使用 Flutter 開發的部分人效提高接近 90% 左右,由於咱們總歸是有一些功能須要用原生進行開發的,這部分工做量很差作對比。
這達成了咱們最初引入 Flutter 設定的目標,提高了整個團隊的人效,完美的支撐了業務的快速迭代。
在業務驗證階段,咱們達成了提高人效 90% 的目標以後,欠缺的持續集成須要被提上日程,最緊迫的兩個事情就是 插件發佈 和 編譯產物發佈。
做爲一個業務團隊,咱們依然沒有太多精力投入到工程建設上,因此不少工程化相關的能力,最開始都是手工的方式進行的,大概能夠分幾個階段:
手工發佈
腳本發佈
這個階段主要是經過腳本實現 插件發佈 和 編譯產物發佈 的半自動化,但依然沒有集成到 App 發佈的 CI 系統。
這個階段也是在不斷完善發佈腳本,最終效果是根據 pubspec.yaml 文件的描述,自動發佈有更新的插件,並最終發佈編譯產物。
一鍵發佈
將現有的發佈腳本集成到 App 發佈的 CI 系統,效果就是一鍵打包,完全將這塊活自動化。
架構建設方面,咱們須要解決的三個主要問題:
在解決這三個問題的過程當中,咱們大體經歷了從 架構 1.0 到 架構 2.0,除了頁面模塊化基本保持不變,頁面間通訊、頁面棧管理從 架構 1.0 到 架構 2.0 的變化是很是大的。
頁面狀態管理 在咱們的業務上還不是一個主要問題,咱們也嘗試過引入 bloc,但還未進行足夠探索,因此這裏不作展開。
模塊化的定義,根據業務域劃分不一樣的業務模塊,爲了不與 WebComponent 的區別,不使用組件化這個名詞。
如何劃分模塊這可能須要另一篇文章來講明,簡單來講就是業務域的劃分。要保持模塊的內聚,每一個模塊的初始化須要獨立進行,要作到這點,咱們的方案是將全部模塊掛載到模塊樹上,相似文件夾的樹形結構。
頁面模塊化 1.0 主要提供如下能力:
掛載完成以後,初始化 root 模塊,會將全部掛載在樹上的模塊都進行初始化。這個樹形結構在葉子節點就是頁面,頁面的路徑自然可做爲頁面的 url。
模塊劃分本質上是根據業務域對頁面進行組織。無論是單一倉庫仍是多倉庫,均可以經過這種簡單的樹形結構來實現模塊掛載和初始化。
模塊間通訊,本質上主要是頁面間通訊。
移動端不少模塊化的方案,都會將模塊間通訊做爲主要能力進行建設,咱們在原生端也有一套這類方案,但在 Flutter 嵌入原生應用中,並不能簡單複用這套方案,若是生搬硬套會帶來不少的編碼量,並非一個很輕量的解決方案。
頁面間通訊的能力,須要重頭開始建設,早期咱們抽象了一個狀態同步的方案,開發一個插件 topic_center 專門用來給原生和 dart 進行狀態同步。
topic_center 提供的能力:
topic_center Flutter 端按需同步原生狀態的數據流:
topic_center 提供以下的 API,topic_center 遵循 Flutter 的多端一致性原則,咱們在三端提供了同樣的 API,下圖僅展現 dart 的 API 定義:
dart void putValue<T>(T value, String topic); Future<T> getValue<T>(String topic); Stream<T> getValueStream<T>(String topic); void putListValue<E>(E value, String topic); Future<List<E>> getListValue<E>(String topic); Stream<List<E>> getListValueStream<E>(String topic); void putMapValue<K, V>(Map<K, V> value, String topic); Future<Map<K, V>> getMapValue<K, V>(String topic); Stream<Map<K, V>> getMapValueStream<K, V>(String topic); void putTuple2Value<T0, T1>(Tuple2<T0, T1> value, String topic); Future<Tuple2<T0, T1>> getTuple2Value<T0, T1>(String topic); Stream<Tuple2<T0, T1>> getTuple2ValueStream<T0, T1>(String topic); void putTuple3Value<T0, T1, T2>(Tuple3<T0, T1, T2> value, String topic); Future<Tuple3<T0, T1, T2>> getTuple3Value<T0, T1, T2>(String topic); Stream<Tuple3<T0, T1, T2>> getTuple3ValueStream<T0, T1, T2>(String topic); void putTuple4Value<T0, T1, T2, T3>(Tuple4<T0, T1, T2, T3> value, String topic); Future<Tuple4<T0, T1, T2, T3>> getTuple4Value<T0, T1, T2, T3>(String topic); Stream<Tuple4<T0, T1, T2, T3>> getTuple4ValueStream<T0, T1, T2, T3>(String topic); void putTuple5Value<T0, T1, T2, T3, T4>(Tuple5<T0, T1, T2, T3, T4> value, String topic); Future<Tuple5<T0, T1, T2, T3, T4>> getTuple5Value<T0, T1, T2, T3, T4>(String topic); Stream<Tuple5<T0, T1, T2, T3, T4>> getTuple5ValueStream<T0, T1, T2, T3, T4>(String topic);
topic_center 是咱們在 架構 1.0 時提供的頁面間通訊解決方案,後面會講到咱們在進行架構升級以後提供的更輕量級的解決方案。
若是沒有混合棧管理,咱們在原生應用上引入 Flutter 將是一個極爲麻煩的事情,咱們可能爲此維護比較混亂的 Channel 通訊層。
flutter_boost 是閒魚開源的優秀的 Flutter 混合棧管理解決方案,也是當時社區惟一可選的解決方案。
flutter_boost 的優點:
若是不使用 flutter_boost,咱們的頁面結構多是這樣的:
使用了 flutter_boost 以後能夠是這樣的:
頁面間通訊 1.0 的問題
topic_center 插件能解決頁面間通訊的問題,但有一個不算問題的問題,對 topic 的管理成本太高。爲了不全局 topic 重複的問題,每一個頁面狀態的同步都須要在 topic 上帶上各類前綴,通常就是 模塊、子模塊、功能、頁面做爲前綴,而後這個 topic 最後長得跟頁面的 url 極爲類似。爲了解決這個問題,須要想辦法去掉這個 topic 的管理成本。
topic_center 這個庫的投入產出比實在是不高,源碼過於複雜 帶來不僅是解決方案的複雜,也帶來 維護成本推高 不少。
頁面棧管理 1.0 的問題
好比,項目上須要實現關閉到某個頁面的場景,或者刪除當前頁面之下的某個頁面,咱們須要在 flutter_boost 上自行擴展,且難於維護,如何跟官方的 flutter_boost 保持代碼同步是一個艱難的事情。
咱們在項目上大量使用頁面回傳參數的能力,可是該 API 在新版本上被移除了。
flutter_boost iOS 端的實現方案,在實際項目上使用時,咱們只能將每個 Flutter 頁面都套在一個原生的 FlutterViewController 中 ,這直接致使每打開一個 Flutter 頁面的內存佔用高出 10M 左右。
爲了解決這些問題,咱們開始了 架構 2.0 的建設。
架構 2.0 主要是解決 頁面間通訊 1.0 和 頁面棧管理 2.0 的解決方案存在的一些問題而演變出來的,同時對 頁面模塊化 作更細緻的職能分解。
頁面模塊化 2.0
方案能夠參考 ThrioModule,ThrioModule 的 API 也遵照多端一致性。
相比於 頁面模塊化 1.0,功能的變遷以下:
以上功能均提供三端一致的 API 2.0
頁面棧路由 2.0
咱們開發了 thrio,主要是解決 頁面間通訊 1.0 和 頁面棧管理 1.0 中存在的問題。
thrio 的頁面棧結構
thrio 的原理上改善點是除了複用 FlutterEngine,還複用了原生的頁面容器,頁面棧結構以下:
thrio 的路由
thrio 提供了三端一致的路由 API
頁面的 push
dart ThrioNavigator.push(url: 'flutter1'); // 傳入參數 ThrioNavigator.push(url: 'native1', params: { '1': {'2': '3'}}); // 是否動畫,目前在內嵌的 dart 頁面中動畫沒法取消,原生 iOS 頁面有效果 ThrioNavigator.push(url: 'native1', animated:true); // 接收鎖打開頁面的關閉回調 ThrioNavigator.push( url: 'biz2/flutter2', params: {'1': {'2': '3'}}, poppedResult: (params) => verbose('biz2/flutter2 popped: $params'), );
objc [ThrioNavigator pushUrl:@"flutter1"]; // 接收所打開頁面的關閉回調 [ThrioNavigator pushUrl:@"biz2/flutter2" poppedResult:^(id _Nonnull params) { ThrioLogV(@"biz2/flutter2 popped: %@", params); }];
kotlin ThrioNavigator.push(this, "biz1/flutter1", mapOf("k1" to 1), false, poppedResult = { Log.e("Thrio", "native1 popResult call params $it") } )
頁面的 pop
dart ThrioNavigator.pop(params: 'popped flutter1'),
objc [ThrioNavigator popParams:@{@"k1": @3}];
kotlin ThrioNavigator.pop(this, params, animated)
頁面的 popTo
dart ThrioNavigator.popTo(url: 'flutter1');
objc [ThrioNavigator popToUrl:@"flutter1" animated:NO];
kotlin ThrioNavigator.popTo(context, url, index)
頁面的 remove
dart ThrioNavigator.remove(url: 'flutter1', animated: true);
objc [ThrioNavigator removeUrl:@"flutter1" animated:NO];
kotlin ThrioNavigator.remove(context, url, index)
thrio 的頁面通知
頁面通知做爲解決頁面間通訊的一個能力被引入 thrio,以一種很是輕量的方式解決了 topic_center 所要解決的問題,並且不須要管理 topic。
發送頁面通知
dart ThrioNavigator.notify(url: 'flutter1', name: 'reload');
objc [ThrioNavigator notifyUrl:@"flutter1" name:@"reload"];
kotlin ThrioNavigator.notify(url, index, params)
接收頁面通知
使用 NavigatorPageNotify 這個 Widget 來實如今任何地方接收當前頁面收到的通知。
dart NavigatorPageNotify( name: 'page1Notify', onPageNotify: (params) => verbose('flutter1 receive notify: $params'), child: Xxxx());
UIViewController實現協議NavigatorPageNotifyProtocol,經過 onNotify 來接收頁面通知:
objc - (void)onNotify:(NSString *)name params:(NSDictionary *)params { ThrioLogV(@"native1 onNotify: %@, %@", name, params); }
Activity實現協議OnNotifyListener,經過 onNotify 來接收頁面通知:
kotlin class Activity : AppCompatActivity(), OnNotifyListener { override fun onNotify(name: String, params: Any?) { } }
由於 Android activity 在後臺可能會被銷燬,因此頁面通知實現了一個懶響應的行爲,只有當頁面呈現以後纔會收到該通知,這也符合頁面須要刷新的場景。
在咱們的業務上存在不少模塊,進去以後是,首頁 -> 列表頁 -> 詳情頁 -> 處理頁 -> 結果頁,大體會是連續打開 5 個 Flutter 頁面的場景。
這裏會對 架構 1.0 和 架構 2.0 咱們所使用的解決方案作一些優劣對比,僅表示咱們業務場景下的結果,不同的場景不具有可參考性。
在此僅列出兩個比較明顯的改善措施,這些改善主要是原理層面的優點帶來的,不表明 thrio 的實現比 flutter_boost 高明,另外數據僅供參考,只是爲了說明原理帶來的優點。
thrio 在 iOS 上的內存佔用
一樣連續打開 5 個頁面的場景,boost 的方案會消耗 91.67M 內存,thrio 只消耗 42.76 內存,模擬器上跑出來的數據大體以下:
thrio 在 Android 上的頁面打開速度
一樣連續打開 5 個頁面的場景,thrio 打開第一個頁面跟 boost 耗時是同樣的,由於都須要打開一個新的 Activity,以後 4 個頁面 thrio 會直接打開 Flutter 頁面,耗時會降下來,如下單位爲 ms:
總的來講,引入 Flutter 是一個很明智的選擇,人效提高是很是明顯的。若是你的 App 對包大小不敏感,那徹底能夠嘗試在項目中引入 Flutter。
固然過程當中也遇到了很是多的問題,但相對於人效提高來講,解決這些問題的成本都是可接受的。
若是你想要無縫的將 Flutter 引入現有項目,thrio 可能會節省你不少精力。固然 thrio 是個很是年輕的庫,相比於前輩 flutter_boost 還有很長的路要走,也歡迎有興趣的同窗給 thrio 貢獻代碼。
做者:稻子,就任於哈囉出行。