美妙的 Dart 前後端體驗,專為 Flutter 設計的 Serverpod 後端框架
Dart 後端是個強大的選項
English version: https://medium.com/flutter-community/wonderful-dart-full-stack-experience-serverpod-framework-designed-specifically-for-flutter-ee486df28025
當一個應用程式開始擁有更多的互動需求或需要儲存、處理大量數據時,單靠 Flutter 前端已經無法滿足。這時,Flutter 開發人員通常需要依賴伺服器來承載更多複雜的功能,特別是像筆記、聊天等應用程序,需要將資料儲存在線上。很多開發者會選擇 Firebase 等無伺服器服務,這樣的解決方案確實方便,但當遇到需要在伺服器上進行複雜計算時,就可能顯得不足。在這樣的情況下, Serverpod 為 Flutter 開發者提供了一個理想的選擇,它被稱之為 “The missing server for Flutter”,是一個專為 Flutter 設計的完整後端框架,能在 Dart 中編寫伺服器端代碼,並自動生成 API 與資料存取,讓 Flutter 專案無縫銜接後端需求,提供一致的開發體驗。
像 Serverpod 這樣的可擴展伺服器旨在應對日益增加的需求,同時維持穩定的效能。這種擴展能力在高負載情況下能確保快速的回應時間,提升使用者的體驗,讓開發人員保持滿意。
Serverpod 成立於 2021 年,總部位於瑞典。2024 年 2 月 15 日表示:「我們籌集了相當可觀的預種子資金,因此我們的 Serverpod 團隊目前有4名全職工程師(希望再增加兩名),擁有至少兩年的資金。 🚀 而且,它都是開源的,所以即使我們失敗了,你也可以訪問你需要的一切。」。(在 2024 年 11 月團隊人數為 7 人)
官網#
安裝#
套件#
官方消息#
為什麼開發者要選擇 Serverpod 實現後端?以下特點你需要了解:
Serverpod 特點與功能#
Unified Experience 統一語言體驗#
Serverpod 前後端皆採用 Dart 語言,讓開發者能以單一語言撰寫整個應用,減少跨語言切換的認知負擔,提升開發效率。
Code Sharing 前後端程式碼共享#
支援前後端共用程式碼,例如:資料類別、服務類別、例外類別。讓開發流程更為簡化,減少重複,節省成本並提供一致且易於維護的程式碼庫。
Type Safety 類型安全#
Serverpod 提供類型安全的環境,確保程式碼中的錯誤可以在編譯期間即時偵測,沒有 dynamic 類型,減少運行時錯誤的機會,增強代碼的可靠性和穩定性。
Codegen 程式碼生成#
Serverpod 可根據 yaml 的配置描述自動生成 Server 和 Client 的程式碼,包含 HTTP 請求、Scoket、回應和錯誤管理,簡化了網路通訊與資料的序列化和反序列化。這讓開發者可以專注於應用功能的開發,而無需深入了解底層網路實現的細節。
提供了高階方便的 API 給開發者,前端跟後端的溝通只需要一般的函式方法即可,就像在本地呼叫一樣,很簡單即可實現兩端的無縫互動。
Postgres SQL 資料庫#
Serverpod 與 Postgres SQL 深度整合,提供高度可擴展的資料庫管理,適合處理大量數據和多層級的應用需求。
ORM (Object-Relational Mapping) 資料庫操作#
Serverpod 提供了一個類型安全且支援空安全的 Dart ORM,讓開發者可以直接使用 Dart API 進行資料庫操作,減少了傳統 SQL 查詢中的錯誤機會,並提升了應用的穩定性。此外,Serverpod 的 ORM 支援連線池技術,透過重複利用資料庫連接,避免頻繁開啟或關閉連接的高昂開銷,有效縮短了回應時間,進而提升效能。
Datebase Migration 遷移工具#
內建的 Migration 無需手動撰寫 SQL 語法,即可進行資料庫結構的調整。Server 在啟動時會檢查資料庫的完整性,當新增或修改 Model 後,會自動提供包含 SQL 查詢的遷移方式。
Efficient Caching 高效快取#
Serverpod 透過內建的本地與 Redis 快取機制,使用分散式緩存,將頻繁存取的資料儲存在記憶體中來降低資料庫成本並縮短回應時間。此功能對於基本上保持不變的資料非常方便,允許應用程式快速提供這些資料,而無需每次都重新取得。節省成本外,即時返回 API 結果。
Data Streaming 資料流#
實現即時溝通與同步,適合需要即時通訊的應用場景,如遊戲或聊天室等。能夠取代 Web Socket 的複雜設定,後端函式 API 可以直接返回一個 Stream,前端持續監聽資料並接收,讓開發者更容易實現雙向數據同步。
Social Auth Support 社群登入支援#
Serverpod 提供多種社交登入選項,支援 Google、Apple、Firebase 等平台的身份驗證,也支援傳統的 Email 登入。與 Google API 完整整合,讓開發者能使用 Google 日曆等服務。
File Upload 檔案上傳#
Serverpod 支援將文件上傳至 Google Cloud Storage(GCS)與 Amazon S3 等平台。
Task Scheduling 任務調度#
Serverpod 包含一個任務調度系統,以 Future Call 取代複雜的 cronjob。可在特定時間或延遲後安排任務,確保後台操作不會幹擾使用者體驗。即使在伺服器重啟後,任務依然有效
Serverpod Insights 日誌監控與健康檢查#
Serverpod 的日誌系統支援查看網路、資料庫的狀況。可以快速查明異常並識別緩慢的資料庫查詢,還能監控 CPU 和 Memory 指摽。讓開發者能夠在效能問題影響使用者之前修正它們。隨著用戶的成長,這種主動的監控和調試方法對於維持高效能後端至關重要。
根據本文撰寫的 Todo 應用範例,當請求發生錯誤後 Insight 都會列出相關資訊與錯誤訊息,對於 Debug 上來說非常便利。
透過圖表了解目前的連線數量、回應時間、CPU 使用狀況以及記憶體資訊。
VSCode Extension 擴充套件#
Serverpod 的 VSCode 擴充套件讓開發者在編寫 yaml 配置檔案時,獲得即時回饋。它包含語法診斷和警告提示,確保配置過程的正確性,並大幅降低設定錯誤的可能性。
Deployment 輕量靈活部署#
Serverpod 支援 Docker 和 Terraform,可在任何平台自行託管。開發者可以透過簡便的腳本快速將應用部署至雲端平台,提供輕量化且自動化的部署流程。
內建 Web 伺服器#
Serverpod 內建 Web 伺服器,可提供基礎的網頁服務,滿足一般的網站部署需求,無需額外配置。
此功能在實驗階段,還沒有穩定發布
Serverpod Cloud 雲端設施#
Serverpod 提供了一整套雲端基礎設施,免去開發者自行維運 Google Cloud 或 AWS 的複雜性,讓部署變得更簡單且成本更低。這樣的設計大大減少了後端架構的開發與維護難度,讓開發者能夠專注於前端的開發需求,而無需投入大量學習成本來理解和操作傳統的雲端服務。
主要優點:
- LoadBalancer → 支援自動負載均衡,確保伺服器在高流量情況下的穩定性,提升應用的可用性。
- Scalable Database → 提供可擴展的資料庫架構,隨著流量增長自動進行水平擴展,滿足大型應用的需求。
- File Upload→ 內建檔案上傳功能,讓使用者能方便地上傳並管理檔案。
- Cache → 支援快取系統,提升資料的存取效率,減少伺服器負載並加快回應速度。
- Inter-Server Communication → 提供伺服器間通訊功能,方便微服務架構的實現,使服務之間能夠快速協同運作。
- Secure Password Storage → 支援密碼的安全存儲,確保用戶資料的隱私與安全。
- In-Memory State Storage → 支援在伺服器記憶體中進行狀態儲存,適合應用於即時性要求較高的功能,例如聊天或遊戲。
- Multi-Region Deployment → 支援多區域部署,確保使用者能在距離最近的伺服器獲取數據,有效縮短回應時間。
部署指令:
serverpod cloud deploy
預計在2024年底或是2025年初釋出
Testing Framework 完整測試框架#
在 Serverpod 2.2 中,開發者可以撰寫全面的整合測試。其中包含幾個特點:
- 直接在測試中請求 API Endpoint
- 輕鬆為資料庫新增 Seed 資料
- 模擬用戶的身份驗證
- 跳過
tearDown()進行手動釋放和重置,一切都會自動 rollback。
框架的核心入口是 withServerpod() 測試助理,提供了自動配置和清理資料庫狀態的工具。透過在 transactions 中執行所有測試,並且確保每次測試完成後都重新開始,從而實現更快、更可靠的整合測試套件。
void main() {
withServerpod('Given Example endpoint', (sessionBuilder, endpoints) {
test('when calling `hello` then should return greeting', () async {
final greeting = await endpoints.example.hello(sessionBuilder, 'Michael');
expect(greeting, 'Hello Michael');
});
});
}
前置作業#
Serverpod 安裝#
在安裝 Serverpod 之前,確保系統擁有 Dart 最新版本以利用其所有功能。接著透過命令安裝 Serverpod CLI:
dart pub global activate serverpod_cli
Docker 安裝#
直接從官網下載即可,安裝完即可獲得 Docker CLI,允許在 Terminal 透過指令操作。
執行 Serverpod 時,需要確保 Docker Desktop 同時運行,它會負責啟動 Postgres 和 Redis,確保伺服器運作正常。
此外,熟悉 YAML 檔案會很有幫助,因為 Serverpod 依賴它們來進行配置。掌握 YAML 的結構和語法可以幫助你根據專案需求自訂 Serverpod 伺服器。也因為使用 YAML 格式,未來在支援其他程式語言或程式碼生成時,能提供更高的可讀性和擴展性。
版本檢查#
serverpod version
專案建立#
移動到想要建立專案的位置,使用 create 指令
serverpod create <project-name>
專案結構:
📁 API Client → xxx_client 目錄,存放從 Server 端生成的程式碼,讓 APP 端存取,是兩端溝通的橋樑
📁 Flutter App → xxx_flutter 目錄,實際運行的應用
📁 Server → xxx_server 目錄
endpoints/→ 每個檔案為一個 Endpoint,為 Client 與 Server 的連接點models/→ 模型定義檔,使用yaml撰寫屬性、細節,透過命令生成。與資料庫自動對應,前後端都能使用config/→ 包含每種環境下的服務配置,有 Api Server、Web Server、Database、Redis 等等deploy/→ 包含 AWS、GCP 的部署腳本與相關檔案
Model 建立#
使用 yaml 檔案定義 Model Class,撰寫起來速度快且可讀性高。在範例 Todo 專案可以新增一個 task.spy.yaml 檔案進行配置
class→ 名稱fields→ 內容欄位。可以使用最基本的 Dart 類型,或是 DateTime、List<T>、Map、UuidValue、Duration 還有 ByteData,選擇上很豐富
class: Task
fields:
id: UuidValue
name: String
isDone: bool
createAt: DateTime
完成後,使用 serverpod generate 生成 Model 類別程式碼,確保安全與穩定性。
Table 建立#
Model 物件可以輕鬆與資料庫綁定,讓儲存跟搜索變得更加容易。只需在 yaml 加上 table 名稱即可,它就是資料表。
class: Task
table: task
fields:
name: String
isDone: bool
createAt: DateTime
當資料表的相關內容在往後更新時,需要執行 Migration,在資料庫結構變化下保留原有資料。
serverpod create migration
要求:Serverpod 使用 Postgres 資料庫,如果沒有新增了資料表的話 ,Serverpod 將無法運作(除非運行的是 Serverpod mini)
Exception 建立#
與創建 Data Model 差不多,只是從 class 改成 exception,並給予裡面的屬性欄位。
exception: CreateTaskException
fields:
message: String
程式碼生成#
當 Server project 中的 endpoint/ 和 model/ 目錄有更新時,都需要重新生成相關的 API 程式碼。可以使用一行指令
serverpod generate
// 監視Server目錄的改動,即時生成程式碼與檔案
serverpod generate --watch
Serverpod 也有支援 Hot Reload,當有更新後可以直接使用 command + s 進行儲存,此時如果有跑 —watch 模式,Serverpod 就會即時生成相關程式碼。
當執行 serverpod generated 後,Serverpod 會生成 Endpoint 和 API 相關程式碼至 Client project 的 protocol/ 目錄和 Server project 的 generated/ 目錄,接著我們可以從 Flutter App 輕鬆存取他們。
伺服器運行#
- 先開啟 Docker Desktop App
- 在 Server 目錄使用 Docker 指令啟動 Postgres 和 Redis。
- 然後,執行指令啟動伺服器。
docker compose up --build --detach
dart bin/main.dart --apply-migrations
在開發環境中,始終使用參數--apply-migrations 會很有幫助,因為這將確保資料庫始終遷移到最新狀態。但是,在正式環境中,通常應該在沒有參數的情況下啟動伺服器,除非需要更新、遷移到新版。
當資料庫結構有更新並遷移後,執行伺服器啟動,並在更新遷移完後關閉伺服器。給予 --role maintenance 代表完成任務後關閉
dart bin/main. dart --apply-migrations --role maintenance
在某個時刻停止 Docker 容器,只需運行
docker compose stop或使用 Docker 桌面應用。
⚡️ Serverpod 基礎範例#
Server Endpoint#
- 每個 Endpoint 命名都以 Endpoint 為後綴,生成之後的介面會自動省略後綴。並且必須繼承 Endpoint
- 每個 API 方法都必須回傳 Future 類型,以非同步執行
- 第一個參數都必須是
Session。保存正在進行的請求資訊,其中包含有關當前溝通的資訊和一些輔助API,可以透過它存取資料庫、快取和其他功能
import 'package:serverpod/serverpod.dart';
class ExampleEndpoint extends Endpoint {
Future<String> hello(Session session, String name) async {
return 'Hello $name';
}
}
Flutter Client#
var client = Client('<http://$localhost:8080/>')
..connectivityMonitor = FlutterConnectivityMonitor();
var result = await client.example.hello('World');
⚡️ Serverpod 範例 — Todo App#
Server Setup#
Model: Task
class: Task
table: task
fields:
name: String
isDone: bool
createAt: DateTime
Endpoint: Todo
class TodoEndpoint extends Endpoint {
Future<Task> createTask(Session session, Task task) async {
final createdTasks = await Task.db.insert(session, [task]);
return createdTasks.first;
}
Future<List<Task>> getTasks(Session session) async {
return await Task.db.find(session);
}
Future<Task> updateTask(Session session, Task task) async {
await Task.db.update(session, [task]);
return task;
}
Future<void> deleteTask(Session session, Task task) async {
await Task.db.delete(session, [task]);
}
}
執行 serverpod generate 指令生成 Server 與 Client 程式碼。
啟動 Server 程式,運行在本地的 Api Server、Database 和 Redis。
cd example_server/
docker compose up --build --detach
dart bin/main.dart --apply-migrations
Client Implementation#
使用後端 API 的過程中,不需要處理資料解析、理會序列化操作,實現無縫體驗。
與 Server 端進行連線,本地測試預設都是 http://$localhost:8080/,實際再依照自己的環境進行替換即可。
var client = Client('<http://$localhost:8080/>')
..connectivityMonitor = FlutterConnectivityMonitor();
在 Flutter 開發中,我們一致透過 client 物件跟 Server 溝通,透過它存取資料庫、資料表,做任何事情。因為 Server 創建了 TodoEndpoint,在執行 serverpod generate 生成後,我們可以使用 client.todo 進行 Todo 資料表的相關操作。
📍取得所有事項
void getTasks() async {
final result = await client.todo.getTasks();
setState(() {
tasks.addAll(result);
});
}
📍新增事項
void createTask() async {
final taskName = _textEditingController.text;
final task = Task(
name: taskName,
isDone: false,
createAt: DateTime.now(),
);
try {
final createdTask = await client.todo.createTask(task);
setState(() {
tasks.insert(0, createdTask);
_textEditingController.clear();
});
} catch (e) {
if (!mounted) {
return;
}
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(e.toString())));
}
}
📍更新事項
Future<void> updateTask(int index) async {
final task = tasks.elementAt(index);
final newTask = task.copyWith(isDone: !task.isDone);
try {
setState(() {
tasks[index] = newTask;
});
await client.todo.updateTask(newTask);
} catch (e) {
setState(() {
tasks[index] = task;
});
if (!mounted) {
return;
}
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(e.toString())));
}
}
📍刪除事項
void deleteTask(int index) async {
final task = tasks.elementAt(index);
try {
setState(() {
tasks.remove(task);
});
await client.todo.deleteTask(task);
} catch (e) {
setState(() {
tasks.insert(index, task);
});
if (!mounted) {
return;
}
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(e.toString())));
}
}
Demo#
運行伺服器和 Flutter App,執行 Todo CRUD 操作,同時檢查資料庫是否有正常更新。
每當進行一次請求操作,Server 都會有即時的 Log 資訊:
可以透過 Postico 軟體確保資料反映在資料庫中:從專案的 config/ 目錄查看 development.yaml 和 passwords.yaml 檔案,裡面有記錄資料庫連線的相關資訊。
就是這樣,我們簡單完成了使用 Serverpod 和 Flutter 建立全端 Todo 應用程式。
🧰 Serverpod Mini#
v2.0 Dreamscape 版本釋出,簡化過的輕量版本。如果沒有資料庫與 Docker 需求,就會適合使用,並且能輕鬆部署到 Google Cloud Run。
適合:
- 不需要 Postgre SQL 資料庫
- 不使用 Postgre SQL,使用其他資料庫
- 不需要 Docker
serverpod create --mini <project-name>
到這裡,就可以開始進行簡易的後端開發了!
如果因為需求要升級成 Serverpod 完整版本也很簡單。需要使用 DB 相關功能,首先切換到 server/ 目錄並使用以下指令:
serverpod create .
成功後,會再專案看到 config 目錄、deploy 目錄以及 Docker 相關檔案。
在升級之前,最好備份相關檔案,因為原本手動建立的檔案可能會被覆蓋。
⚡️ Serverpod Mini 範例 — Todo App#
Server Implementation#
Model: Task
class: Task
fields:
id: UuidValue
name: String
isDone: bool
createAt: DateTime
Endpoint: Todo
Serverpod Mini 沒有資料庫支援,將資料短暫儲存在內部記憶體,不需要永久保存。
final tasks = <Task>[];
class TodoEndpoint extends Endpoint {
Future<Task> createTask(Session session, Task task) async {
tasks.add(task);
return task;
}
Future<List<Task>> getTasks(Session session) async {
return tasks;
}
Future<Task> updateTask(Session session, Task task) async {
tasks[tasks.indexWhere((t) => t.id == task.id)] = task;
return task;
}
Future<void> deleteTask(Session session, UuidValue id) async {
tasks.removeWhere((t) => t.id == id);
}
}
執行 serverpod generate 指令生成 Server 與 Client 程式碼。
最後使用 CLI 啟動 Server 程式,運行在本地的 8080 Port
cd example_server/
dart bin/main. dart --apply-migrations
Client Implementation#
📍取得代辦事項
void getTasks() async {
tasks.clear();
final result = await client.todo.getTasks();
setState(() {
tasks.addAll(result);
});
}
📍新增事項
void createTask() async {
final taskName = _textEditingController.text;
final task = Task(
id: UuidValue.fromString(const Uuid().v6()),
name: taskName,
isDone: false,
createAt: DateTime.now(),
);
try {
setState(() {
tasks.insert(0, task);
_textEditingController.clear();
});
final _ = await client.todo.createTask(task);
} catch (e) {
setState(() {
tasks.remove(task);
});
if (!mounted) {
return;
}
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(e.toString())));
}
}
📍更新事項
Future<void> updateTask(int index) async {
final task = tasks.elementAt(index);
final newTask = task.copyWith(isDone: !task.isDone);
try {
setState(() {
tasks[index] = newTask;
});
await client.todo.updateTask(newTask);
} catch (e) {
setState(() {
tasks[index] = task;
});
if (!mounted) {
return;
}
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(e.toString())));
}
}
📍刪除事項
void deleteTask(int index) async {
final task = tasks.elementAt(index);
try {
setState(() {
tasks.remove(task);
});
await client.todo.deleteTask(task.id);
} catch (e) {
setState(() {
tasks.insert(index, task);
});
if (!mounted) {
return;
}
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(e.toString())));
}
}
Demo#
實際效果跟上一個範例相同,差異只是資料儲存在資料庫還是記憶體裡。
⚡️ Serverpod Mini 範例 — Todo App by Stream#
實作上使用 Stream 以及訊息通道的接收,其實概念都差不多,結尾有提供範例 Github 連結。
常見 Q&A#
Serverpod 是免費的嗎?#
是的,它是開源的。可以隨意 Fork 並優化它,隨意部署到自己理想的雲端平台上(例如:AWS)。同時,Serverpod 提供的 Serverpod Cloud 可以讓用戶直接部署,而這部分就會因為有使用而需要收費。
與 Firebase 的差別?#
- 使用 Firebase 需要將所有後端邏輯撰寫在前端應用裡,無法做有效區隔。Serverpod 可以確保前後端乾淨,各自處理邏輯,只需要 API 呼叫即可
- 隨著時間與規模擴大,Firebase 的成本提升很快,也很難有乾淨的架構進行維護。Serverpod 在靈活性與往後擴展的部分比 Firebase 方便很多,很容易整合其他服務
- Serverpod 可以進行即時存取,但可以選擇是否要使用資料庫,功能都是分開獨立
- Serverpod 可以託管在任何雲端平台,AWS、GCP 等等。甚至使用 Serverpod Cloud,可允許直接部署到 Serverpod 雲端,只需簡單的配置和幾行指令
未來釋出與路線圖#
💡 Inherited Models#
關於 API 的回傳值,可以給予 Sub Class。或是使用 Stream 即時溝通時,可以避免使用 dynamic 動態資料,可以使用繼承類別,給予嚴格的強型別回傳值。
💡 Web Server#
能部署網站,提供網頁給用戶瀏覽
結論#
Serverpod 作為 Flutter 專屬的後端解決方案,帶來統一性和便利性,讓開發者可以專注於應用邏輯的實現,而非繁瑣的後端基礎設施。這樣的框架使 Flutter 開發者不僅能打造優秀的前端體驗,更能以 Dart 後端實現完整的產品生態。我自己的小專案都有使用,真的不錯呢!
GitHub 程式碼#
參考與學習資源#
- https://www.youtube.com/watch?v=FwttjcKyWFk&ab_channel=Serverpod%F0%9F%9A%80
- https://www.youtube.com/watch?v=zZCqpm4ZNbQ&ab_channel=HeyFlutter%E2%80%A4com
- https://www.youtube.com/watch?v=F7WKovEFdnw&ab_channel=Serverpod%F0%9F%9A%80
- https://medium.com/serverpod/serverpod-2-2-futurism-new-test-tools-and-a-sneak-peek-of-the-future-cab4d8bfdc20
- https://medium.com/serverpod/getting-started-with-serverpod-mini-58c71ffefa54
- https://suragch.medium.com/web-server-frameworks-for-dart-197a073299eb
- https://dinkomarinac.dev/dart-on-the-server-exploring-server-side-dart-technologies-in-2024
其他文章#
- 2025 最新 Flutter 書籍《Flutter勇者之書》
- Flutter December 2024 💙 Flutter Monthly
- Flutter November 2024 💙 Flutter Monthly
- Flutter October 2024 💙 Flutter Monthly
- Flutter September 2024 💙 Flutter Monthly
- Flutter August 2024 💙 Flutter Monthly
- Flutter July 2024 💙 Flutter Monthly
- Flutter 3.24 和 Dart 3.5 重點回顧!
- Flutter 實作 DeepLink 完整指南 ⎮ Part 4: 適配與掌握社交平台
- Flutter 實作 DeepLink 完整指南 ⎮ Part 3: Flutter 開發
- Flutter 實作 DeepLink 完整指南 ⎮ Part 2: Android 與 iOS 設定
- Flutter 實作 DeepLink 完整指南 ⎮ Part 1: 基本介紹
- Flutter June 2024 💙 Flutter Monthly
- What can I do with “Extension Types” in Dart?
- Flutter May 2024 💙 Flutter Monthly
- Flutter April 2024 💙 Flutter Monthly
- How to implement Shorebird Code Push in Flutter?
- Flutter March 2024 💙 Flutter Monthly
- Flutter February 2024 💙 Flutter Monthly
- Flutter January 2024 💙 Flutter Monthly
- Use Dart 3 to Improve Development Skills. More Examples and Tips.
- Flutter December 2023 💙 Flutter Monthly
- Flutter November 2023 💙 Flutter Monthly
- Get Familiar with Dart 3, Make your Life Easier!
本文原刊登於 Medium。