重磅消息!Flutter在哈囉出行 B 端創新業務的實踐,帶你直達巔峯!

做者 | 稻子

原文連接:https://mp.weixin.qq.com/s/m0...git

時間線

Flutter 在咱們團隊的起步算是比較晚的,直到 Flutter 要出 1.0 版本前夕纔開始實踐。github

大概的時間線以下:面試

  • 2018 年 11 月初,在 B 端小範圍嘗試接入 Flutter;
  • 2018 年 12 月 5 日,Flutter 發佈了 1.0;
  • 2019 年 4 月中旬,開始大範圍使用;
  • 2019 年 6 月中旬,Flutter 在業務上的效率提高效果開始體現出來;
  • 2019 年 7 月中旬,我所在的業務線的 B 端基本上全員轉 Flutter 進行移動端開發;
  • 2020 年 1 月初,咱們用 Flutter 開發了很是多的頁面,積累超過 10 萬行 Flutter 代碼,開始嘗試去 flutter_boost 的解決方案;
  • 2020 年 3 月中旬,開源 Flutter 嵌入原生移動 App 混合棧解決方案。

實踐路線

做爲一個創新業務的團隊,要作一門全新技術棧的技術儲備面臨如下幾個問題:架構

  • 團隊可投入時間少,要保證業務迭代;
  • 團隊成員沒有 Flutter 技術棧的基礎;
  • 如何驗證引入 Flutter 能帶來什麼業務價值。

這三個問題都是很是現實的問題,若是沒有明確的路線規劃盲目的引入 Flutter 的,踩坑過多最終會導入投入產出比過低而在業務上沒法接受。異步

我把實踐路線主要分一下四個階段:ide

  • 路線規劃
  • 技術儲備
  • 業務驗證
  • 持續集成

下面介紹在每一個階段咱們作了哪些事以及得到的成果和經驗。模塊化

路線規劃階段

目標設定:提高人效 50% ~ 100%組件化

關鍵行動佈局

  • 能用 Flutter 進行開發的優先使用 Flutter 來開發,不大範圍使用 Flutter 進行開發是很難達成提高人效的目標的;
  • Flutter 方案不成熟的直接使用原生開發,避免踩坑過多下降人效,好比地圖,存在地圖的頁面,咱們仍是直接用原生進行開發;
  • 不在早期引入狀態管理的庫,避免入門成本上升,也避免引入以後代碼量變多;
  • 團隊成員分批入坑 Flutter,不過於保守也不能太過於激進,避免在引入 Flutter 階段對業務迭代的影響;
  • 作好降級,異常監控等穩定性相關的工做。

Flutter技術學習階段

一樣是手機開發,爲何要分IOS和Android?若是分IOS&Android,那麼,它必定是由於價格不一樣。Ios手機5K以上,Android基本在500~5000。可是對於應用開發了說,一個應用,須要開發兩套,一套IOS,一套Android,那麼,能不能只開發一套,讓他們運行在兩個平臺上呢?答案是確定的,那就是混合開發(Hybrid)Flutter計算。
須要的夥伴能夠post

若是你正在尋找 Flutter 的學習資源,下面我整理了一些關於 Flutter的資料,須要的私信( Flutter)我分享給你。這份資料能夠幫助新手開始 Flutter 的旅程,也能夠幫已經瞭解過這方面的朋友更進一步。但願能幫到大家。

有須要的朋友迎加入羣聊:875911285(記得備註思否)到管理員處領取,或者 點擊下面連接哦,有不對的地方也歡迎指出,一塊兒交流共同進步。

Android學習PDF+架構視頻+面試文檔+源碼筆記

若是你有其餘須要的話,也能夠在 GitHub 上查看,下面的資料也會陸續上傳到Github

Flutter核心進階學習資料

Flutter基礎篇

Flutter項目實戰

技術儲備階段

demo 驗證

在技術儲備階段,主要是準備最小可驗證的 demo,驗證如下幾點:

  • 驗證 Flutter 嵌入現有 iOS 和 Android App 的方案,最終採用 Flutter 官方提供的解決方案;
  • 驗證 Flutter 包管理中的 開發模式 和 發佈模式,雖然做爲創新業務,但哈囉出行的 B 端集合了幾乎全部業務線的功能,咱們在實踐 Flutter 的時候不能影響其它業務線的正常開發,因此咱們須要一個發佈模式,避免其它的開發者也要安裝 Flutter 的開發環境;
  • 驗證 包大小內存佔用,以及 性能 是否知足,做爲創新業務的 B 端 App,在這方面咱們可能要求並不高,不作展開;
  • 解決 Flutter 異常收集和監控 的問題,底褲是必定要穿上的,考慮各類方案以後,最終選擇 Sentry 做爲早期的解決方案;
  • 驗證 混合棧 管理的方案是否可行,最終採納 flutter_boost 的方案;
  • 解決原生和 dart 狀態同步 的問題,爲了不開發過多的插件來作狀態的同步,抽象了一個通用的狀態同步插件;
  • 驗證持續集成的方案。

創建規範

沒有規範,會增長後續人員的入門成本:

  • 包和分支管理的規範,做爲一個多業務線的 App,包管理必定要考慮後續其它業務線接入的狀況;
  • dart 編碼規範,主要是 dart linter 的接入,考量每一個規則以及規則之間存在的衝突,解決這些規則上的衝突,由於最終要求每個 linter 的警告都必須解決掉;
  • 創建 最佳實踐 的積累方式,讓團隊每一個人能避免他人踩過的坑。

人員準備

團隊分紅兩組,前後入坑 Flutter,主要作如下準備:

  • 瞭解 dart 語言,能用 dart 進行基本的頁面開發;
  • 瞭解 開發規範,包括包和分支管理、編碼等規範;
  • 儘可能查閱相關的最佳實踐。

業務驗證階段

降級方案

雖然咱們是創新業務,但出於對線上敬畏之心,咱們依然準備了降級的方案,一旦 Flutter 上線以後影響到 App 的穩定性,能夠隨時降級。

因此咱們選擇了既有的模塊,將這些模塊用 Flutter 從新開發一遍。同時也爲後續的人效對比提供數據支撐。

代碼量減小

僅供參考,咱們 Flutter 的代碼量實踐下來會比任何一端的代碼量都少一些,相對於 iOS,咱們通常是純代碼佈局,代碼量減小更多。

更少的代碼,必定程度上表示更少的 bug,更少的 bug 表示花在修復 bug 上的時間也減小了。

多端一致性

Flutter 渲染的多端一致性,讓咱們在 UI 佈局上所花費的時間更少了。固然早期的 Flutter SDK 在處理字體、光標等方面略有差別,甚至有 bug,但都不是很大的問題。

人效提高

僅供參考,畢竟每一個團隊的狀況不盡相同,業務複雜度也不盡相同。

這裏給出咱們早期的三個數據的對比,19 年咱們下半年的時間基本上進入了純 Flutter 開發的階段,但 iOS 和 Android 兩端仍是須要分別打包、測試、上線,這會必定程度上下降人效提高的百分比,因此咱們綜合的人效提高會在 90% 左右。

業務價值

經過引入 Flutter,咱們在業務上能更快的進行迭代,使用 Flutter 開發的部分人效提高接近 90% 左右,由於咱們總歸是有一些功能須要用原生進行開發的,這部分工做量很差作對比。

這達成了咱們最初引入 Flutter 設定的目標,提高了整個團隊的人效,完美的支撐了業務的快速迭代。

持續集成階段

在業務驗證階段,咱們達成了提高人效 90% 的目標以後,欠缺的持續集成須要被提上日程,最緊迫的兩個事情就是 插件發佈 和 編譯產物發佈

做爲一個業務團隊,咱們依然沒有太多精力投入到工程建設上,因此不少工程化相關的能力,最開始都是手工的方式進行的,大概能夠分幾個階段:

  • 手工發佈,持續 3 個月;
  • 腳本發佈,持續 2 個月;
  • 一鍵發佈,19 年 12 月份至今。

手工發佈

  • flutter plugin 的發佈都是手工活,好比 iOS 發佈 pod 源碼和 Android 的 aar 都是手工進行的,部分還須要拷貝代碼;
  • flutter 編譯產物的發佈也是同樣靠手工,必定程度上下降了人效;

腳本發佈

這個階段主要是經過腳本實現 插件發佈 和 編譯產物發佈 的半自動化,但依然沒有集成到 App 發佈的 CI 系統。

這個階段也是在不斷完善發佈腳本,最終效果是根據 pubspec.yaml 文件的描述,自動發佈有更新的插件,並最終發佈編譯產物。

一鍵發佈

將現有的發佈腳本集成到 App 發佈的 CI 系統,效果就是一鍵打包,完全將這塊活自動化。

架構 1.0 的建設

架構建設方面,咱們須要解決的三個主要問題:

  • 頁面模塊化
  • 頁面間通訊
  • 頁面棧管理

在解決這三個問題的過程當中,咱們大體經歷了從 架構 1.0 到 架構 2.0,除了頁面模塊化基本保持不變,頁面間通訊、頁面棧管理從 架構 1.0 到 架構 2.0 的變化是很是大的。

頁面狀態管理 在咱們的業務上還不是一個主要問題,咱們也嘗試過引入 bloc,但還未進行足夠探索,因此這裏不作展開。

頁面模塊化 1.0

模塊化的定義,根據業務域劃分不一樣的業務模塊,爲了不與 WebComponent 的區別,不使用組件化這個名詞。

如何劃分模塊這可能須要另一篇文章來講明,簡單來講就是業務域的劃分。要保持模塊的內聚,每一個模塊的初始化須要獨立進行,要作到這點,咱們的方案是將全部模塊掛載到模塊樹上,相似文件夾的樹形結構。

頁面模塊化 1.0 主要提供如下能力:

  • 模塊掛載
  • 模塊初始化
  • 模塊異步初始化

掛載完成以後,初始化 root 模塊,會將全部掛載在樹上的模塊都進行初始化。這個樹形結構在葉子節點就是頁面,頁面的路徑自然可做爲頁面的 url。

模塊劃分本質上是根據業務域對頁面進行組織。無論是單一倉庫仍是多倉庫,均可以經過這種簡單的樹形結構來實現模塊掛載和初始化。

頁面間通訊 1.0

模塊間通訊,本質上主要是頁面間通訊。

移動端不少模塊化的方案,都會將模塊間通訊做爲主要能力進行建設,咱們在原生端也有一套這類方案,但在 Flutter 嵌入原生應用中,並不能簡單複用這套方案,若是生搬硬套會帶來不少的編碼量,並非一個很輕量的解決方案。

頁面間通訊的能力,須要重頭開始建設,早期咱們抽象了一個狀態同步的方案,開發一個插件 topic_center 專門用來給原生和 dart 進行狀態同步。

topic_center 提供的能力:

  • 原生模塊間的狀態同步
  • Flutter 模塊間的狀態同步
  • Flutter 端按需同步原生狀態
  • 三端一致的狀態的獲取與訂閱 API

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 時提供的頁面間通訊解決方案,後面會講到咱們在進行架構升級以後提供的更輕量級的解決方案。

頁面棧管理 1.0

若是沒有混合棧管理,咱們在原生應用上引入 Flutter 將是一個極爲麻煩的事情,咱們可能爲此維護比較混亂的 Channel 通訊層。

flutter_boost 是閒魚開源的優秀的 Flutter 混合棧管理解決方案,也是當時社區惟一可選的解決方案。

flutter_boost 的優點:

  • Flutter 頁面的路由與原生頁面同樣
  • Flutter 頁面的交互手勢與原生頁面同樣
  • 提供頁面關閉回傳參數的能力

若是不使用 flutter_boost,咱們的頁面結構多是這樣的:

使用了 flutter_boost 以後能夠是這樣的:

架構 1.0 的問題

頁面間通訊 1.0 的問題

  • topic 的管理成本太高

topic_center 插件能解決頁面間通訊的問題,但有一個不算問題的問題,對 topic 的管理成本太高。爲了不全局 topic 重複的問題,每一個頁面狀態的同步都須要在 topic 上帶上各類前綴,通常就是 模塊、子模塊、功能、頁面做爲前綴,而後這個 topic 最後長得跟頁面的 url 極爲類似。爲了解決這個問題,須要想辦法去掉這個 topic 的管理成本。

  • 源碼過於複雜

topic_center 這個庫的投入產出比實在是不高,源碼過於複雜 帶來不僅是解決方案的複雜,也帶來 維護成本推高 不少。

頁面棧管理 1.0 的問題

  • 路由 API 過於簡陋

好比,項目上須要實現關閉到某個頁面的場景,或者刪除當前頁面之下的某個頁面,咱們須要在 flutter_boost 上自行擴展,且難於維護,如何跟官方的 flutter_boost 保持代碼同步是一個艱難的事情。

  • 使用的開源庫的 API 再也不向後兼容

咱們在項目上大量使用頁面回傳參數的能力,可是該 API 在新版本上被移除了。

  • 最大的問題 iOS 內存佔用太高

flutter_boost iOS 端的實現方案,在實際項目上使用時,咱們只能將每個 Flutter 頁面都套在一個原生的 FlutterViewController 中 ,這直接致使每打開一個 Flutter 頁面的內存佔用高出 10M 左右。

爲了解決這些問題,咱們開始了 架構 2.0 的建設。

架構 2.0 的建設

架構 2.0 主要是解決 頁面間通訊 1.0 和 頁面棧管理 2.0 的解決方案存在的一些問題而演變出來的,同時對 頁面模塊化 作更細緻的職能分解。

頁面模塊化 2.0

方案能夠參考 ThrioModule,ThrioModule 的 API 也遵照多端一致性。

相比於 頁面模塊化 1.0,功能的變遷以下:

  • 模塊掛載 1.0
  • 模塊初始化 1.0
  • 模塊異步初始化 1.0
  • 頁面路由註冊 2.0
  • 頁面路由行爲觀察 2.0
  • 頁面生命週期觀察 2.0
  • 頁面通知接收 2.0

以上功能均提供三端一致的 API 2.0

頁面棧路由 2.0

咱們開發了 thrio,主要是解決 頁面間通訊 1.0 和 頁面棧管理 1.0 中存在的問題。

thrio 的頁面棧結構

thrio 的原理上改善點是除了複用 FlutterEngine,還複用了原生的頁面容器,頁面棧結構以下:

thrio 的路由

thrio 提供了三端一致的路由 API

頁面的 push

  • dart 端打開頁面
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'),
);
  • iOS 端打開頁面
objc
[ThrioNavigator pushUrl:@"flutter1"];
// 接收所打開頁面的關閉回調
[ThrioNavigator pushUrl:@"biz2/flutter2" poppedResult:^(id _Nonnull params) {
    ThrioLogV(@"biz2/flutter2 popped: %@", params);
}];
  • Android 端打開頁面
kotlin
ThrioNavigator.push(this, "biz1/flutter1",
        mapOf("k1" to 1),
        false,
        poppedResult = {
            Log.e("Thrio", "native1 popResult call params $it")
        }
)

頁面的 pop

  • dart 端關閉頂層頁面
dart
ThrioNavigator.pop(params: 'popped flutter1'),
  • iOS 端關閉頂層頁面
objc
[ThrioNavigator popParams:@{@"k1": @3}];
  • Android 端關閉頂層頁面
kotlin
ThrioNavigator.pop(this, params, animated)

頁面的 popTo

  • dart 端關閉到頁面
dart
ThrioNavigator.popTo(url: 'flutter1');
  • iOS 端關閉到頁面
objc
[ThrioNavigator popToUrl:@"flutter1" animated:NO];
  • Android 端關閉到頁面
kotlin
ThrioNavigator.popTo(context, url, index)

頁面的 remove

  • dart 端關閉特定頁面
dart
ThrioNavigator.remove(url: 'flutter1', animated: true);
  • iOS 端關閉特定頁面
objc
[ThrioNavigator removeUrl:@"flutter1" animated:NO];
  • Android 端關閉特定頁面
kotlin
ThrioNavigator.remove(context, url, index)

thrio 的頁面通知

頁面通知做爲解決頁面間通訊的一個能力被引入 thrio,以一種很是輕量的方式解決了 topic_center 所要解決的問題,並且不須要管理 topic。

發送頁面通知

  • dart 端給特定頁面發通知
dart
ThrioNavigator.notify(url: 'flutter1', name: 'reload');
  • iOS 端給特定頁面發通知
objc
[ThrioNavigator notifyUrl:@"flutter1" name:@"reload"];
  • Android 端給特定頁面發通知
kotlin
ThrioNavigator.notify(url, index, params)

接收頁面通知

  • dart 端接收頁面通知

使用 NavigatorPageNotify 這個 Widget 來實如今任何地方接收當前頁面收到的通知。

dart
NavigatorPageNotify(
      name: 'page1Notify',
      onPageNotify: (params) =>
          verbose('flutter1 receive notify: $params'),
      child: Xxxx());
  • iOS 端接收頁面通知

UIViewController實現協議NavigatorPageNotifyProtocol,經過 onNotify 來接收頁面通知:

objc
- (void)onNotify:(NSString *)name params:(NSDictionary *)params {
  ThrioLogV(@"native1 onNotify: %@, %@", name, params);
}
  • Android 端接收頁面通知

Activity實現協議OnNotifyListener,經過 onNotify 來接收頁面通知:

kotlin
class Activity : AppCompatActivity(), OnNotifyListener {
    override fun onNotify(name: String, params: Any?) {
    }
}

由於 Android activity 在後臺可能會被銷燬,因此頁面通知實現了一個懶響應的行爲,只有當頁面呈現以後纔會收到該通知,這也符合頁面須要刷新的場景。

架構 2.0 的優點

在咱們的業務上存在不少模塊,進去以後是,首頁 -> 列表頁 -> 詳情頁 -> 處理頁 -> 結果頁,大體會是連續打開 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 貢獻代碼。

做者:稻子,就任於哈囉出行。

相關文章
相關標籤/搜索