嗨嗨大家!👋 讓我們今天一起再跳入 Flutter 的魔法世界,不過這次我們的主題會稍微進階一些——「Clean Architecture」。記得昨天我們聊到了 SOLID 原則嗎?那只是個開始。如果你正在想怎麼讓你的 Flutter 項目更加組織有序、更加容易維護,這篇文章是給你的。
在昨天的介紹裡,我們深入探討了 SOLID 原則,了解了它如何成為軟體設計的核心基石。今天,我們要進一步看看如何在 Flutter 中實踐 Clean Architecture。首先,我們要明確 Clean Architecture 的目的:它希望能建立一個獨立於框架、可測試、獨立於 UI 的架構。這樣一來,應用程式的業務邏輯和外部界面就能夠獨立發展、維護。
一樣先開始介紹先擺上最常見的這張圈圈圖,理解 Flutter 的功能與 Clean Architecture 之間的對應關係。

下面我們拿購物車來做一個完整舉例:
💡 完成購物車必須具備以下的任務,選擇商品與數量 → 點擊確認 → 完成更新購物車
首先最重要的實體就是 購物車,因此我們會定義一個 model cart.dart,代表最核心的實體。
class Cart {
	Map<Item,int>  cartStorage;
}
接下來考慮 use case,也就是我們的購物車會有哪些要完成的功能,寫成 cart_service_interface.dart,裡面包含 CartService 需要的功能,這裡的例子就是更新購物車。
寫成 abstract 的好處也有助於我們在開始完善程式碼前,先想清楚這個 Service 會有哪些功能需要被完善。甚至你在完善 Repository 之前,也可以先寫上然後後續再補上 Repostory 。
abstract class CartService {
		void updateCart(Item, amount);
		List<Item> getItems();
}
class CartService {
	CartService(cartRepository);
	List<Item> getItems(){
		return cartRepository.getItems();
	}
	void updateCart(item, amount) {
		cartRepository.updateCart(item, amount);
	}
}
在實作中加入 repository 操作 db(local) 或 api(remote)
abstract class CartRepository {
		void saveChart(List<Item> items);
		List<Item> getItems();
}
class LocalCartRepository implements CartRepository {
  @override
  void saveCart(List<Item> items) {
    // 在這裡實現 DB 存儲的邏輯
  }
 @override
  void getItems(List<Item> items) {
    // 在這裡實現從 DB 取得購物車資料
  }
}
最後在我們來完善 Controller,想像一下用戶實際操作 UI 會是什麼樣的場景,可能他會有一個加入購物車的按鈕,所以我們加上 addCart 的方法,檢查一下操作是否正常,最後更新資料庫。這裡假設如果他要加入購物車的數量,已經大於商品本身的剩餘數量,會拋出錯誤。
class CartController {
	Future addCart(Item item,int amount) async {
		if(amount > item.avalible){
			throw ItemNotEnoughException();
    }
		await cartService.updateCart(item, amount);
	}
}
UI 會根據 Controller 收到的訊息來顯示
TextButton(
  onPressed: () async {
    try {
      await cartcontroller.addCart(item, amount);
      showToast("商品已加入");
		} on ItemNotEnoughException {
      showToast("選擇數量大於剩餘數量");
    }catch (e) {
      showToast("加入購物車失敗");
    }
  },
  child: const Text(
    "加入購物車",
  ),
);
至此我們完成了一個完整的功能模組,當然在每個專案不同搭配上不同的管理工具,在使用上可能會有些許變化,但都可以遵循上面的原則去完成。
上面實踐的方法,有借鑑和參考這篇文章,強烈建議大家有空也可以去拜讀,會收穫滿滿。
https://codewithandrea.com/articles/flutter-app-architecture-riverpod-introduction/
講完程式碼,比程式碼本身更抽象的就是資料夾結構,這裡當作額外 Bonus 介紹幾種資料夾結構跟大家分享。
優點:
缺點:
lib/
│
├── ui/
│   ├── pages/
│   │   ├── home_page.dart
│   │   └── ...
│   └── widgets/
│
├── application/
│   ├── service/
│   └── repository_interfaces/
│
├── data/
│   ├── models/
│   ├── datasources/
│   └── repositories/
│
├── infrastructure/
│   ├── network/
│   ├── local_db/
│   └── ...
│
└── main.dart
優點:
缺點:
lib/
│
├── features/
│   ├── feature1/
│   │   ├── data/
│   │   ├── application/
│   │   │   ├── service/
│   │   ├── domain/
│   │   └── presentation/
│   ├── feature2/
│   │   ├── data/
│   │   ├── domain/
│   │   └── presentation/
│   └── ...
│
├── core/
│   ├── errors/
│   ├── service/
│   ├── entities/
│   └── ...
│
└── main.dart
目前我們的團隊是採用第二種 Feature-Based 結構為基礎,確實一定程度上可以讓分工更容易,但這完全取決於你們不同的工作模式,歡迎大家參考分享。
寫到這裡,希望透過這篇文章,你能夠有一個更深入的認識和了解 Clean Architecture 在 Flutter 的應用。正如我們所見,將清晰的架構帶入到 Flutter 中,不僅使得代碼更加模組化、可維護,還能夠確保業務邏輯和 UI 之間的獨立性。但這只是冰山一角,實際在項目中可能會遇到更多複雜的場景和挑戰。無論如何,只要堅持學習和實踐,我們總會找到最適合的方案!期待下次再和大家分享更多的知識和經驗!🚀👩💻👨💻🎉