skills/flutter-widget-design/SKILL.md
Flutter Widget の設計・分割・composition パターンに関するベストプラクティス
npx skillsauth add oto1720/claude-agents-skills flutter-widget-designInstall 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.
分割のトリガー:
// ❌ 巨大な build()
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Column(
children: [
// 50行のヘッダー
// 100行のリスト
// 30行のフッター
],
),
);
}
}
// ✅ 責任ごとに分割
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const HomeAppBar(),
body: const Column(
children: [
HomeHeader(),
Expanded(child: HomeList()),
HomeFooter(),
],
),
);
}
}
// ✅ 変更されない Widget は必ず const
class UserAvatar extends StatelessWidget {
const UserAvatar({super.key, required this.imageUrl});
final String imageUrl;
@override
Widget build(BuildContext context) {
return CircleAvatar(
backgroundImage: NetworkImage(imageUrl),
radius: 24,
);
}
}
// 使用時も const
const UserAvatar(imageUrl: 'https://example.com/avatar.jpg')
// ❌ 内部で状態を持つ(テスト・再利用が困難)
class SearchField extends StatefulWidget {
@override
State<SearchField> createState() => _SearchFieldState();
}
class _SearchFieldState extends State<SearchField> {
String _query = '';
@override
Widget build(BuildContext context) {
return TextField(
onChanged: (v) => setState(() => _query = v),
);
}
}
// ✅ 状態を親から受け取る(テスタブル・再利用可能)
class SearchField extends StatelessWidget {
const SearchField({
super.key,
required this.value,
required this.onChanged,
});
final String value;
final ValueChanged<String> onChanged;
@override
Widget build(BuildContext context) {
return TextField(
controller: TextEditingController(text: value),
onChanged: onChanged,
);
}
}
// StatelessWidget: 外部から渡されたデータのみ表示
class UserCard extends StatelessWidget {
const UserCard({super.key, required this.user});
final User user;
@override
Widget build(BuildContext context) => Card(
child: Text(user.name),
);
}
// ConsumerWidget (Riverpod): グローバル状態を読む
class UserProfile extends ConsumerWidget {
const UserProfile({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(currentUserProvider);
return user.when(
data: (u) => UserCard(user: u),
loading: () => const CircularProgressIndicator(),
error: (e, _) => ErrorView(message: e.toString()),
);
}
}
// HookConsumerWidget (hooks_riverpod): ローカル状態 + グローバル状態
// → StatefulWidget の代替として最も推奨
class SearchScreen extends HookConsumerWidget {
const SearchScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// コントローラー類が自動 dispose される
final controller = useTextEditingController();
final isLoading = useState(false);
final results = ref.watch(searchResultsProvider);
return TextField(controller: controller);
}
}
// StatefulWidget: hooks が使えない場面(外部パッケージとの統合など)
// → 原則として HookConsumerWidget で代替できないか先に検討する
class AnimatedCounter extends StatefulWidget {
const AnimatedCounter({super.key, required this.count});
final int count;
@override
State<AnimatedCounter> createState() => _AnimatedCounterState();
}
Widget を作るとき
↓
グローバル状態(Riverpod)が必要?
YES → ローカル状態(useState)も必要?
YES → HookConsumerWidget ← 最も多いケース
NO → ConsumerWidget
NO → ローカル状態(useState)が必要?
YES → HookWidget
NO → StatelessWidget(const 推奨)
※ StatefulWidget は原則として最後の手段
// 関連する Widget をグループ化
class UserListTile extends StatelessWidget {
const UserListTile({super.key, required this.user, this.onTap});
final User user;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return ListTile(
leading: UserAvatar(imageUrl: user.avatarUrl),
title: UserName(name: user.name),
subtitle: UserRole(role: user.role),
onTap: onTap,
);
}
}
class PageTemplate extends StatelessWidget {
const PageTemplate({
super.key,
required this.title,
required this.body,
this.floatingActionButton,
this.bottomBar,
});
final String title;
final Widget body;
final Widget? floatingActionButton;
final Widget? bottomBar;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: body,
floatingActionButton: floatingActionButton,
bottomNavigationBar: bottomBar,
);
}
}
| アンチパターン | 問題 | 修正方法 |
|-------------|------|---------|
| God Widget(1000行の build) | 読めない・テスト不可 | 責任ごとに分割 |
| BuildContext を async で使用 | クラッシュ | mounted チェック後に使用 |
| Widget内でビジネスロジック | テスト不可 | Notifier/UseCase に移動 |
| 毎フレーム重い計算 | jank | build() 外 or Notifier に移動 |
| 深すぎるネスト | 読みにくい | Widget に抽出 |
const を付けたmounted チェックがあるdevelopment
プロジェクト全体の技術構成図(アーキテクチャダイアグラム)を自動生成するスキル。リポジトリやプロジェクトのコードベースを解析し、使用技術・依存関係・レイヤー構造・データフロー・インフラ構成を可視化したMermaid/SVG/HTML図を生成する。「技術構成図を作って」「アーキテクチャ図」「システム構成を可視化」「プロジェクトの全体像」「tech stack diagram」などのリクエストで必ずこのスキルを使用すること。プロジェクトの理解・オンボーディング資料・ドキュメント作成にも活用できる。
testing
Create new skills, modify and improve existing skills, and measure skill performance. Use when users want to create a skill from scratch, update or optimize an existing skill, run evals to test a skill, benchmark skill performance with variance analysis, or optimize a skill's description for better triggering accuracy.
development
セキュリティ観点でコードを精査し、脆弱性・リスクをレポートする。 以下のトリガーで自動発動: - 「セキュリティレビューして」「脆弱性チェック」「セキュリティ問題ない?」 - 「認証コードを確認して」「APIキーや秘密情報が漏れていないか確認して」 - /security-review [ファイルパス]
tools
PRやコミットの差分をレビューして、マージ可否の判断と指摘事項を出力する。 以下のトリガーで自動発動: - 「PRレビューして」「このPRどう思う?」「マージしても大丈夫?」 - 「差分をレビューして」「コミット内容を確認して」 - /pr-review [ブランチ名 or コミットハッシュ]