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
--- name: trellisx-workspace description: 维护 `.trellis/task.md` 任务看板 —— trellis 缺的跨任务总览。**一个表格, 一行一个任务**, 列为 id/名称/描述/状态/阶段/进度/worktree (状态/阶段中文显示)。在 task create/start/阶段切换/archive 后**及时更新**对应行; 并**自动清理超 7 天的已完成行**防膨胀。保持看板与 task.json 实时一致。 when_to_use: 维护 / 创建 / 更新 `.trellis/task.md` 任务看板时; task 生命周期任一节点 (create/start/阶段推进/archive) 之后同步看板时; 用户问"当前有哪些任务 / 任务进度 / 任务看板"时。被 trellisx-flow 与 trellisx-apply 注入的流程引用。 user-invocable: true argument-hint: [show|update|sync|cleanup ...] [task id] arguments:
testing
强制以 Trellis task 闭环处理用户指定的请求 (自判新建/并入 → plan→exec→check→finish 全程不跳步)。**仅用户显式主动调用** (/trellisx-flow 或明确要求"强制走 task 处理这个"); **禁止自动 / 被动 / 推断式调用** —— 不要因为某个请求"看起来该建 task"就自动触发本 skill, 那是 apply 注入的 no_task 倾向的职责。
testing
把 强推task + subtask拆分 + worktree隔离 + 闭环收尾 四维度增量注入当前项目 .trellis/ (workflow.md 的 no_task/planning/in_progress 块 + spec 背书文档 + trellis 生命周期 hook worktree 自动化)。强推 task 与闭环为纯 prompt 软约束 (非平台 hook 硬拦截)。**纯增量追加, 绝不替换 trellis 原生文本** (no_task 分类+征同意/check/finish/前缀全保留)。幂等 (marker 包裹)。
development
Claude Code 会话历史整理 — 扫 ~/.claude/projects/**/*.jsonl 全部 session transcripts, 提取学习增量 (用户校正/决策/踩坑/L0 规则) → 全局记忆库 ~/.cortex/.wiki/memory/. 默认 --apply 落盘 (--dry-run opt-in 仅出 JSON plan 预览). 与 cortex-extract (L4-inbox 内部) 互补.