SKILL.md
唯讀
名稱
flutter-add-widget-preview
描述
使用 previews.dart 系統為專案新增互動式 Widget 預覽功能。在建立新 UI 元件或更新現有畫面時使用,以確保設計一致性並進行互動測試。
預覽 Flutter Widget
目錄
預覽指引
使用 Flutter Widget Previewer 可以在獨立於完整應用程式上下文的環境中,即時轉譯(render)Widget。
- 目標元素: 可將
@Preview註解套用至頂層函式(top-level functions)、類別內的靜態方法,或是無需必填引數且回傳Widget或WidgetBuilder的公用 Widget 建構子/工廠建構子。 - 匯入檔: 務必匯入
package:flutter/widget_previews.dart以存取預覽註解。 - 自訂註解: 繼承
Preview類別來建立自訂註解,藉此跨多個 Widget 注入常用屬性(例如主題、包裝器 wrappers)。 - 多重設定: 可對單一目標套用多個
@Preview註解以產生多個預覽實例。此外,也可繼承MultiPreview來封裝常見的多重預覽設定。 - 執行階段轉換: 在自訂的
Preview或MultiPreview類別中覆寫transform()方法,即可在執行階段動態修改預覽設定(例如根據動態數值產生名稱,這在const上下文中是無法實現的)。
限制事項與處理方式
撰寫支援預覽的 Widget 時,請留意以下限制,因為 Widget Previewer 是在 Web 環境中執行:
- 禁止使用原生 API: 請勿使用來自
dart:io或dart:ffi的原生外掛程式或 API。若 Widget 間接相依於dart:io或dart:ffi,在呼叫時將會拋出例外。請使用條件式匯入(conditional imports)在預覽模式下進行 Mock 或避開這些 API。 - 資源路徑(Asset Paths): 透過
dart:ui的fromAssetAPI 載入資源時,請使用基於 Package 的路徑(例如:packages/my_package_name/assets/my_image.png,而非assets/my_image.png)。 - 公用回呼函式: 傳入預覽註解的所有回呼函式引數都必須是公用且為常數(constant),以滿足程式碼產生的需求。
- 尺寸約束: 若你的 Widget 沒有設定約束,請在
@Preview註解中使用size參數套用明確的約束,因為預覽器預設會將其限制在大約半個視埠(viewport)的大小。
工作流程
建立 Widget 預覽
實作新的 Widget 預覽時,請複製並追蹤以下檢查清單:
- [ ] 匯入
package:flutter/widget_previews.dart。 - [ ] 確認合法的目標(頂層函式、靜態方法,或無引數的公用建構子)。
- [ ] 對目標套用
@Preview註解。 - [ ] 依需求設定預覽參數(
name、group、size、theme、brightness等)。 - [ ] 若要將相同設定套用到多個 Widget,請將設定抽離為繼承自
Preview的自訂類別。
與預覽互動
請依照對應的條件工作流程來啟動並使用 Widget Previewer:
若使用受支援的 IDE(Android Studio、IntelliJ、包含 Flutter 3.38+ 的 VS Code):
- 啟動 IDE,Widget Previewer 會自動啟動。
- 開啟側邊欄的 "Flutter Widget Preview" 頁籤。
- 若想檢視目前開啟檔案以外的預覽,可切換左下角的 "Filter previews by selected file"(依選取檔案篩選預覽)。
若使用命令列(Command Line):
- 切換至 Flutter 專案的根目錄。
- 執行
flutter widget-preview start。 - 檢視自動開啟的 Chrome 環境。
回饋循環:預覽疊代
- 修改 Widget 程式碼或預覽設定。
- 觀察 Widget Previewer 中的自動更新。
- 若修改了全域狀態(例如靜態初始化程式):點擊右下角的全域 Hot Restart 按鈕。
- 若只需要重置單一 Widget 的區域狀態:點擊該預覽卡片上的個別 Hot Restart 按鈕。
- 檢視 IDE/CLI 主控台中的錯誤 -> 修復 -> 重複上述步驟。
範例
基本預覽
import 'package:flutter/widget_previews.dart';
import 'package:flutter/material.dart';
@Preview(name: 'My Sample Text', group: 'Typography')
Widget mySampleText() {
return const Text('Hello, World!');
}
包含執行階段轉換的自訂預覽
import 'package:flutter/widget_previews.dart';
import 'package:flutter/material.dart';
final class TransformativePreview extends Preview {
const TransformativePreview({
super.name,
super.group,
});
PreviewThemeData _themeBuilder() {
return PreviewThemeData(
materialLight: ThemeData.light(),
materialDark: ThemeData.dark(),
);
}
@override
Preview transform() {
final originalPreview = super.transform();
final builder = originalPreview.toBuilder();
builder
..name = 'Transformed - ${originalPreview.name}'
..theme = _themeBuilder;
return builder.toPreview();
}
}
@TransformativePreview(name: 'Custom Themed Button')
Widget myButton() => const ElevatedButton(onPressed: null, child: Text('Click'));
MultiPreview 實作
import 'package:flutter/widget_previews.dart';
import 'package:flutter/material.dart';
/// 自動建立淺色與深色模式的預覽。
final class MultiBrightnessPreview extends MultiPreview {
const MultiBrightnessPreview({required this.name});
final String name;
@override
List<Preview> get previews => const [
Preview(brightness: Brightness.light),
Preview(brightness: Brightness.dark),
];
@override
List<Preview> transform() {
final previews = super.transform();
return previews.map((preview) {
final builder = preview.toBuilder()
..group = 'Brightness'
..name = '$name - ${preview.brightness!.name}';
return builder.toPreview();
}).toList();
}
}
@MultiBrightnessPreview(name: 'Primary Card')
Widget cardPreview() => const Card(child: Padding(padding: EdgeInsets.all(8.0), child: Text('Content')));






