關於將大型 App 移植到 Flutter 的文章並不多,深入實作後,結果令我非常驚訝。
在澳洲有一個名為 Easy Diet Diary 的原生 iOS App。這個 App:
- 下載量達 120 萬次;
- 使用 Objective C 和 Swift 共同編寫,後端是 Amazon AWS;
- 包含 75,000 行程式碼。
我工作的公司曾將開發 Android 版本列在待辦事項的最優先一項,可是過了很久仍然沒有這樣做,因為:
- 支援兩個版本的程式原始碼太昂貴、且難以管理;
- 主要的跨平台開發選項 Xamarin 和 React Native 都有致命性的缺陷(這是另一個故事)。
投向 Flutter 的懷抱
Flutter 運行速度很快,並且提供與原生 App 式沒有分別的順暢體驗(尤其是在 2018 年 9 月 Preview 2 中添加的 iOS widgets)。
讓我們一步步看我的 Flutter 旅程:
- 程式碼行數和開發速度
- 結構
- 社群
- 效能
- 語言
- 不足之處
- 總結
程式碼行數和開發速度
當我開始進行移植時,估計需要 6 個月的時間。但最後該專案提前完成了!這一點對我來說是意料之外的。😀
我是如何辦到的?
在 Flutter 中,我們並不是使用 Storyboard (iOS) 或 XML (Android) 佈局來創建 App UI 的,而是完全利用程式碼在 widget tree 中組建 widget 來創建的。這聽起來有點嚇人吧!不過我深入研究後發現,雖然是需要一些時間來習慣,但不久我就開始掌握到靈巧的 widget tree。
大約在開始移植專案的三個月後,一件奇妙的事情發生了。隨著我越漸熟練並迅速地移植功能,專案中的程式碼行數增加速度卻減慢了。這一點很奇怪,因為我正在移植相當多的業務邏輯,這些邏輯在程式碼數量幾乎是一比一的比率。
事實是,相較於原生 iOS 開發,利用 Flutter 能夠更容易創建類別、和編寫函數來重用程式碼的 UI。我通常都可以簡單地重構 UI widget,透過一些參數讓我的 UI widgets 可以重複使用,然後任務就完成了!如果這行不通,我也可以簡單地在現有 widget 上包裝另一個 widget,以完成所需行為。這一點非常令人滿意!
在最終版本中,我預計程式碼行數將小於 30,000(原生 iOS 版本程式碼行數為 75,000)。當然,原生 iOS 版本包含一些最終被取代或未被使用的程式碼。我估計未使用的程式碼佔 15,000 行。換句話說,我需要移植 60,000 行。
也就是說,移植至 Flutter 後程式碼僅是原生 iOS 原始碼行數的一半!
此外,Flutter 專案不包含任何 Storyboard XML。但那些 Storyboard 包含很多 XML,原生 iOS 專案就包含了:
- 15 個 Storyboards
- 47 個 nib files
- 92 個 View Controllers
與 Storyboard 抗戰多年、不斷嘗試遵循最佳實踐後,在 Flutter 中構建 UI 變得暢快而有效率。
我沒有意識到原來我花了大部分時間來編寫 UI 相關的程式碼、學習使用 Storyboard 和自動佈局限制條件 (auto layout constraints)。我十分同意將 UI 佈局與程式碼分開,不僅是原生 iOS 最佳實踐提倡 UI 與程式碼分開的概念,就連在 Quora 亦有 Why does Android use XML to define user UI and not just Java code?(為什麼 Android 使用 XML 而非 Java 程式碼來定義 UI ?)這種問題。
Storyboard 是一種自上而下的佈局(適用於電腦 App)方式,而 widget 採用自下而上的方法,這大大簡化了構建手機 app 的過程。我在 React Native 或 CSS flex-boxes 中讀過類似這種的螢幕佈局方式。Wm Leler 在 Hackernoon 的這篇文章 What’s Revolutionary about Flutter(Flutter 有甚麼革新的功能) 亦對相關內容作出很好的解釋。
透過以下方式利用 widget 構建 UI 很有幫助:
- 在多數情況下,Flutter 的 “Hot Reload”(熱重載)特性都能迅速將程式碼無縫地整合到正在運行的 App 中。
- Android Studio 的 shortcut Alt + Enter 可插入或刪除 UI widget (rows、columns、containers),詳情請參閱 這篇文章。我假設 VS Code 中有類似的東西,那麼使用 Hot Reload 和 Alt + Enter,我幾分鐘內就可以打開一個螢幕,或者天馬行空地嘗試 UI 的呈現方式。
- 將 debugPaintSizeEnabled 設置為 true,這樣所有 UI widget 周圍都會顯示顏色鮮豔的邊框,當佈局問題發生時,這個功能就非常有用了。
架構
我在選擇架構的過程經歷了:
- 觀看了多次 Brian Egan 的Keep it Simple, State: Architecture for Flutter Apps (DartConf 2018) 演講;
- 閱讀了幾次 Eric Windmill 撰寫的 Using Flutter Inherited Widgets Effectively 這篇文章;
- 查看了這些 Flutter 架構範例。
我最後選擇了 Eric 的建議,使用了一種名為 Lifting State Up 的架構模式,這是 Redux 的第一步,看起來非常好用。 然而,Redux 對我來說太難上手了,由於開發時間緊迫,學習和試用 Redux 實在太艱鉅了。
在此過程中,我得到 Günter Zöchbauer 和 Collin Jackson等多位 Stack Overflowers 針對架構相關問題的討論串啟發,讓我接觸到 Flutter 的社群。
社群
與 Flutter 有關的開源社區非常多元化,給予我很大的支持和希望(特別是在這個非常時期)。
讓我舉個例子,我使用了 Romain Rastel 的 flutter_slidable 套件,並提出了改進建議。他在 48 小時內就實作了解決方案,而且比我提出的更好⋯⋯ 這種經驗在 Flutter 社群中並不罕見。
唯一遺憾的是,由於我一直忙於移植,所以在我收獲建議的同時無法給予更多回饋。
效能
總體而言,進行內部測試的用戶對移植 Flutter 的成果感到高興。在同一 iOS 設備上,與 Flutter App 一起運行原生 iOS App 時,效能並沒有明顯的下降。
這個 App 沒有做很多繁重的工作,但其中一個 Flutter App 表現明顯較佳的地方,就是從數百個 JSON 文件讀取數據、並對該數據進行一系列浮點計算時,Flutter App 明顯比較迅速。我沒有花時間深入了解,所以我無法肯定地說當中的差異是在於檔案讀取函式、JSON 解析、還是日期處理中,但我覺得這是 Flutter 引以為傲的地方!
我期待能看到其他人提出相關的基準數據。
語言
Flutter 使用 Dart 語言開發,它曾遭 Google 冷落,但近年隨著 Flutter 漸受關注後再次獲重視。
然而,這種語言十分成熟又易於學習,我很幸運在 Dart 2.0 變得如此強大時開始使用,所以不再需要使用關鍵字 “new” 來不斷地編寫程式碼。
Dart 可能沒有 Swift 和 Kotlin 所擁有的 nullable/non-nullable 型別,但我已經開始喜歡它的簡單。例如:
- 使用前導下劃線 (leading underscore) ‘_’ 就可以將函式設為 private;
- Automatic formatting,為我減少了一件頭痛的事,而且我喜歡使用 trailing comma 將參數放在單獨一行上;
- 套件管理簡單。
我可以繼續細數我喜歡的特性,但不是所有人都會喜歡。我重申,對我而言,最重要的是我可以更快地(而且更愉快地)實作不同功能。
不足之處
老實說,它的不足之處並不多。
在原生 iOS App 中:
- 我在 Xcode 中使用了 6 個 targets,結合 #defines 來創建具有不同 bundle ID 的 App;但我不知道如何在 Flutter 這樣做。
- 我在 iOS 中使用了一個很好的第三方 logging 框架 CocoaLumberjack;但目前在 Flutter 尚未找到替代品。
- 我在 UIKit 框架中使用 WKWebView 來加載和呈現本地 HTML 文件(例如使用條款、隱私政策);但我無法在 Flutter 找到執行此操作的套件。我最終使用了 html2md 這個套件,將 HTML 轉換為 Markdown 格式,並用另一個套件 flutter_markdown 來渲染 Markdown。
- 我使用了 AVFoundation 框架來結合 barcode 掃描、 QR code 掃描、和在單個非全屏視圖中拍照的功能;雖然在 Flutter 中 camera package 適用於照片,但我還是沒法獲得同樣的細緻化控制 (Fine grained control)。另外,facundomedica 編寫了一個很棒的掃描套件 fast_qr_reader_view,在 Android 設備上,這個套件將 Firebase 的 ML Kit 與 Camera package 中的即時圖片整合在一起。
結論
如果你已經閱讀本篇教程至此,那麼應該明白我大力讚賞 Flutter 的原因。
到目前為止:
- Flutter UI 幾乎與原生 Android 和原生 iOS UI 沒有差異;
- Flutter 的 UI 構建方式,讓我在 Flutter 中能以比在原生程式碼中更快的速度構建新功能。
- 使用移植了的 App 時,測試人員未有報告任何性能下降的情況。
- 目前在 iOS 和 Android 版本的 App 之間,共享的 code base 大於 90%。雖然我尚未整合 Apple HealthKit 或 Google Fit,但我不認為這個百分比會怎麼下降。
雖然 Flutter 還未夠成熟(在 2018 年 5 月才發布第一個 “Ready for Production Apps” 的 Beta 3 版本),但它對我來說非常強大。我唯一遇到的問題是當 Dart 函式同步讀取目錄時,ListSync 在最新的版本中出現了問題。我已經將問題添加到 Github 上的 Flutter repository,並切換到異步版本。
歡迎任何意見回饋 😃。
FB : https://www.facebook.com/yishen.chen.54
Twitter : https://twitter.com/YeEeEsS