skills/provider-architecture/SKILL.md
Riverpod Provider 架構設計規範 - 確保正確的依賴注入、介面隔離和測試可行性。Use for: (1) 設計新的 ViewModel/Notifier 類別, (2) 審查 Provider 依賴注入是否正確, (3) 測試中配置 ProviderScope.overrides, (4) 發現 ref.read/watch 使用錯誤時。Use when: 程式碼涉及 Riverpod Provider、Notifier、ViewModel 設計或出現 ref 操作問題時。
npx skillsauth add tarrragon/claude provider-architectureInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Riverpod Provider 架構設計規範 - 確保正確的依賴注入、介面隔離和測試可行性。
要查詢 Provider 架構設計指南,輸入:
/provider-architecture
ref 問題的根源不只是 Provider 定義方式,而是直接操作狀態而非透過介面提供語意化方法。
對內和對外使用不同的接口:
| 接口類型 | 暴露對象 | 範例方法 |
|---------|---------|---------|
| 對外(Widget 層) | 語意化方法 | selectFile(), startImport(), reset() |
| 對內(ViewModel) | 私有方法 | _updateProgress(), _handleError() |
程式碼範例:
class ChromeExtensionImportViewModel extends Notifier<ChromeExtensionImportState> {
// === 對外語意化方法(Widget 層呼叫)===
Future<void> selectFile() async { ... }
Future<void> startImport() async { ... }
void reset() { ... }
void retry() { ... }
// === 對內私有方法(狀態操作封裝)===
void _updateProgress(double progress) { ... }
void _handleError(AppException error) { ... }
}
不直接操作狀態,而是透過有意義的方法名稱:
// 錯誤:直接操作狀態
ref.read(provider.notifier).state = newState;
// 正確:透過語意化方法
ref.read(provider.notifier).selectFile();
服務透過 Provider 注入,不硬編碼實例:
// 錯誤:ViewModel 直接依賴具體實作
class MyViewModel {
final _bookService = BookService(); // 硬編碼,無法測試替換
}
// 正確:透過 Provider 注入
class MyViewModel extends Notifier<MyState> {
late final BookService _bookService; // 延遲初始化
@override
MyState build() {
// 在 build() 中透過 ref.read() 取得服務
// 這些服務可以在測試中被 override
_bookService = ref.read(bookServiceProvider);
return MyState.initial();
}
}
/// ChromeExtensionImportViewModel
///
/// 職責:管理 Chrome Extension 資料匯入流程的狀態
///
/// 設計原則:
/// - 服務透過 Provider 注入(支援測試替換)
/// - 對外只暴露語意化方法
/// - 狀態操作封裝在內部
class ChromeExtensionImportViewModel extends Notifier<ChromeExtensionImportState> {
// === 透過 Provider 注入的服務 ===
late final BookService _bookService;
late final FileService _fileService;
late final JsonValidationService _jsonValidationService;
@override
ChromeExtensionImportState build() {
// 在 build() 中透過 ref.read() 取得服務
_bookService = ref.read(bookServiceProvider);
_fileService = ref.read(fileServiceProvider);
_jsonValidationService = ref.read(jsonValidationServiceProvider);
return const ChromeExtensionImportState();
}
// === 對外語意化方法 ===
/// 選擇要匯入的 JSON 檔案
Future<void> selectFile() async {
final file = await _fileService.pickJsonFile();
if (file != null) {
state = state.copyWith(selectedFile: file);
await _validateFile(file);
}
}
/// 開始執行匯入
Future<void> startImport() async {
_updateProgress(0.0);
try {
final books = await _processImport();
await _bookService.saveBooks(books);
_updateProgress(1.0);
} catch (e) {
_handleError(e as AppException);
}
}
/// 重設狀態
void reset() {
state = const ChromeExtensionImportState();
}
/// 重試上次操作
void retry() {
if (state.selectedFile != null) {
startImport();
}
}
// === 對內私有方法 ===
void _updateProgress(double progress) {
state = state.copyWith(progress: progress);
}
void _handleError(AppException error) {
state = state.copyWith(error: error);
}
Future<void> _validateFile(File file) async { ... }
Future<List<Book>> _processImport() async { ... }
}
/// Provider 定義
///
/// 使用 .new 簡化寫法,讓 Notifier 在 build() 中自行取得依賴
final chromeExtensionImportViewModelProvider =
NotifierProvider<ChromeExtensionImportViewModel, ChromeExtensionImportState>(
ChromeExtensionImportViewModel.new,
);
在測試中透過 ProviderScope.overrides 注入 Mock 服務:
testWidgets('完整匯入流程', (tester) async {
// 1. 建立共享的 Mock 服務實例
final mockBookService = MockBookService();
final mockFileService = MockFileServiceForImport();
final mockJsonValidationService = MockJsonValidationService();
// 2. 透過語意化方法配置 Mock 行為
mockFileService.setPickResult(tempJsonFile);
// 3. 使用 ProviderScope.overrides 注入 Mock
await tester.pumpWidget(
ProviderScope(
overrides: [
bookServiceProvider.overrideWithValue(mockBookService),
fileServiceProvider.overrideWithValue(mockFileService),
jsonValidationServiceProvider.overrideWithValue(mockJsonValidationService),
],
child: MaterialApp(
home: const ChromeExtensionImportWidget(),
),
),
);
// 4. 測試透過 UI 互動驗證行為
await tester.tap(find.byKey(const Key('import_data_button')));
await tester.pumpAndSettle();
expect(find.text('成功匯入 5 本書籍'), findsOneWidget);
});
Mock 服務也應提供語意化方法:
class MockFileServiceForImport implements FileService {
File? _pickResult;
/// 設定檔案選取結果(語意化方法)
void setPickResult(File? file) {
_pickResult = file;
}
@override
Future<File?> pickJsonFile() async => _pickResult;
}
.state// 錯誤
ref.read(provider.notifier).state = newState;
// 正確
ref.read(provider.notifier).updateSomething(value);
// 錯誤
class MyViewModel {
final _service = ConcreteService(); // 硬編碼
}
// 正確
class MyViewModel extends Notifier<MyState> {
late final AbstractService _service;
@override
MyState build() {
_service = ref.read(serviceProvider);
return MyState.initial();
}
}
ref.watch() 在非 build 方法中// 錯誤:在普通方法中使用 watch
void someMethod() {
final service = ref.watch(serviceProvider); // 會導致不必要的重建
}
// 正確:在 build() 中使用 watch,其他地方用 read
@override
MyState build() {
final reactiveData = ref.watch(someDataProvider); // OK:響應式資料
_service = ref.read(serviceProvider); // OK:服務實例
return MyState(data: reactiveData);
}
void someMethod() {
final service = ref.read(serviceProvider); // OK:一次性讀取
}
已使用此模式的 ViewModel:
| 檔案 | 行數 | 說明 |
|------|------|------|
| lib/presentation/viewmodels/advanced_search_viewmodel.dart | 149-151 | 搜尋功能 ViewModel |
| lib/presentation/library/library_viewmodel.dart | 84-94 | 圖書館主頁 ViewModel |
| lib/presentation/import/chrome_extension_import_view_model.dart | - | 匯入功能 ViewModel |
late final 聲明?build() 中透過 ref.read() 取得?.new 簡化寫法?ProviderScope.overrides 注入 Mock?TM-007: 測試期望與實作行為不符Last Updated: 2026-01-13 Version: 1.0.0 Source: UC-01 整合測試修復過程中的經驗總結
development
Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
development
Claude Code release notes 框架影響評估工具。比對 last-reviewed 版本篩出新版本,逐項分類(對框架有幫助 / 需評估 / 無影響 / 不適用),對採用項引導建 ANA + WRAP + spawn 落地。Use when: 執行 /release-notes 看到新版本、定期檢查 CC 更新、評估新功能對專案框架的影響時。Triggers: release notes, release-notes, CC 更新, claude code 更新, 版本更新評估, 新功能評估, 框架影響評估。
development
Assertion design judgment framework for flaky and design-quality issues. Use when writing tests, reviewing assertions, diagnosing flaky tests, or deciding if a timing/float/cache assertion is appropriate. Do NOT use for API syntax or refactoring.
tools
Chrome Extension 實機測試與 debug 工作流,以 chrome-devtools-mcp 為核心工具。Use when: (1) 完成功能後實機驗證 / manual test / 試看看 / 跑看看 / verify feature, (2) extension debug / popup 不作動 / content script 不注入 / service worker 報錯 / background 出問題, (3) 安裝 unpacked extension / load unpacked / 載入未封裝, (4) 看 console / 看 network / 看 log / view console / inspect requests, (5) 功能更新後重新載入 extension / rebuild reload / reload extension。涵蓋 Manifest V3 service worker / content script / popup / options page 的 chrome-devtools-mcp 工具呼叫流程。不取代 Puppeteer / Playwright 自動化 E2E(CI 用),定位為開發期手動驗證與 LLM-assisted debug。