flutter-bloc-development by abdelhakrazi/flutter-bloc-clean-architecture-skill
npx skills add https://github.com/abdelhakrazi/flutter-bloc-clean-architecture-skill --skill flutter-bloc-development此技能强制要求在此代码库中的所有 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)
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
功能优先结构(官方 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
| 场景 | 位置 | 示例 |
|---|---|---|
| 仅一个功能使用的 API 端点 | feature/data/ | EarningsDataSource → /api/earnings/... |
| 多个功能使用的 API 客户端/服务 | shared/data/ | ApiClient, UserDataSource |
| 仅一个功能使用的模型 | feature/data/models/ | EarningsSummary |
| 多个功能使用的模型 | shared/data/models/ | User, ApiResponse |
Barrel 文件 — 每层单一导入:
// 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';
关键规则:
shared/事件 — 用户操作和系统触发器:
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 — 事件处理器,遵循加载 → 成功/错误模式:
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, 内联十六进制值
✅ 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), 内联文本样式
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()),
),
),
],
),
),
)
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();
},
)
❌ 业务逻辑在 widget 中 → 移至 BLoC ❌ 仓库中直接调用 Supabase/Firebase → 移至数据源 ❌ 异步操作前跳过加载状态 → 始终先发出 Loading ❌ 硬编码颜色如 Color(0xFF4A90A4) → 使用 AppColors.primary ❌ 魔法数字如 padding: 16 → 使用 AppSpacing.md
| 操作 | 模式 |
|---|---|
| 分发事件 | context.read<Bloc>().add(Event()) |
| 内联监听状态 | context.watch<Bloc>().state |
| 监听并构建 | BlocConsumer |
| 仅监听 | BlocListener |
| 仅构建 | BlocBuilder |
EquatableAppColors.error 的 SnackBardart format 格式化每周安装次数
115
仓库
GitHub 星标数
12
首次出现
2026年1月26日
安全审计
已安装于
opencode110
github-copilot108
gemini-cli105
codex104
kimi-cli91
amp90
This skill enforces BLoC state management, strict layer separation, and mandatory use of design system constants for all Flutter development in this codebase.
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)
Feature-first structure (official BLoC recommendation):
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
| Scenario | Location | Example |
|---|---|---|
| API endpoints used by ONE feature | feature/data/ | EarningsDataSource → /api/earnings/... |
| API client/service used by MANY features | shared/data/ | ApiClient, UserDataSource |
| Models used by ONE feature | feature/data/models/ |
Barrel Files — Single import per layer:
// 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';
Key Rules:
shared/Events — User actions and system triggers:
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];
}
States — All possible UI states:
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 — Event handlers with Loading → Success/Error pattern:
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()));
}
}
}
CRITICAL : Always emit Loading before async work, then Success or Error. Never skip the loading state.
Data Flow:
UI Event → BLoC (emit Loading) → Repository → Datasource (SDK)
↓
Response → Repository (map to entity) → BLoC (emit Success/Error) → UI
Datasource — Backend SDK calls only:
class FeatureDataSource {
final SupabaseClient _supabase;
FeatureDataSource(this._supabase);
Future<Map<String, dynamic>> fetch() async {
return await _supabase.from('table').select().single();
}
}
Repository — Orchestration and mapping:
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, inline hex values
✅ 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), hardcoded padding values
✅ AppRadius.sm (8), AppRadius.md (12), AppRadius.lg (16), AppRadius.xl (24) ❌ BorderRadius.circular(12), inline radius values
✅ AppTypography.headlineLarge, AppTypography.bodyMedium, theme.textTheme.bodyMedium ❌ TextStyle(fontSize: 16), inline text styles
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()),
),
),
],
),
),
)
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();
},
)
❌ Business logic in widgets → Move to BLoC ❌ Direct Supabase/Firebase calls in repository → Move to datasource ❌ Skipping loading state before async operations → Always emit Loading first ❌ Hardcoded colors like Color(0xFF4A90A4) → Use AppColors.primary ❌ Magic numbers like padding: 16 → Use AppSpacing.md
| Action | Pattern |
|---|---|
| Dispatch event | context.read<Bloc>().add(Event()) |
| Watch state inline | context.watch<Bloc>().state |
| Listen + Build | BlocConsumer |
| Listen only | BlocListener |
| Build only | BlocBuilder |
EquatableAppColors.errordart formatWeekly Installs
115
Repository
GitHub Stars
12
First Seen
Jan 26, 2026
Security Audits
Gen Agent Trust HubWarnSocketPassSnykPass
Installed on
opencode110
github-copilot108
gemini-cli105
codex104
kimi-cli91
amp90
Flutter 主屏幕小组件开发指南:iOS/Android 原生小组件集成与数据通信
3,500 周安装
EarningsSummary |
| Models used by MANY features | shared/data/models/ | User, ApiResponse |