flutter-bloc-development について
flutter-bloc-developmentは、Flutterコードベース向けにBLoCによる状態管理、機能単位のレイヤー構成、Repositoryを介したデータアクセス、デザインシステム定数の使用を定めるスキルです。機能構成からUIとデータフローまでを扱います。
主な機能
- 01機能単位のプロジェクト構成
- 02BLoCのイベントと状態パターン
- 03Repositoryとデータソースの分離
- 04UIでのデザインシステム定数の使用
- 05ローディング、成功、エラーの状態遷移
活用シーン
- 01新しいFlutter機能の実装
- 02ウィジェットからビジネスロジックを分離
- 03バックエンド呼び出しをデータソースに集約
- 04UIのハードコードされたスタイル値の置き換え
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ abdelhakrazi/ flutter-bloc-clean-architecture-skill --skill flutter-bloc-developmentインストール後、Claude Code で/flutter-bloc-developmentを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
LICENSE.txtREADME.mdexamples/earnings/earnings.dartexamples/earnings/bloc/earnings_bloc.dartearnings_state.dartearnings_event.dartexamples/earnings/data/data.dartexamples/earnings/data/datasources/earnings_datasource.dartexamples/earnings/data/models/earnings_summary.dartexamples/earnings/data/repositories/earnings_repository.dartexamples/earnings/view/earnings_page.dartview.dartexamples/earnings/view/widgets/daily_earnings_list.dartearnings_summary_card.dartexamples/shared/data/data.dartexamples/shared/data/datasources/api_client.dartuser_datasource.dartexamples/shared/data/models/api_response.dartuser.dartexamples/shared/widgets/trip_summary_card.dart詳細
Flutter BLoC 開発
このスキルは、このコードベースでのすべての Flutter 開発において、BLoC による状態管理、厳格なレイヤー分離、デザインシステム定数の必須使用を徹底します。
判断ツリー:アプローチの選択
User task → What are they building?
│
├─ New screen/feature → Full feature implementation:
│ 1. Create feature folder (lib/[feature]/)
│ 2. Define BLoC (bloc/[feature]_event.dart, _state.dart, _bloc.dart)
│ 3. Create data layer (data/datasources/, data/repositories/, data/models/)
│ 4. Build UI (view/[feature]_page.dart, view/widgets/)
│ 5. Create barrel files ([feature].dart, data/data.dart, view/view.dart)
│
├─ New widget only → Presentation layer:
│ 1. Feature-specific: feature/view/widgets/
│ 2. Shared/reusable: shared/widgets/
│ 3. Use design system constants (NO hardcoded values)
│ 4. Connect to existing BLoC if needed
│
├─ Data integration → Data layer only:
│ 1. Create datasource (feature/data/datasources/)
│ 2. Create repository (feature/data/repositories/)
│ 3. Wire up in existing or new BLoC
│
└─ Refactoring → Identify violations:
1. Check for hardcoded colors/spacing/typography
2. Check for business logic in UI
3. Check for direct SDK calls outside datasources
4. Check for missing Loading state before async operations
5. Check for missing Equatable on Events/States
6. Check for improper error handling (use SnackBar + AppColors.error)
アーキテクチャの概要
フィーチャー優先の構成(公式の BLoC 推奨):
lib/
├── [feature]/ # Feature folder (e.g., earnings/, auth/, trips/)
│ ├── bloc/
│ │ ├── [feature]_bloc.dart
│ │ ├── [feature]_event.dart
│ │ └── [feature]_state.dart
│ ├── data/
│ │ ├── datasources/ # Feature-specific API calls
│ │ ├── repositories/ # Data orchestration
│ │ ├── models/ # Feature-specific DTOs
│ │ └── data.dart # Data layer barrel file
│ ├── view/
│ │ ├── [feature]_page.dart # Main screen
│ │ ├── widgets/ # Feature-specific widgets
│ │ └── view.dart # View barrel file
│ └── [feature].dart # Feature barrel file
├── shared/ # Cross-feature code
│ ├── data/
│ │ ├── datasources/ # Shared API clients (ApiClient, UserDataSource)
│ │ ├── models/ # Shared models (User, ApiResponse)
│ │ └── data.dart # Shared data barrel file
│ ├── widgets/ # Reusable UI components
│ └── utils/ # Design system (colors, spacing, typography)
└── app.dart # App entry point
フィーチャーと共有データの使い分け
| シナリオ | 配置場所 | 例 |
|---|---|---|
| 1 つのフィーチャーで使用する API エンドポイント | feature/data/ | EarningsDataSource → /api/earnings/... |
| 複数のフィーチャーで使用する API クライアント/サービス | shared/data/ | ApiClient、UserDataSource |
| 1 つのフィーチャーで使用するモデル | feature/data/models/ | EarningsSummary |
| 複数のフィーチャーで使用するモデル | shared/data/models/ | User、ApiResponse |
バレルファイル — レイヤーごとに 1 つのインポート:
// Feature barrel: earnings/earnings.dart
export 'bloc/earnings_bloc.dart';
export 'bloc/earnings_event.dart';
export 'bloc/earnings_state.dart';
export 'data/data.dart';
export 'view/view.dart';
// Data layer barrel: earnings/data/data.dart
export 'datasources/earnings_datasource.dart';
export 'repositories/earnings_repository.dart';
export 'models/earnings_summary.dart';
// Shared data barrel: shared/data/data.dart
export 'datasources/api_client.dart';
export 'datasources/user_datasource.dart';
export 'models/user.dart';
主なルール:
- すべての状態変更は BLoC を経由します
- データソース以外でバックエンド SDK を直接呼び出しません
- ハードコードされた値は一切使用しません(色、間隔、タイポグラフィ)
- すべてのデータアクセスにリポジトリパターンを使用します
- フィーチャー固有のコードはフィーチャーフォルダーに配置します
- 共有コード(2 つ以上のフィーチャーで使用)は
shared/に配置します
BLoC の実装
イベント → 状態 → BLoC(フィーチャーごとに 3 ファイル)
イベント — ユーザー操作とシステムによるトリガー:
abstract class FeatureEvent extends Equatable {
const FeatureEvent();
@override
List<Object?> get props => [];
}
class FeatureActionRequested extends FeatureEvent {
final String param;
const FeatureActionRequested({required this.param});
@override
List<Object> get props => [param];
}
状態 — UI で起こりうるすべての状態:
abstract class FeatureState extends Equatable {
const FeatureState();
@override
List<Object?> get props => [];
}
class FeatureInitial extends FeatureState {}
class FeatureLoading extends FeatureState {}
class FeatureSuccess extends FeatureState {
final DataType data;
const FeatureSuccess(this.data);
@override
List<Object> get props => [data];
}
class FeatureError extends FeatureState {
final String message;
const FeatureError(this.message);
@override
List<Object> get props => [message];
}
BLoC — Loading → Success/Error パターンによるイベントハンドラー:
class FeatureBloc extends Bloc<FeatureEvent, FeatureState> {
final FeatureRepository _repository;
FeatureBloc({required FeatureRepository repository})
: _repository = repository,
super(FeatureInitial()) {
on<FeatureActionRequested>(_onActionRequested);
}
Future<void> _onActionRequested(
FeatureActionRequested event,
Emitter<FeatureState> emit,
) async {
emit(FeatureLoading());
try {
final result = await _repository.doSomething(event.param);
emit(FeatureSuccess(result));
} catch (e) {
emit(FeatureError(e.toString()));
}
}
}
重要:非同期処理の前に必ず Loading を発行し、その後 Success または Error を発行します。ローディング状態を省略しないでください。
データレイヤー
データフロー:
UI Event → BLoC (emit Loading) → Repository → Datasource (SDK)
↓
Response → Repository (map to entity) → BLoC (emit Success/Error) → UI
データソース — バックエンド SDK の呼び出しのみ:
class FeatureDataSource {
final SupabaseClient _supabase;
FeatureDataSource(this._supabase);
Future<Map<String, dynamic>> fetch() async {
return await _supabase.from('table').select().single();
}
}
リポジトリ — オーケストレーションとマッピング:
class FeatureRepository {
final FeatureDataSource _dataSource;
FeatureRepository(this._dataSource);
Future<DomainEntity> fetchData() async {
final response = await _dataSource.fetch();
return DomainEntity.fromJson(response);
}
}
デザインシステム(必須)
色
✅ AppColors.primary、AppColors.error、AppColors.textPrimary
❌ Color(0xFF...)、Colors.blue、インラインの 16 進数値
間隔
✅ AppSpacing.xs (4)、AppSpacing.sm (8)、AppSpacing.md (16)、AppSpacing.lg (24)、AppSpacing.xl (32)
✅ AppSpacing.screenHorizontal (24)、AppSpacing.screenVertical (16)
❌ EdgeInsets.all(16.0)、ハードコードされたパディング値
角丸
✅ AppRadius.sm (8)、AppRadius.md (12)、AppRadius.lg (16)、AppRadius.xl (24)
❌ BorderRadius.circular(12)、インラインの角丸値
タイポグラフィ
✅ AppTypography.headlineLarge、AppTypography.bodyMedium、theme.textTheme.bodyMedium
❌ TextStyle(fontSize: 16)、インラインのテキストスタイル
UI パターン
画面テンプレート
GradientScaffold(
body: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(AppSpacing.screenHorizontal),
child: HeaderWidget(),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.screenHorizontal),
child: ContentWidget(),
),
),
Padding(
padding: const EdgeInsets.all(AppSpacing.screenHorizontal),
child: ActionButton(
onPressed: () => context.read<FeatureBloc>().add(ActionEvent()),
),
),
],
),
),
)
BLoC Consumer パターン
BlocConsumer<FeatureBloc, FeatureState>(
listener: (context, state) {
if (state is FeatureError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message), backgroundColor: AppColors.error),
);
}
},
builder: (context, state) {
if (state is FeatureLoading) return const Center(child: CircularProgressIndicator());
if (state is FeatureSuccess) return SuccessWidget(data: state.data);
return const SizedBox.shrink();
},
)
よくある落とし穴
❌ ウィジェット内のビジネスロジック → BLoC に移動します
❌ リポジトリ内での Supabase/Firebase の直接呼び出し → データソースに移動します
❌ 非同期処理前のローディング状態の省略 → 必ず最初に Loading を発行します
❌ Color(0xFF4A90A4) のようなハードコードされた色 → AppColors.primary を使用します
❌ padding: 16 のようなマジックナンバー → AppSpacing.md を使用します
クイックリファレンス
| 操作 | パターン |
|---|---|
| イベントをディスパッチする | context.read<Bloc>().add(Event()) |
| インラインで状態を監視する | context.watch<Bloc>().state |
| 監視 + ビルド | BlocConsumer |
| 監視のみ | BlocListener |
| ビルドのみ | BlocBuilder |
提出前のチェックリスト
- Events/States/BLoC で
Equatableを使用している - すべての非同期処理が Loading → Success/Error になっている
- UI にビジネスロジックがない
- データソース以外に SDK 呼び出しがない
- 色/間隔/タイポグラフィが一切ハードコードされていない
- エラー処理で
AppColors.errorを使った SnackBar を表示する -
dart formatでコードをフォーマットしている