plugins/languages/flutter/skills/state/SKILL.md
Flutter 状态管理规范 — Riverpod 3.x (首选, 含 mutations/offline persistence/generic codegen)、Bloc 8.x (企业级)、AsyncValue 异步状态、ref.watch/listen/read 正确用法。当用户设计数据流、实现 Provider/Bloc/Notifier、讨论 "状态管理"、"Riverpod"、"Bloc"、"setState 替代方案"、"依赖注入" 时加载。
npx skillsauth add lazygophers/ccplugin flutter-stateInstall 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.
| 方案 | 场景 | 评价 | | --- | --- | --- | | Riverpod 3.x | 中大型新项目 | 首选 — 编译期安全、自动 dispose、代码生成 | | Bloc 8.x | 企业级、强分层 | 推荐 — 事件驱动、可预测、强测试 | | Provider | 遗留项目 | 不推荐,已停维,迁 Riverpod | | GetX | — | 禁止,技术债 | | StateNotifier | — | Riverpod 3 已弃用,迁 Notifier/AsyncNotifier |
铁律: 全项目只用一种方案。
dependencies:
flutter_riverpod: ^3.0.0
riverpod_annotation: ^3.0.0
dev_dependencies:
riverpod_generator: ^3.0.0
build_runner: ^2.4.0
riverpod_lint: ^3.0.0
custom_lint: ^0.6.0
@riverpod 代码生成// 简单值
@riverpod
int counter(Ref ref) => 0;
// 异步数据
@riverpod
Future<List<User>> users(Ref ref) async {
final repo = ref.watch(userRepositoryProvider);
return repo.fetchAll();
}
// AsyncNotifier (替代 StateNotifier)
@riverpod
class AuthController extends _$AuthController {
@override
FutureOr<AuthState> build() async {
final user = await ref.watch(authRepoProvider).getCurrentUser();
return user != null ? Authenticated(user) : const Unauthenticated();
}
Future<void> signIn(String email, String password) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
final user = await ref.read(authRepoProvider).signIn(email, password);
return Authenticated(user);
});
}
}
final usersAsync = ref.watch(usersProvider);
return usersAsync.when(
data: (users) => UserList(users),
loading: () => const CircularProgressIndicator(),
error: (e, _) => ErrorView(error: e, onRetry: () => ref.invalidate(usersProvider)),
);
ref.watch / ref.listen / ref.readWidget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider); // build: 监听 + 重建
ref.listen(authControllerProvider, (prev, next) { // build: 监听副作用 (不重建)
if (next.hasError) showSnackBar(next.error.toString());
});
return ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).inc(), // 事件: 只读
child: Text('$count'),
);
}
select 性能优化// 只在 title 变化时重建
final title = ref.watch(productProvider(id).select((p) => p.value?.title));
// Dart 3 sealed event/state
sealed class AuthEvent {}
final class SignInRequested extends AuthEvent {
SignInRequested({required this.email, required this.password});
final String email;
final String password;
}
sealed class AuthState { const AuthState(); }
final class AuthInitial extends AuthState { const AuthInitial(); }
final class AuthLoading extends AuthState { const AuthLoading(); }
final class Authenticated extends AuthState {
const Authenticated(this.user);
final User user;
}
final class AuthError extends AuthState {
const AuthError(this.message);
final String message;
}
class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthBloc(this._repo) : super(const AuthInitial()) {
on<SignInRequested>(_onSignIn);
}
final AuthRepository _repo;
Future<void> _onSignIn(SignInRequested e, Emitter<AuthState> emit) async {
emit(const AuthLoading());
try {
emit(Authenticated(await _repo.signIn(e.email, e.password)));
} on AuthException catch (ex) {
emit(AuthError(ex.message));
}
}
}
// 使用 (Dart 3 pattern matching)
BlocBuilder<AuthBloc, AuthState>(
builder: (ctx, state) => switch (state) {
AuthInitial() || AuthLoading() => const LoadingView(),
Authenticated(:final user) => ProfileView(user: user),
AuthError(:final message) => ErrorView(message: message),
},
);
// Riverpod
test('AuthController signIn', () async {
final container = ProviderContainer(overrides: [
authRepoProvider.overrideWithValue(MockAuthRepository()),
]);
addTearDown(container.dispose);
await container.read(authControllerProvider.notifier).signIn('e', 'p');
expect(container.read(authControllerProvider).value, isA<Authenticated>());
});
// Bloc
blocTest<AuthBloc, AuthState>(
'emits [Loading, Authenticated]',
build: () => AuthBloc(MockAuthRepository()),
act: (b) => b.add(SignInRequested(email: 'e', password: 'p')),
expect: () => [const AuthLoading(), isA<Authenticated>()],
);
| AI 借口 | 实际检查 | 严重度 |
| --- | --- | --- |
| "setState 就够了" | 是否用 Riverpod/Bloc? | 高 |
| "Provider 够用了" | Provider 停维,迁 Riverpod | 高 |
| "StateNotifier 也能用" | Riverpod 3 弃用,用 Notifier/AsyncNotifier | 高 |
| "手写 Provider 更清晰" | 是否用 @riverpod 代码生成? | 中 |
| "build 里 ref.read" | build 应 ref.watch | 高 |
| "异步直接 setState" | 是否统一 AsyncValue.when? | 高 |
| "ChangeNotifier 简单" | Notifier (Riverpod) 或 Cubit (Bloc) | 中 |
@riverpod 代码生成ref.watch (build) / ref.read (event) 正确分用AsyncValue.whenriverpod_lint / bloc_lint 通过Skills(flutter:core) / Skills(flutter:ui)tools
UI/UX 与布局设计——做界面布局/结构/导航/组件/交互的设计决策。触发:做UI/UX/布局/排版/导航/组件/交互/栅格/响应式/图表选型/字体配对。按媒介路由 HTML/Web、原生 App(iOS/Android/桌面)、CLI、TUI。需后端动态系统不适用;配色/主题/色板走姊妹 skill design-color。
tools
主题与配色设计——做颜色搭配/调色板/主题/品牌色阶/暗模式的设计决策。触发:选配色/调色/主题/色板/品牌色/暗模式/对比度/色盲/UI风格。按媒介路由 HTML/Web(CSS变量)、原生App(平台token)、CLI(ANSI)、TUI(真彩/256/16降级)。保证可访问性(对比度/色盲安全)。需后端动态系统不适用;UI/UX 布局/组件/交互走姊妹 skill design-uiux。
tools
跨任意组件(plugin/skill/agent/command)的验证驱动优化循环纪律 skill。当用户要优化某个已有组件却无明确方向、或要防止改了反而更差(自评乐观偏差 / 多维同改归因失效 / 为凑分加废话膨胀)、或要把一套通用「评分→单变量改→改后验证严格更好才留否则回滚→触顶停」的纪律套到任意组件上时使用。管优化过程本身的纪律(validation gate / ratchet / 独立验证 / 触顶停),不评单组件深度(交 skill-dev),不查插件接线(交 plugin-dev)。仅手动 /optimize-any 触发。
data-ai
两层规则记忆 (基于 .skein/spec)。planning 时 recall 召回相关规则、task finish 后 sediment 沉淀学习 + prune 自动精简过期/重复/断链规则。core 常驻硬规 + recall 按需召回, 经判定门自动写盘 (不逐次问用户)。产出 .skein/spec 下 core/recall 规则文件 + index。另支持空仓 bootstrap 播种规则基线、记忆大面积失效 (大重构/换栈) 时 reconstruct 可逆归档后按项目类型分型重建、maintain 手动体检 (超预算/stale/断链/重复/废弃, --apply 自动修复)、auto-fix (Stop hook 写 .pending-fix 标记 → main 派 skein-specer bg 跑 maintain --apply 全自动修, 断链只报告)。