0%

【文章翻譯】Cross Platform development with Flutter — How Google Classroom gets teachers and students on the…

【文章內容使用 Gemini 1.5 Pro 自動產生】

使用 Flutter 開發跨平台應用程式 - Google Classroom 如何讓教師和學生步調一致

Google Classroom 應用程式最初於 2014 年推出,如今全球有 1.5 億教師和學生使用它來組織作業、成績和課堂通訊。該應用程式可在 Android 和 iOS 上使用,開發工作始於當年早些時候,並經歷了兩個行動平台的巨變時代。事實證明,管理這些分歧的變化帶來了挑戰。

儘管努力同步,但在 2021 年,也就是 7 年後,Classroom 應用程式的 Android 和 iOS 程式碼庫在功能、UI 和實作方面逐漸出現分歧。從最明顯的地方,例如屏幕對相同 UI 採取不同方法,到不太明顯的地方,例如驗證和應用程式啟動邏輯的差異;Classroom 逐漸變成一個難以維護和改進的應用程式,兩個程式碼庫給一個小型開發人員團隊帶來了沉重的負擔。

有許多選項可供選擇,從堅持現狀,到增加更多開發人員,再到使用跨平台框架完全重寫兩個程式碼庫。該團隊致力於改進,這排除了堅持現狀的選項,然後評估穩定兩個現有程式碼庫需要做些什麼,這排除了僅添加更多開發人員的方案。最終,團隊選擇了第三個方案:使用單一來源、跨平台的解決方案重新構想 Classroom,而這個方案最終是 Flutter。

Flutter 如何簡化 Classroom 應用程式

不一致的 UI

Classroom 最明顯的問題 - UI 變異 - 強迫教師熟悉 Android 和 iOS 的 UI。畢竟,很容易想像學生會問關於作業屏幕和說明的問題,而一個平台上的說明在另一個平台上對於學生來說可能毫無意義。

傳統方法是由不同的團隊開發的獨立的客戶端應用程式,這些應用程式會隨著時間推移而出現分歧。只有對每個功能進行一致而艱苦的同步工作才能阻止這種情況。相比之下,Flutter 的本質顛覆了這種預設結果。使用 Flutter,UI 預設情況下是相同的 [1],直到主動工作(通常為了適應性)迫使它們為了使用者利益而出現分歧。

[1] 在 Classroom 的 Flutter 客戶端中,保留了一些細微的故意差異,例如系統欄和底部控制項。Flutter 保留了這些平台特有的細節,同時將屏幕中央 90% 的區域留給單個 UI 實作來填充。

混亂的業務邏輯

Classroom 的 Android 和 iOS 客戶端之間存在著差異,這不僅僅是 UI 的問題。受伺服器端解決方案的影響,該方案將一些複雜的業務邏輯轉移到客戶端,舊版的 Classroom 應用程式還處理核心實作方面的差異。除了造成偶爾的平台特定錯誤(這可能會讓工程師難以重現!)之外,這還給任何評估兩種實作正確性的人都帶來了相當大的認知負擔。

使用 Flutter 重寫 Classroom 解決了一系列錯誤,包括先前報告的和未報告的錯誤,這純粹是 Flutter 如何處理原生平台互動的結果。

在原始程式碼中,多年的持續開發偶爾會模糊 UI、業務和平台特定邏輯之間的界限。這意味著使用者的錯誤報告幾乎總是需要付出巨大的努力才能隔離,因為整個呼叫堆疊都可能存在問題。是否由於檔案系統讀取錯誤、業務邏輯通信錯誤,或者由於 UI 接收了檔案但隨後遺失了檔案而導致請求的檔案無法載入?唯一的解決方法是 調查所有內容

當然,Flutter 開發人員可以像其他人一樣混淆這些界限並混合邏輯,但 Classroom 工程團隊發現,遵循框架最佳實務使這種嘗試變得十分明顯。Flutter 的聲明式 UI 系統強烈建議不要將業務邏輯意外放置在 UI Widget 中,而新的 MVVM 架構甚至有助於在位於 Flutter Widget 背後的龐大程式碼庫中強制執行明確的責任層級。

Flutter 應用程式仍然會定期與底層平台進行通信 - 畢竟,上傳和查看作業的使用者旅程離不開檔案系統 - 但 Flutter 將平台特定邏輯隔離到專用 Plugin 的模式再次阻止了例行磁碟 I/O 偷偷溜進不應該出現的地方。以下範例展示了 Flutter 應用程式在不混淆整個呼叫堆疊的情況下存取檔案系統的一種實際方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import "dart:io";
import "package:path/path.dart" as path;
import "package:path_provider/path_provider.dart" as path_provider;

// 載入學生為特定作業儲存的作業。
// 回傳值的 exists() 函式會在學生將作業交給狗狗吃掉的情況下返回 False。
Future<File> getHomework(Assignment assignment) async {
// 使用 `path_provider` 套件將平台特定的檔案系統怪癖抽象化
final Directory homeworkDirectory =
await path_provider.getApplicationSupportDirectory();

// 提取學生上傳的作業
return File(
path.join([homeworkDirectory.absolute.path, assignment.name]),
);
}

這個範例很簡單,Classroom 工程團隊最終開發了他們自己的幾個 Plugin,以包含更複雜的與主機平台的互動。有趣的是,這麼做讓他們的原生程式碼 更容易除錯,比他們最初的原生應用程式更容易。這是怎麼做到的呢?在 Flutter Plugin 中遵循「不要重複自己 (DRY)」原則意味著將儘可能多的業務邏輯提升到 Dart 程式碼中,只為原生互動保留最簡單的進出方法呼叫。這會強制在領域邏輯和平台邏輯之間建立堅如磐石的分隔;這意味著 Classroom 的 Android 或 iOS 程式碼中的任何錯誤都可能出現在隔離的單一責任函式中,這些函式很容易進行推理。

效能消耗

當使用者旅程失敗時,就會提交一個具體的錯誤,所有人都同意需要解決。但對於更微妙的問題,例如應用程式啟動時間隨著時間推移越來越慢,自應用程式推出以來每年都在惡化,該怎麼辦?再加上對保持多個客戶端同步的擔憂,突然之間,對應用程式啟動流程的緩慢進行故障排除感覺就像一項無望的任務。

在這裡,Flutter 通過自身速度足够快,不至於加剧问题,更重要的是,它提供了從頭開始的機會。Classroom 團隊知道他們是在建立新的東西,而不是繞過多年開發積累的骨架,他們通過移除冗餘的 API 呼叫,並行化其他獨立的 API 呼叫,以及在所有內容解析完成時顯示閃爍效果和其他 UI 預覽,使他們的授權和啟動流程更加清晰明了。结果是應用程式启动时间惊人地 *减少了 80%*!

注釋功能

Classroom 的大部分功能可以看作是一個相當例行的應用程式,它將使用者圍繞共享內容(如作業和上傳的作業)聚集在一起。但有一個功能顯得 非常棘手。Classroom 的主要功能之一是檔案共享,教師和學生都可以創建、查看和編輯檔案,包括自由形式的注釋,就像在真紙上用筆或記號筆繪畫一樣。這個注釋共享功能已經存在於 Classroom 的原生 Android 和 iOS 客戶端中,因此將其移植到 Flutter 的任何摩擦都會成為阻礙。

Classroom 團隊能夠將這個注釋功能重新打包到一個 Plugin 中,該 Plugin 將平台特定實作委派給單獨的函式庫。對於檔案注釋,這些函式庫成為了 Google One、Google Keep 和舊版 Classroom 應用程式中已經使用的預先存在的原生函式庫的薄薄包裝。在內部,Android 和 iOS 對檔案共享有不同的實作要求。在 iOS 上,Classroom 應用程式通過原生視圖存取檔案,但在 Android 上,它會直接打開 Google Keep 應用程式。但是,良好的 Plugin 設計原則能夠將這些實作細節隔離,同時仍然為應用程式中的其他部分提供一個乾淨的、單一的 Dart API 來進行導航。最終,Classroom 中「最棘手」的功能之一已成功移植到 Flutter。

以下是 Classroom 在 Android 上的注釋功能的可視化,概述了原生和 Flutter UI 組件的混合。

四個並排的行動裝置屏幕,共同展示了選擇和注釋檔案的使用者流程

更籠統地說,典型的 Flutter Plugin 設計看起來像下面這樣,其中一個單一的、簡化的介面載入平台特定的函式庫,這些函式庫反過來又使用 FFI 或 JNI 與底層平台進行通信。這允許 Flutter 應用程式與所有構建目標的平台特定原生 API 互動,而不會將這些考慮因素洩露到 Dart 程式碼中。

Flutter Plugin 設計圖

回顧

開發速度

Classroom 團隊花費了兩年的時間來使用一個團隊重寫他們的應用程式,這個團隊從 1 名工程師(用於最初的原型設計階段)到開發高峰期的 10 名全職工程師不等。這不是一筆小投入,但團隊基於更快開發和維護的承諾做出了這個決定。Classroom 於 2023 年 6 月在 iOS 上推出了他們的 Flutter 重寫版本,並於 2024 年 1 月在 Android 上推出該專案。此後,用於新功能的平均工程師時間減少了三分之二,這意味著開發速度提高了三倍!在等了兩年之後,利益相關者對期待已久的 ✨更快功能開發✨ 感到非常高興。

Classroom 團隊決定重寫的部分原因是,他們知道他們的專案永遠不會「完成」,新的功能很可能會在很長一段時間內不斷添加。這為一個有說服力的案例提供了支持,即重寫,即使是一項昂貴的重寫,最終也會得到回報。Classroom 團隊計算他們使用 Flutter 重寫應用程式後何時才能實現投資回報的公式是:

估算使用 Flutter 重寫應用程式後,提高的速度何時會超過重寫時間的公式。公式為重寫時間除以使用 Flutter 編寫功能所需時間的減少量,等於達到盈虧平衡點的發佈後功能數量。

在 iOS 推出 9 個月後,Classroom 估計已經通過 Flutter 提供的開發速度提高三倍,從而收回了最初投資的 40%。

開發人員體驗

除了利益相關者之外,唯一比開發速度提高更讓 開發人員自己 高興的是 - 開發人員自己。對於 Classroom 團隊來說,他們 3 倍的速度增長來自於僅需編寫一次每個功能(或者至少 1.5 次,在具有大量原生組件的情況下),消除了協調兩個團隊的成本,而這兩個團隊的時間表通常相差數月,當然還有熱重載。熱重載本身及其大約 99% 的重建時間減少,讓 Classroom 團隊的士氣比他們使用兩個原生客戶端時更高。最終,Classroom 團隊發現,在他們轉向 Flutter 之後,很容易吸引和留住工程師。

此外,Classroom 團隊發現,平均來說,新的功能需要減少至少 50% 的程式碼行數才能實現。實際上,減少量可能要高得多,因為他們在重寫過程中建立的每個功能在兩個原生客戶端中都沒有真正實現!換句話說,50% 的程式碼行數實現了所有舊的功能,還覆蓋了 大量 功能缺口(包括 iOS 上的離線支援)。

總結

在開始重寫大約兩年後,Classroom 團隊將他們的應用程式發佈到 Android 和 iOS,並添加了足夠的額外功能,以償還他們前期投資的 40%。他們的全新應用程式啟動速度是舊版本啟動速度的 五倍,為開發人員和最終使用者節省了時間和挫折。展望未來,新的功能平均開發成本是舊狀況的 三分之一,可以在兩個平台上同時發佈,並且 更容易 除錯和維護。最終,在 Classroom 切換到 Flutter 以重新投資他們的 未來 後,使用者、開發人員和利益相關者的士氣達到了前所未有的高度。


使用 Flutter 開發跨平台應用程式 - Google Classroom 如何讓教師和學生步調一致 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。