Skip to content

Instantly share code, notes, and snippets.

@memory-lovers
Last active June 9, 2025 04:40
Show Gist options
  • Save memory-lovers/eb784a20abf8667aea312466699d3400 to your computer and use it in GitHub Desktop.
Save memory-lovers/eb784a20abf8667aea312466699d3400 to your computer and use it in GitHub Desktop.
Claude Opus 4で生成してもらったCLAUD.mdのサンプル(Flutter+Melos+FVM)
# CLAUD.md - Flutter Monorepo Project Guide
## プロジェクト概要
このプロジェクトはFlutter + Melos + FVMを使用したモノレポ構成のモバイルアプリケーションです。Claudeに対する包括的なガイドラインを提供し、一貫性のある高品質なコード生成を実現します。
## 技術スタック
- **Flutter**: 3.19.0 (FVMで管理)
- **Dart**: 3.3.0
- **Melos**: 3.4.0
- **状態管理**: Riverpod 2.5.0
- **ルーティング**: go_router 13.2.0
- **DI**: get_it + injectable
- **ローカルDB**: drift
- **API通信**: dio + retrofit
- **テスト**: flutter_test, mockito, golden_toolkit
## プロジェクト構造
```
project_root/
├── melos.yaml
├── .fvm/
│ └── fvm_config.json
├── apps/
│ ├── mobile_app/ # メインアプリ
│ └── admin_app/ # 管理画面アプリ
├── packages/
│ ├── core/ # 共通コア機能
│ ├── ui_kit/ # UIコンポーネントライブラリ
│ ├── api_client/ # API通信層
│ ├── domain/ # ドメインモデル
│ └── infrastructure/ # インフラ層
└── tools/ # 開発ツール
```
## コーディング規約
### 命名規則
```dart
// ファイル名: snake_case
user_profile_screen.dart
// クラス名: PascalCase
class UserProfileScreen extends ConsumerWidget {}
// 変数・関数: camelCase
final userRepository = ref.watch(userRepositoryProvider);
void updateUserProfile() {}
// 定数: lowerCamelCase (constプレフィックス不要)
const defaultTimeout = Duration(seconds: 30);
// Provider: camelCase + Provider suffix
final userRepositoryProvider = Provider<UserRepository>((ref) => ...);
```
### ディレクトリ構造(各パッケージ内)
```
lib/
├── src/
│ ├── presentation/ # UI層
│ │ ├── screens/
│ │ ├── widgets/
│ │ └── providers/
│ ├── application/ # アプリケーションロジック
│ │ ├── use_cases/
│ │ └── services/
│ ├── domain/ # ドメイン層
│ │ ├── models/
│ │ ├── repositories/
│ │ └── value_objects/
│ └── infrastructure/ # インフラ層
│ ├── datasources/
│ ├── repositories/
│ └── dtos/
└── package_name.dart # パブリックAPI
```
## Riverpod規約
### Provider定義
```dart
// 常にファイル最上部に配置
// part指定の直後
part 'user_screen.g.dart';
// 1. 単純なProvider
@riverpod
UserRepository userRepository(UserRepositoryRef ref) {
return UserRepository(ref.watch(dioProvider));
}
// 2. FutureProvider (自動的にAsyncValue<T>を返す)
@riverpod
Future<User> currentUser(CurrentUserRef ref) async {
final repo = ref.watch(userRepositoryProvider);
return repo.getCurrentUser();
}
// 3. StreamProvider
@riverpod
Stream<List<Message>> messages(MessagesRef ref) {
return ref.watch(chatRepositoryProvider).messagesStream();
}
// 4. NotifierProvider (状態管理)
@riverpod
class UserProfile extends _$UserProfile {
@override
User? build() => null;
Future<void> update(User user) async {
state = await ref.read(userRepositoryProvider).updateUser(user);
}
}
```
### Widget内での使用
```dart
class UserScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// 監視(リビルドあり)
final user = ref.watch(currentUserProvider);
// 読み取り(リビルドなし)
final onTap = () => ref.read(userProfileProvider.notifier).update(user);
return user.when(
data: (user) => UserProfile(user: user),
loading: () => const CircularProgressIndicator(),
error: (err, stack) => ErrorWidget(err),
);
}
}
```
## エラーハンドリング
### API通信
```dart
@riverpod
class UserRepository extends _$UserRepository {
Future<User> getUser(String id) async {
try {
final response = await ref.read(dioProvider).get('/users/$id');
return User.fromJson(response.data);
} on DioException catch (e) {
throw switch (e.response?.statusCode) {
404 => UserNotFoundException(id),
401 => UnauthorizedException(),
_ => NetworkException(e.message ?? 'Unknown error'),
};
}
}
}
```
### UI層でのエラー表示
```dart
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(currentUserProvider);
return userAsync.when(
data: (user) => UserContent(user: user),
loading: () => const LoadingIndicator(),
error: (error, stack) => ErrorView(
message: switch (error) {
UserNotFoundException() => 'ユーザーが見つかりません',
UnauthorizedException() => 'ログインが必要です',
NetworkException() => 'ネットワークエラーが発生しました',
_ => '予期しないエラーが発生しました',
},
onRetry: () => ref.invalidate(currentUserProvider),
),
);
}
```
## テスト規約
### 単体テスト
```dart
@GenerateNiceMocks([MockSpec<UserRepository>()])
void main() {
group('UserProfile Notifier', () {
late ProviderContainer container;
late MockUserRepository mockRepo;
setUp(() {
mockRepo = MockUserRepository();
container = ProviderContainer(
overrides: [
userRepositoryProvider.overrideWithValue(mockRepo),
],
);
});
test('should update user profile', () async {
final user = User(id: '1', name: 'Test');
when(mockRepo.updateUser(any)).thenAnswer((_) async => user);
await container.read(userProfileProvider.notifier).update(user);
expect(container.read(userProfileProvider), equals(user));
});
});
}
```
### Widgetテスト
```dart
testWidgets('shows loading indicator', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
currentUserProvider.overrideWith((ref) async {
await Future.delayed(const Duration(seconds: 1));
return User(id: '1', name: 'Test');
}),
],
child: const MaterialApp(home: UserScreen()),
),
);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
```
## Melos Scripts
```yaml
# melos.yaml
name: my_project
packages:
- apps/**
- packages/**
scripts:
# 開発用
get:
exec: flutter pub get
description: すべてのパッケージで依存関係を取得
analyze:
exec: flutter analyze
description: 静的解析を実行
test:
exec: flutter test
description: テストを実行
packageFilters:
dirExists: test
# コード生成
generate:
exec: dart run build_runner build --delete-conflicting-outputs
description: コード生成を実行
packageFilters:
dependsOn: build_runner
# リリース用
build:apk:
run: |
cd apps/mobile_app && \
flutter build apk --release
description: APKをビルド
```
## パフォーマンス最適化
### 1. Widget最適化
```dart
// ❌ 避けるべき: 不必要な再ビルド
class BadExample extends ConsumerWidget {
Widget build(context, ref) {
final allData = ref.watch(heavyDataProvider);
return Column(
children: [
Text(allData.title), // titleだけ使用
],
);
}
}
// ✅ 推奨: 必要なデータのみ監視
class GoodExample extends ConsumerWidget {
Widget build(context, ref) {
final title = ref.watch(heavyDataProvider.select((data) => data.title));
return Column(
children: [
Text(title),
],
);
}
}
```
### 2. Provider最適化
```dart
// ✅ 自動破棄を活用
@riverpod
Future<List<Product>> products(ProductsRef ref, {required String category}) async {
// keepAlive: falseがデフォルト - 使用されなくなったら自動破棄
final repo = ref.watch(productRepositoryProvider);
return repo.getProducts(category);
}
// ✅ 必要な場合のみkeepAlive
@Riverpod(keepAlive: true)
AppConfig appConfig(AppConfigRef ref) {
// アプリ全体で使用する設定は保持
return AppConfig.load();
}
```
### 3. 画像最適化
```dart
// ✅ 適切なキャッシュ設定
CachedNetworkImage(
imageUrl: url,
cacheKey: 'user_avatar_${user.id}',
maxHeightDiskCache: 200,
maxWidthDiskCache: 200,
memCacheHeight: 100,
memCacheWidth: 100,
)
```
## セキュリティ考慮事項
### 1. 認証トークン管理
```dart
// ✅ SecureStorageを使用
@Riverpod(keepAlive: true)
class AuthToken extends _$AuthToken {
@override
Future<String?> build() async {
return await ref.read(secureStorageProvider).read(key: 'auth_token');
}
Future<void> save(String token) async {
await ref.read(secureStorageProvider).write(key: 'auth_token', value: token);
ref.invalidateSelf();
}
}
```
### 2. API通信
```dart
// ✅ Interceptorで自動的にトークンを付与
class AuthInterceptor extends Interceptor {
final Ref ref;
@override
void onRequest(options, handler) {
final token = ref.read(authTokenProvider).valueOrNull;
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
}
```
## よくあるパターン
### 1. 無限スクロール
```dart
@riverpod
class PostList extends _$PostList {
int _page = 0;
bool _hasMore = true;
@override
Future<List<Post>> build() async {
return _loadPage(0);
}
Future<void> loadMore() async {
if (!_hasMore || state.isLoading) return;
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final current = state.valueOrNull ?? [];
final newPosts = await _loadPage(_page + 1);
_page++;
_hasMore = newPosts.length == 20; // ページサイズが20の場合
return [...current, ...newPosts];
});
}
Future<List<Post>> _loadPage(int page) async {
final repo = ref.read(postRepositoryProvider);
return repo.getPosts(page: page, limit: 20);
}
}
```
### 2. フォーム管理
```dart
@freezed
class ProfileForm with _$ProfileForm {
const factory ProfileForm({
@Default('') String name,
@Default('') String email,
@Default(false) bool isSubmitting,
String? errorMessage,
}) = _ProfileForm;
}
@riverpod
class ProfileFormController extends _$ProfileFormController {
@override
ProfileForm build() => const ProfileForm();
void updateName(String name) {
state = state.copyWith(name: name);
}
void updateEmail(String email) {
state = state.copyWith(email: email);
}
Future<void> submit() async {
if (state.isSubmitting) return;
state = state.copyWith(isSubmitting: true, errorMessage: null);
try {
await ref.read(userRepositoryProvider).updateProfile(
name: state.name,
email: state.email,
);
// 成功処理
} catch (e) {
state = state.copyWith(
isSubmitting: false,
errorMessage: e.toString(),
);
}
}
}
```
## Claudeへの指示
### コード生成時の規則
1. **必ずriverpod_generatorを使用** - 手動でProviderを書かない
2. **freezedを積極的に活用** - データクラスは全てfreezedで定義
3. **明示的な型定義** - 型推論に頼らず明示的に型を指定
4. **エラーハンドリング必須** - try-catchまたはAsyncValue.guardを使用
5. **日本語コメント推奨** - ビジネスロジックは日本語で説明
### 回答形式
1. **段階的な実装** - 複雑な機能は段階的に説明
2. **完全なコード例** - importから含めた完全なコード
3. **テストコード付き** - 実装と同時にテストコードも提供
4. **パフォーマンス考慮** - 最適化ポイントを明示
### 避けるべきこと
- StatefulWidgetの使用(ConsumerWidget/ConsumerStatefulWidgetを使用)
- context.read/watch(ref.read/watchを使用)
- 手動でのProviderScope管理(必要最小限に)
- グローバル変数(Providerで管理)
- 非同期処理でのsetState(Riverpodで管理)
## 開発フロー
1. **機能追加時**
```bash
# 新しいパッケージ作成
melos exec -- flutter create --template=package packages/new_feature
# 依存関係追加
cd packages/new_feature
flutter pub add freezed_annotation riverpod_annotation
flutter pub add --dev build_runner freezed riverpod_generator
# コード生成
melos run generate
```
2. **テスト実行**
```bash
# 全体テスト
melos run test
# 特定パッケージのみ
melos exec --scope="*api_client*" -- flutter test
```
3. **リリースビルド**
```bash
# バージョン更新
melos version
# ビルド
melos run build:apk
melos run build:ios
```
## トラブルシューティング
### よくある問題と解決法
1. **コード生成エラー**
```bash
# キャッシュクリア
melos clean
melos bootstrap
melos run generate
```
2. **Provider not found エラー**
- ProviderScopeでラップされているか確認
- overrideが正しく設定されているか確認
3. **パフォーマンス問題**
- DevToolsでWidget rebuildsを確認
- 不要なref.watchを削除
- selectを使用して必要なデータのみ監視
## 参考リンク
- [Riverpod Documentation](https://riverpod.dev/)
- [Melos Documentation](https://melos.invertase.dev/)
- [Flutter Performance Best Practices](https://docs.flutter.dev/perf/best-practices)
- [当プロジェクトのDesign System](packages/ui_kit/README.md)
@memory-lovers
Copy link
Author

ultrathink: flutter+melos+fvmを使ったスマホアプリを考えています
どんなCLAUD.mdを作成するとよいですか。最高のパフォーマンスを引き出すフルサンプルを教えて

Flutter + Melos + FVMプロジェクト用の包括的なCLAUD.mdを作成します:

このCLAUD.mdは、Flutter + Melos + FVMプロジェクトで最高のパフォーマンスを引き出すために設計されています。主な特徴:

  1. 包括的な技術スタック定義: Riverpod、go_router、freezedなど、モダンなFlutter開発に必要なライブラリを網羅
  2. 詳細なコーディング規約: 命名規則からディレクトリ構造まで、チーム全体で一貫性を保つためのルール
  3. Riverpodベストプラクティス: riverpod_generatorを使った最新のアプローチで、ボイラープレートを削減
  4. 実践的なコード例: 無限スクロール、フォーム管理など、よく使うパターンの実装例
  5. パフォーマンス最適化: Widget再ビルドの最小化、適切なProvider設計、画像キャッシュ戦略
  6. Melosワークフロー: モノレポ管理のための具体的なスクリプトとコマンド
  7. エラーハンドリング: 型安全なエラー処理とユーザーフレンドリーなエラー表示
  8. セキュリティ考慮: 認証トークンの安全な管理方法

このCLAUD.mdをプロジェクトルートに配置することで、Claudeは:

  • プロジェクトの構造と規約を正確に理解
  • 一貫性のあるコードを生成
  • ベストプラクティスに従った実装を提案
  • 適切なエラーハンドリングとテストコードを提供

必要に応じて、プロジェクトの具体的な要件に合わせてカスタマイズしてください。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment