大家好,歡迎來到第七天!在 Day 6,我們建立了完整的狀態管理架構,深入探討了 Riverpod 2.0 的實戰應用。今天,我們將開始打造第一個核心功能:在首頁顯示活動列表。
這立刻引出了一個問題:資料從何而來?更重要的是,我們的 App 如何設計,才能在未來輕鬆地更換資料來源(例如從假資料換成真實 API),而不需要重寫整個 App?
今天,我們將深入探討一個極其重要的設計模式——Repository 模式,並理解它為何是我們軟體架構自由的基石。
在我們的專案中,Repository 模式是資料存取層的核心設計。透過這個模式,我們將資料來源的複雜性隱藏在抽象介面後面,讓業務邏輯層能夠專注於核心功能。
Repository 模式的實作讓我們能夠:
讓我們先想像一個「不那麼好」的設計:如果我們的 UI 層或業務邏輯層,直接去呼叫一個 FirebaseDataSource.getActivities()
或是 Http.get(...)
,會發生什麼事?
邏輯與實作綁死: 我們的業務邏輯現在與 Firebase 或某個特定的 API 緊緊地綁在一起。如果未來要更換後端服務,那將是一場災難。
難以測試: 我們無法在沒有真實網路連線的情況下,輕易地對業務邏輯進行單元測試。
違反單一職責: 業務邏輯層承擔了它不該知道的職責——它不應該關心資料是「如何」被獲取的,它只需要「得到」資料就夠了。
Repository 模式的核心,就是建立一個「抽象層」,像一個「中介者」或「翻譯官」,優雅地解決上述所有問題。它的魔法在於「契約式程式設計」:
定義「契約」 (The Contract):
我們首先在 Domain Layer (領域層) 中,定義一個 abstract class (抽象類別),你可以把它想像成一份「契約」或「介面 (Interface)」。這份契約只做一件事:宣告所有「活動 Repository」都必須遵守的規範。
履行「契約」 (The Implementation):
接著,我們在 Data Layer (資料層) 中,建立一個具體的 class,去 implements (實作/履行) 上面那份契約。這個 class 才是真正負責做事的傢伙。
這就引出了一個關鍵問題:
未來如果要把假資料換成真實 API,我們的 ViewModel 或 Domain 層需要改動嗎?
答案是:完全不需要。 因為我們的業務邏輯,永遠只依賴 Domain 層那份抽象的「契約」,它從頭到尾都不知道 Data 層的具體實作是什麼。我們可以隨意抽換 Data 層的實作,而上層建築毫無察覺。這就是「依賴反轉原則」的完美展現。
在傳統的緊密耦合架構中,業務邏輯直接依賴具體的資料來源:
// lib/features/activity/presentation/providers/activity_provider.dart
// (imports omitted)
// ❌ 緊密耦合的設計
class ActivityViewModel {
Future<List<Activity>> getActivities() async {
// 直接依賴 Firebase
final snapshot = await FirebaseFirestore.instance
.collection('activities')
.get();
return snapshot.docs.map((doc) => Activity.fromFirestore(doc)).toList();
}
}
這種設計的問題在於:
而 Repository 模式通過抽象層解決了這些問題:
// lib/features/activity/presentation/providers/activity_provider.dart
// (imports omitted)
// ✅ 解耦合的設計
class ActivityViewModel {
final ActivityRepository _repository;
Future<List<Activity>> getActivities() async {
return await _repository.getActivities();
}
}
Repository 模式讓單元測試變得輕而易舉:
// test/features/activity/presentation/providers/activity_provider_test.dart
// (imports omitted)
// 測試時可以輕鬆注入 Mock Repository
class MockActivityRepository implements ActivityRepository {
@override
Future<List<Activity>> getActivities() async {
return [Activity(id: '1', title: '測試活動1')];
}
}
// 測試程式碼
test('should return activities from repository', () async {
final mockRepository = MockActivityRepository();
final viewModel = ActivityViewModel(mockRepository);
final activities = await viewModel.getActivities();
expect(activities.length, 1);
});
有了 Repository 抽象層,我們可以輕鬆切換不同的資料來源:
// lib/features/activity/data/repositories/activity_repository_impl.dart
// (imports omitted)
// 本地資料來源
class LocalActivityRepository implements ActivityRepository {
@override
Future<List<Activity>> getActivities() async {
return await ActivityLocalStorage.getAllActivities();
}
}
// 遠端 API 資料來源
class RemoteActivityRepository implements ActivityRepository {
@override
Future<List<Activity>> getActivities() async {
final response = await http.get(Uri.parse('https://api.example.com/activities'));
return Activity.fromJsonList(jsonDecode(response.body));
}
}
// 混合資料來源(快取 + 遠端)
class HybridActivityRepository implements ActivityRepository {
final LocalActivityRepository _localRepo;
final RemoteActivityRepository _remoteRepo;
@override
Future<List<Activity>> getActivities() async {
try {
final activities = await _remoteRepo.getActivities();
await _localRepo.saveActivities(activities);
return activities;
} catch (e) {
return await _localRepo.getActivities();
}
}
}
Repository 模式讓業務邏輯層保持純粹,專注於業務規則:
// lib/features/activity/domain/usecases/get_activities_usecase.dart
// (imports omitted)
class ActivityUseCase {
final ActivityRepository _repository;
Future<List<Activity>> getActiveActivities() async {
final allActivities = await _repository.getActivities();
return allActivities.where((activity) =>
activity.status == ActivityStatus.inProgress
).toList();
}
}
// lib/common/domain/repositories/base_repository.dart
// (imports omitted)
abstract class Repository<T, ID> {
Future<List<T>> getAll();
Future<T?> getById(ID id);
Future<void> save(T entity);
}
// lib/features/activity/domain/repositories/activity_repository.dart
// (imports omitted)
abstract class ActivityRepository extends Repository<Activity, String> {
Future<List<Activity>> getActivitiesByCategory(ActivityCategory category);
}
// lib/features/activity/domain/repositories/activity_repository.dart
// (imports omitted)
abstract class ActivityRepository {
Future<Result<List<Activity>>> getActivities();
Future<Result<Activity>> getActivityById(String id);
}
// lib/common/domain/entities/result.dart
// (imports omitted)
// 結果類型
class Result<T> {
final T? data;
final String? error;
final bool isSuccess;
}
在 Clean Architecture 中,Repository 介面就像是 Domain Layer 與 Data Layer 之間的「契約」。這份契約定義了所有資料操作必須遵守的規範,讓業務邏輯層能夠專注於核心業務,而不需要關心資料的具體來源。
在設計契約前,我們先定義契約的主體。在我們的 App 中,一個「活動」應該包含 id, 標題, 圖片URL, 成員數量, 地點等資訊。這就是我們業務的核心,我們稱之為 Entity。
在設計 Repository 契約時,我們遵循以下原則:
接下來,讓我們看看這些概念在我們的「Crew Up!」專案中是如何被完整地實踐的。這包含了 Repository 的具體實作、實體設計,以及它們在 Use Case 中的應用。
Domain Layer 定義了 ActivityRepository
介面,包含完整的業務契約,而 Data Layer 則實作了 ActivityRepositoryImpl
,支援本地和遠端雙資料來源的架構。
這樣的設計讓我們能夠:
Result
類型統一處理成功和失敗情況Activity
實體包含了豐富的業務邏輯,如判斷活動是否可以報名、是否為最新活動等。同時,我們也定義了 ActivityCategory
、ActivityStatus
和 ActivityCardType
等枚舉類型,讓業務邏輯更加清晰和型別安全。
這樣的設計讓我們能夠:
Use Cases 如 CreateActivityUseCase
和 GetActivitiesUseCase
都透過 Repository 進行業務邏輯處理,包含完整的驗證和錯誤處理。
這樣的設計讓我們能夠:
Result
類型統一處理成功和失敗情況在 Clean Architecture 中,Repository 扮演著關鍵的角色。結合我們的 Feature-First 架構,整個系統的結構如下:
╔═══════════════════════════════════════════════╗
║ Presentation Layer ║
║ ║
║ [Widgets] [Providers] [Screens] ║
║ (Riverpod) ║
╚═══════════════════════════════════════════════╝
│
▼
╔═══════════════════════════════════════════════╗
║ Domain Layer ║
║ ║
║ [Entities] [Use Cases] [Repositories] ║
║ (Interfaces) ║
╚═══════════════════════════════════════════════╝
▲
│
╔═══════════════════════════════════════════════╗
║ Data Layer ║
║ ║
║ [Models] [Repositories] [Data Sources] ║
║ (Implementations) ║
╚═══════════════════════════════════════════════╝
在我們的專案中,Repository 模式已經被完整實作,並且完美整合了 Feature-First 架構:
Domain Layer (領域層):
ActivityRepository
介面定義在 lib/features/activity/domain/repositories/
Activity
實體定義在 lib/features/activity/domain/entities/
lib/features/activity/domain/usecases/
lib/features/activity/domain/services/
Data Layer (資料層):
ActivityRepositoryImpl
實作在 lib/features/activity/data/repositories/
ActivityLocalDataSource
本地資料來源在 lib/features/activity/data/datasources/
ActivityDataSource
遠端資料來源介面Presentation Layer (表現層):
這樣的架構實現了:
恭喜!今天我們深入探討了 Repository 模式的「道」——為什麼抽象是軟體自由的基石。我們不僅理解了理論,更看到了實際的實作。
在我們的專案中,Repository 模式已經被完整實作,並且完美整合了 Feature-First 架構:
ActivityRepository
介面,包含完整的業務契約ActivityRepositoryImpl
,支援本地和遠端雙資料來源Result
類型統一管理成功和失敗情況這樣的架構讓我們獲得了:
藍圖已經畫好,實作也已完成。
明天,我們將深入探討 Repository 模式的另一個重要環節:本地儲存與安全儲存。我們會學習如何為不同類型的資料選擇合適的儲存方案,實作完整的資料持久化架構,並確保敏感資料的安全防護。
期待與您在 Day 8 相見!