flutter
RiverpodによるState管理、Freezed、go_router、mocktailを活用したFlutterアプリ開発をサポートするスキルです。コード生成からテストまで、モダンなFlutter開発スタックを一貫してカバーします。
description の原文を見る
Flutter development with Riverpod state management, Freezed, go_router, and mocktail testing
SKILL.md 本文
Flutter Skill
プロジェクト構成
project/
├── lib/
│ ├── core/ # コアユーティリティ
│ │ ├── constants/ # アプリ定数
│ │ ├── extensions/ # Dart拡張機能
│ │ ├── router/ # go_router設定
│ │ │ └── app_router.dart
│ │ └── theme/ # アプリテーマ
│ │ └── app_theme.dart
│ ├── data/ # データレイヤー
│ │ ├── models/ # Freezedデータモデル
│ │ ├── repositories/ # リポジトリ実装
│ │ └── services/ # APIサービス
│ ├── domain/ # ドメインレイヤー
│ │ ├── entities/ # ビジネスエンティティ
│ │ └── repositories/ # リポジトリインターフェース
│ ├── presentation/ # UIレイヤー
│ │ ├── common/ # 共有ウィジェット
│ │ ├── features/ # 機能モジュール
│ │ │ └── feature_name/
│ │ │ ├── providers/ # Riverpodプロバイダー
│ │ │ ├── widgets/ # 機能固有ウィジェット
│ │ │ └── feature_screen.dart
│ │ └── providers/ # グローバルプロバイダー
│ ├── main.dart
│ └── app.dart
├── test/
│ ├── unit/ # ユニットテスト
│ ├── widget/ # ウィジェットテスト
│ └── integration/ # 統合テスト
├── pubspec.yaml
├── analysis_options.yaml
└── CLAUDE.md
Riverpod状態管理
プロバイダータイプ
// シンプルな値プロバイダー
final appNameProvider = Provider<String>((ref) => 'My App');
// シンプルな可変状態用StateProvider
final counterProvider = StateProvider<int>((ref) => 0);
// 複雑な状態ロジック用NotifierProvider
final userProvider = NotifierProvider<UserNotifier, User?>(() => UserNotifier());
// 非同期操作用AsyncNotifierProvider
final usersProvider = AsyncNotifierProvider<UsersNotifier, List<User>>(
() => UsersNotifier(),
);
// シンプルな非同期データ用FutureProvider
final configProvider = FutureProvider<Config>((ref) async {
return await ref.watch(configServiceProvider).loadConfig();
});
// リアルタイムデータ用StreamProvider
final messagesProvider = StreamProvider<List<Message>>((ref) {
return ref.watch(messageServiceProvider).watchMessages();
});
// パラメータ化されたデータ用Familyプロバイダー
final userByIdProvider = FutureProvider.family<User, String>((ref, userId) async {
return await ref.watch(userRepositoryProvider).getUser(userId);
});
Notifierパターン
@riverpod
class Users extends _$Users {
@override
Future<List<User>> build() async {
return await _fetchUsers();
}
Future<List<User>> _fetchUsers() async {
final repository = ref.read(userRepositoryProvider);
return await repository.getUsers();
}
Future<void> refresh() async {
state = const AsyncLoading();
state = await AsyncValue.guard(() => _fetchUsers());
}
Future<void> addUser(User user) async {
final repository = ref.read(userRepositoryProvider);
await repository.addUser(user);
ref.invalidateSelf();
}
}
AsyncValue処理
class UsersScreen extends ConsumerWidget {
const UsersScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final usersAsync = ref.watch(usersProvider);
return usersAsync.when(
data: (users) => UsersList(users: users),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorDisplay(
error: error,
onRetry: () => ref.invalidate(usersProvider),
),
);
}
}
// パターンマッチング代替案
Widget build(BuildContext context, WidgetRef ref) {
final usersAsync = ref.watch(usersProvider);
return switch (usersAsync) {
AsyncData(:final value) => UsersList(users: value),
AsyncLoading() => const LoadingIndicator(),
AsyncError(:final error) => ErrorDisplay(error: error),
};
}
refメソッド
// watch - プロバイダーが変更されるとリビルド
final users = ref.watch(usersProvider);
// read - 1回限りの読み込み、リビルドなし
void onButtonPressed() {
ref.read(counterProvider.notifier).state++;
}
// listen - リビルドなしで変更に反応
ref.listen(authProvider, (previous, next) {
if (next == null) {
context.go('/login');
}
});
// invalidate - 強制的にリフレッシュ
ref.invalidate(usersProvider);
// keepAlive - 自動破棄を防ぐ
final link = ref.keepAlive();
// 後で: link.close() で破棄を許可
Freezedデータモデル
モデル定義
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
class User with _$User {
const factory User({
required String id,
required String name,
required String email,
@Default(false) bool isActive,
DateTime? createdAt,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
// 状態用ユニオン型
@freezed
sealed class AuthState with _$AuthState {
const factory AuthState.initial() = _Initial;
const factory AuthState.loading() = _Loading;
const factory AuthState.authenticated(User user) = _Authenticated;
const factory AuthState.unauthenticated() = _Unauthenticated;
const factory AuthState.error(String message) = _Error;
}
Freezedユニオンの使用
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(authProvider);
return authState.when(
initial: () => const SplashScreen(),
loading: () => const LoadingScreen(),
authenticated: (user) => HomeScreen(user: user),
unauthenticated: () => const LoginScreen(),
error: (message) => ErrorScreen(message: message),
);
}
go_routerナビゲーション
ルーター設定
final routerProvider = Provider<GoRouter>((ref) {
final authState = ref.watch(authProvider);
return GoRouter(
initialLocation: '/',
refreshListenable: authState,
redirect: (context, state) {
final isLoggedIn = authState.valueOrNull != null;
final isLoggingIn = state.matchedLocation == '/login';
if (!isLoggedIn && !isLoggingIn) return '/login';
if (isLoggedIn && isLoggingIn) return '/';
return null;
},
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
routes: [
GoRoute(
path: 'user/:id',
builder: (context, state) => UserScreen(
userId: state.pathParameters['id']!,
),
),
],
),
GoRoute(
path: '/login',
builder: (context, state) => const LoginScreen(),
),
],
errorBuilder: (context, state) => ErrorScreen(error: state.error),
);
});
ナビゲーション
// ルートへナビゲート
context.go('/user/123');
// スタックにプッシュ
context.push('/user/123');
// 現在のルートをポップ
context.pop();
// 現在のルートを置換
context.pushReplacement('/home');
// 名前付きルート
context.goNamed('user', pathParameters: {'id': '123'});
ウィジェットパターン
ConsumerWidget vs ConsumerStatefulWidget
// Riverpod対応ステートレス
class UserCard extends ConsumerWidget {
const UserCard({super.key, required this.userId});
final String userId;
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userByIdProvider(userId));
return user.when(
data: (user) => Card(child: Text(user.name)),
loading: () => const CardSkeleton(),
error: (e, _) => ErrorCard(error: e),
);
}
}
// Riverpod対応ステートフル
class SearchScreen extends ConsumerStatefulWidget {
const SearchScreen({super.key});
@override
ConsumerState<SearchScreen> createState() => _SearchScreenState();
}
class _SearchScreenState extends ConsumerState<SearchScreen> {
final _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final results = ref.watch(searchProvider(_controller.text));
return Column(
children: [
TextField(
controller: _controller,
onChanged: (_) => setState(() {}),
),
Expanded(child: SearchResults(results: results)),
],
);
}
}
HookConsumerWidget (flutter_hooks使用)
class AnimatedCounter extends HookConsumerWidget {
const AnimatedCounter({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final controller = useAnimationController(duration: const Duration(milliseconds: 300));
final count = ref.watch(counterProvider);
useEffect(() {
controller.forward(from: 0);
return null;
}, [count]);
return ScaleTransition(
scale: controller,
child: Text('$count'),
);
}
}
Mocktailを使用したテスト
ユニットテスト
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:riverpod/riverpod.dart';
class MockUserRepository extends Mock implements UserRepository {}
void main() {
late MockUserRepository mockRepository;
late ProviderContainer container;
setUp(() {
mockRepository = MockUserRepository();
container = ProviderContainer(
overrides: [
userRepositoryProvider.overrideWithValue(mockRepository),
],
);
});
tearDown(() {
container.dispose();
});
test('usersProvider returns list of users', () async {
final users = [User(id: '1', name: 'John', email: 'john@example.com')];
when(() => mockRepository.getUsers()).thenAnswer((_) async => users);
final result = await container.read(usersProvider.future);
expect(result, equals(users));
verify(() => mockRepository.getUsers()).called(1);
});
}
ウィジェットテスト
void main() {
testWidgets('UserCard displays user name', (tester) async {
final user = User(id: '1', name: 'John', email: 'john@example.com');
await tester.pumpWidget(
ProviderScope(
overrides: [
userByIdProvider('1').overrideWith((_) => AsyncData(user)),
],
child: const MaterialApp(home: UserCard(userId: '1')),
),
);
expect(find.text('John'), findsOneWidget);
});
testWidgets('UserCard shows loading indicator', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
userByIdProvider('1').overrideWith((_) => const AsyncLoading()),
],
child: const MaterialApp(home: UserCard(userId: '1')),
),
);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
}
pubspec.yaml
name: my_app
description: A Flutter application
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '>=3.2.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
# 状態管理
flutter_riverpod: ^2.4.9
riverpod_annotation: ^2.3.3
# データモデル
freezed_annotation: ^2.4.1
json_annotation: ^4.8.1
# ナビゲーション
go_router: ^13.0.0
# ネットワーク
dio: ^5.4.0
# ストレージ
shared_preferences: ^2.2.2
# ユーティリティ
intl: ^0.19.0
dev_dependencies:
flutter_test:
sdk: flutter
# コード生成
build_runner: ^2.4.8
freezed: ^2.4.6
json_serializable: ^6.7.1
riverpod_generator: ^2.3.9
# テスト
mocktail: ^1.0.2
# Lint
flutter_lints: ^3.0.1
GitHub Actions
name: Flutter CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.0'
channel: 'stable'
cache: true
- name: Install dependencies
run: flutter pub get
- name: Generate code
run: dart run build_runner build --delete-conflicting-outputs
- name: Analyze
run: flutter analyze --fatal-infos
- name: Run tests
run: flutter test --coverage
- name: Build APK
run: flutter build apk --release
analysis_options.yaml
include: package:flutter_lints/flutter.yaml
analyzer:
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
errors:
invalid_annotation_target: ignore
language:
strict-casts: true
strict-inference: true
strict-raw-types: true
linter:
rules:
- always_declare_return_types
- avoid_dynamic_calls
- avoid_print
- avoid_type_to_string
- cancel_subscriptions
- close_sinks
- prefer_const_constructors
- prefer_const_declarations
- prefer_final_locals
- require_trailing_commas
- unawaited_futures
- use_super_parameters
Flutterアンチパターン
- ❌ autoDisposeなしのプロバイダー - メモリリーク防止のために
.autoDisposeを使用 - ❌ コールバック内でwatch - onPressed/コールバック内では
ref.watch()ではなくref.read()を使用 - ❌ ウィジェット内のビジネスロジック - Notifiers/プロバイダーに移動
- ❌ プロバイダー内の可変状態 - イミュータブルモデルにFreezedを使用
- ❌ AsyncValueを使用しない -
when()でロード/エラー状態を処理 - ❌ Riverpodでsetstate - 共有状態にはプロバイダーを使用
- ❌ 関数にrefを渡す - refの使用をウィジェット/プロバイダー内に留める
- ❌ 深くネストされたConsumer - 代わりにConsumerWidgetを使用
- ❌ パラメータにfamilyを使用しない - パラメータ化されたプロバイダーに
.familyを使用 - ❌ グローバルGoRouterインスタンス - リダイレクトロジック付きProviderを使用
- ❌ 非同期を通じたBuildContext - contextではなく、awaitの前に値を保存
- ❌ disposeを無視 - ConsumerStatefulWidgetでコントローラーをクリーンアップ
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- alinaqi
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/alinaqi/claude-bootstrap / ライセンス: MIT
関連スキル
superfluid
Superfluidプロトコルおよびそのエコシステムに関するナレッジベースです。Superfluidについて情報を検索する際は、ウェブ検索の前にこちらを参照してください。対応キーワード:Superfluid、CFA、GDA、Super App、Super Token、stream、flow rate、real-time balance、pool(member/distributor)、IDA、sentinels、liquidation、TOGA、@sfpro/sdk、semantic money、yellowpaper、whitepaper
civ-finish-quotes
実質的なタスクが真に完了した際に、文明風の儀式的な引用句を追加します。ユーザーやエージェントが機能追加、リファクタリング、分析、設計ドキュメント、プロセス改善、レポート、執筆タスクといった実際の成果物を完成させるときに、明示的な依頼がなくても使用します。短い返信や小さな修正、未完成の作業には適用しません。
nookplot
Base(Ethereum L2)上のAIエージェント向け分散型調整ネットワークです。エージェントがオンチェーンアイデンティティを登録する、コンテンツを公開する、他のエージェントにメッセージを送る、マーケットプレイスで専門家を雇う、バウンティを投稿・請求する、レピュテーションを構築する、共有プロジェクトで協業する、リサーチチャレンジを解くことでNOOKをマイニングする、キュレーションされたナレッジを備えたスタンドアロンオンチェーンエージェントをデプロイする、またはアグリーメントとリワードで収益を得る場合に利用できます。エージェントネットワーク、エージェント調整、分散型エージェント、NOOKトークン、マイニングチャレンジ、ナレッジバンドル、エージェントレピュテーション、エージェントマーケットプレイス、ERC-2771メタトランザクション、Prepare-Sign-Relay、AgentFactory、またはNookplotが言及された場合にトリガーされます。
web3-polymarket
Polygon上でのPolymarket予測市場取引統合です。認証機能(L1 EIP-712、L2 HMAC-SHA256、ビルダーヘッダー)、注文発注(GTC/GTD/FOK/FAK、バッチ、ポストオンリー、ハートビート)、市場データ(Gamma API、Data API、オーダーブック、サブグラフ)、WebSocketストリーミング(市場・ユーザー・スポーツチャネル)、CTF操作(分割、統合、償却、ネガティブリスク)、ブリッジ機能(入金、出金、マルチチェーン)、およびガスレスリレイトランザクションに対応しています。AIエージェント、自動マーケットメーカー、予測市場UI、またはPolygraph上のPolymarketと統合するアプリケーション構築時に活用できます。
ethskills
Ethereum、EVM、またはブロックチェーン関連のリクエストに対応します。スマートコントラクト、dApps、ウォレット、DeFiプロトコルの構築、監査、デプロイ、インタラクションに適用されます。Solidityの開発、コントラクトアドレス、トークン規格(ERC-20、ERC-721、ERC-4626など)、Layer 2ネットワーク(Base、Arbitrum、Optimism、zkSync、Polygon)、Uniswap、Aave、Curveなどのプロトコルとの統合をカバーします。ガスコスト、コントラクトのデシマル設定、オラクルセキュリティ、リエントランシー、MEV、ブリッジング、ウォレット管理、オンチェーンデータの取得、本番環境へのデプロイ、プロトコル進化(EIPライフサイクル、フォーク追跡、今後の変更予定)といったトピックを含みます。
xxyy-trade
このスキルは、ユーザーが「トークン購入」「トークン売却」「トークンスワップ」「暗号資産取引」「取引ステータス確認」「トランザクション照会」「トークンスキャン」「フィード」「チェーン監視」「トークン照会」「トークン詳細」「トークン安全性確認」「ウォレット一覧表示」「マイウォレット」「AIスキャン」「自動スキャン」「ツイートスキャン」「オンボーディング」「IP確認」「IPホワイトリスト」「トークン発行」「自動売却」「損切り」「利益確定」「トレーリングストップ」「保有者」「トップホルダー」「KOLホルダー」などをリクエストした場合、またはSolana/ETH/BSC/BaseチェーンでXXYYを経由した取引について言及した場合に使用します。XXYY Open APIを通じてオンチェーン取引とデータ照会を実現します。