.github/skills/implement-view/SKILL.md
Implements Flutter View screens following the project architecture. Use whenever creating or modifying a View (StatefulWidget + Cubit + BlocBuilder), adding a new screen, wiring up BlocBuilder/BlocConsumer/BlocListener, setting up SafeArea, or navigating from the View. Covers State, Cubit, View file, route, DI registration, and common mistakes. Activate even when the user just says "create a screen" or "add a new page", without explicitly mentioning Cubit or BLoC.
npx skillsauth add andrelucassvt/CleanMacForDevsWeb implement-viewInstall 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.
StatefulWidget, obtenha o Cubit via AppInjector.inject.get<XCubit>(), use BlocBuilder para reagir a estados._cubit.load*() no initState() — use didChangeDependencies() apenas se precisar do context.context.push/go/pop na View ou em BlocListener — nunca passe BuildContext ao Cubit.context.l10n.<chave> — nunca string hardcoded.dispose() com _cubit.close().Widget _buildHeader()) nem classes privadas de widget dentro do arquivo de View. Extraia para widgets/ (reutilizável) ou content/ (auxiliar específico da View).showDialog(), showModalBottomSheet() ou similares podem permanecer na View.SafeArea.CRIAÇÃO MÍNIMA: Crie apenas o essencial. Não adicione estruturas (entities, repositories, datasources) até que sejam realmente necessárias.
Ao criar uma feature chamada profile, crie APENAS:
lib/presentation/profile/
├── view/
│ └── profile_view.dart # ✅ OBRIGATÓRIO
├── view_model/
│ ├── profile_cubit.dart # ✅ OBRIGATÓRIO
│ └── profile_state.dart # ✅ OBRIGATÓRIO
├── (widgets/) # ❌ Criar apenas se houver widgets reutilizáveis
│ └── profile_form.dart
└── (content/) # ❌ Criar apenas se houver auxiliares específicos da View
└── profile_content.dart
NÃO CRIAR automaticamente:
Crie <feature>_state.dart (sealed class com Initial, Loading, Loaded, Error) e <feature>_cubit.dart.
Ver skill implement-view-model para detalhes.
Arquivo: lib/presentation/<feature>/view/<feature>_view.dart
import 'package:base_app/config/inject/app_injector.dart';
import 'package:base_app/l10n/l10n.dart';
import 'package:base_app/presentation/profile/view_model/profile_cubit.dart';
import 'package:base_app/presentation/profile/view_model/profile_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ProfileView extends StatefulWidget {
const ProfileView({super.key});
@override
State<ProfileView> createState() => _ProfileViewState();
}
class _ProfileViewState extends State<ProfileView> {
final _cubit = AppInjector.inject.get<ProfileCubit>();
@override
void initState() {
super.initState();
_cubit.loadProfile();
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
// Use BlocProvider.value para expor o Cubit ao subtree inteiro.
// Assim, widgets em `widgets/` e `content/` podem chamar
// context.read<ProfileCubit>() sem receber callbacks.
return BlocProvider.value(
value: _cubit,
child: Scaffold(
appBar: AppBar(
title: Text(l10n.counterAppBarTitle),
),
body: SafeArea(
top: false, // AppBar já protege o topo
child: BlocBuilder<ProfileCubit, ProfileState>(
// Sem bloc: _cubit — obtido via BlocProvider acima
builder: (context, state) => switch (state) {
ProfileLoading() => const Center(child: CircularProgressIndicator()),
ProfileError(:final message) => Center(
child: Text(message, style: const TextStyle(color: Colors.red)),
),
ProfileLoaded(:final name, :final email) => Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${l10n.profileNameLabel} $name'),
const SizedBox(height: 8),
Text('${l10n.profileEmailLabel} $email'),
],
),
),
ProfileInitial() => const SizedBox.shrink(),
},
),
),
),
);
}
@override
void dispose() {
_cubit.close();
super.dispose();
}
}
Regras da View:
StatefulWidgetAppInjector (DI)BlocProvider.value para expor o Cubit ao subtreeBlocBuilder sem bloc: quando BlocProvider.value está acimaloadData() no initState()context.l10n e SEMPRE usa para textos visíveisdispose()SafeAreaWidget _buildXxx() dentro da Viewbloc: diretoEscolha o padrão com base em quem precisa acessar o Cubit:
| Situação | Padrão recomendado |
|---|---|
| Só o BlocBuilder da View precisa do Cubit | BlocBuilder(bloc: _cubit, ...) — sem BlocProvider |
| Widgets extraídos (content/, widgets/) chamam o Cubit | BlocProvider.value(value: _cubit, ...) + BlocBuilder sem bloc: |
// Padrão simples — sem BlocProvider.value
body: BlocBuilder<ProfileCubit, ProfileState>(
bloc: _cubit, // ← obrigatório quando não há BlocProvider acima
builder: (context, state) { /* ... */ },
)
// Padrão com acesso no subtree — use BlocProvider.value
return BlocProvider.value(
value: _cubit,
child: Scaffold(
body: BlocBuilder<ProfileCubit, ProfileState>(
// sem bloc: — BlocBuilder encontra o cubit via context
builder: (context, state) { /* ... */ },
),
),
);
Quando um widget em content/ ou widgets/ precisa chamar um método do Cubit, use context.read<>() — nunca passe o Cubit como parâmetro:
// lib/presentation/profile/content/profile_action_bar.dart
class ProfileActionBar extends StatelessWidget {
const ProfileActionBar({super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(
// ✅ context.read — não rebuild; só chama o método
onPressed: () => context.read<ProfileCubit>().saveProfile(),
child: Text(context.l10n.saveButton),
);
}
}
context.read<>()não causa rebuild — use-o apenas dentro de callbacks. Para exibir dados do estado, usecontext.watch<>()ouBlocBuilder.
| Cenário | SafeArea? |
|---|---|
| Scaffold com AppBar | ✅ Envolver o body com top: false (AppBar protege o topo) |
| Scaffold sem AppBar | ✅ Envolver o body (protege topo e bottom) |
| Tela fullscreen (splash, onboarding) | ✅ Envolver todo o conteúdo principal |
| Modal/BottomSheet | ✅ Envolver conteúdo com SafeArea(bottom: true) |
body: SafeArea(
top: false, // AppBar já protege o topo
child: BlocBuilder<HomeCubit, HomeState>(
bloc: _cubit,
builder: (context, state) { /* ... */ },
),
),
body: SafeArea(
child: BlocBuilder<SplashCubit, SplashState>(
bloc: _cubit,
builder: (context, state) { /* ... */ },
),
),
Métodos Widget _buildXxx() não têm Element próprio — toda mudança de estado reconstrói o bloco inteiro. Extraia para widgets/ (reutilizável) ou content/ (auxiliar específico da View).
Para exemplos detalhados de como extrair widgets corretamente, veja a skill
implement-widget.
void _showLanguageDialog(String current) {
showDialog<void>(context: context, builder: (_) => AlertDialog(/*...*/));
}
void _showOptionsBottomSheet() {
showModalBottomSheet<void>(context: context, builder: (_) => SafeArea(/*...*/));
}
| Tipo | Permitido na View? |
|---|---|
| void _showXxxDialog() | ✅ Sim |
| void _showXxxBottomSheet() | ✅ Sim |
| void _onTapXxx() (handler) | ✅ Sim |
| Widget _buildXxx() | ❌ Não — extrair para widgets/ ou content/ |
| class _XxxContent extends StatelessWidget | ❌ Não — extrair para content/ |
// lib/config/routes/app_routes.dart
class AppRoutes {
static const String profile = '/profile'; // ✅ Adicionar
}
// lib/config/routes/app_router.dart
GoRoute(
path: AppRoutes.profile,
builder: (context, state) => const ProfileView(),
),
// lib/config/inject/app_injector.dart
inject.registerFactory<ProfileCubit>(() => ProfileCubit());
// Com repository:
inject.registerFactory<ProfileCubit>(() => ProfileCubit(inject()));
builder: (context, state) => switch (state) {
ProfileLoaded(:final items) => ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return ListTile(
title: Text(item.name),
onTap: () => context.read<ProfileCubit>().selectItem(item),
);
},
),
ProfileLoading() => const Center(child: CircularProgressIndicator()),
ProfileError(:final message) => Center(child: Text(message)),
ProfileInitial() => const SizedBox.shrink(),
},
Quando a mesma View precisa reagir a estados e atualizar a UI, use BlocConsumer. Nunca aninhe BlocListener e BlocBuilder para o mesmo bloc — isso cria dois listeners desnecessários.
// ✅ CORRETO — um único widget para listener + builder
BlocConsumer<ProfileCubit, ProfileState>(
listener: (context, state) {
if (state is ProfileSaved) {
AppSnackbar.showSucess(context, message: context.l10n.profileSavedMessage);
context.pop();
}
if (state is ProfileError) {
AppSnackbar.showError(context, message: state.message);
}
},
builder: (context, state) { /* ... */ },
)
// ❌ ERRADO — BlocListener + BlocBuilder aninhados para o MESMO bloc
BlocListener<ProfileCubit, ProfileState>(
listener: (context, state) { /* ... */ },
child: BlocBuilder<ProfileCubit, ProfileState>( // ❌ redundante
builder: (context, state) { /* ... */ },
),
)
Use BlocListener sozinho somente quando não há BlocBuilder para o mesmo bloc no mesmo nível, ou quando o listener observa um bloc diferente do que constrói a UI.
// ✅ CORRETO — listener para um bloc, builder para outro
BlocListener<AuthCubit, AuthState>(
listener: (context, state) {
if (state is AuthLoggedOut) context.go(AppRoutes.login);
},
child: BlocBuilder<ProfileCubit, ProfileState>( // bloc diferente ✅
builder: (context, state) { /* ... */ },
),
)
// Opção A: navegação direta
ElevatedButton(
onPressed: () => context.push('/details/${item.id}'),
child: Text(l10n.detailsButton),
)
// Opção B: estado de navegação
class ProfileNavigateToDetails extends ProfileState {
const ProfileNavigateToDetails(this.id);
final String id;
}
BlocListener<ProfileCubit, ProfileState>(
listener: (context, state) {
if (state is ProfileNavigateToDetails) context.push('/details/${state.id}');
},
child: /* ... */,
)
widgets/ vs content/ — qual usar?| Critério | widgets/ | content/ |
|---|---|---|
| Pode ser reaproveitado em outro lugar? | ✅ Sim | ❌ Não (específico desta View) |
| É um bloco estrutural/auxiliar da View? | Talvez | ✅ Sim |
| Exemplo | ProfileCard, HomeItemList | RecursosContent, HomeEmptySection |
<feature>_state.dart e <feature>_cubit.dart<feature>_view.dart (StatefulWidget, SafeArea, BlocBuilder, todos os estados)app_routes.dartGoRoute em app_router.dartapp_injector.dartCriar depois, SE necessário:
widgets/content/domain/entities/| Erro | Correto |
|---|---|
| import '../widgets/profile_card.dart' | import 'package:base_app/...' |
| inject.registerSingleton<ProfileCubit>() | inject.registerFactory<ProfileCubit>() |
| Não implementar dispose() | _cubit.close(); super.dispose() |
| Tratar apenas Loading e Loaded | Tratar Initial, Loading, Loaded, Error |
| Strings hardcoded Text('Salvar') | Text(l10n.saveButton) |
Última atualização: 28 de março de 2026
testing
Create new skills, modify and improve existing skills, and measure skill performance. Use when users want to create a skill from scratch, edit, or optimize an existing skill, run evals to test a skill, benchmark skill performance with variance analysis, or optimize a skill's description for better triggering accuracy. Activate even when the user says 'create a skill for X', 'the skill is not triggering', 'improve this skill description', 'the agent is not using the skill', 'add a skill to teach the agent how to do X', 'this skill is wrong', or 'update the skill' without explicitly mentioning evals or benchmark.
development
Implements Flutter reusable widgets following the project architecture. Use whenever creating or modifying widgets in presentation/<feature>/widgets/, presentation/<feature>/content/, or common/widgets/. Covers StatelessWidget vs StatefulWidget decision, Entity as parameter, i18n, dispose, componentization rules, and when to access the Cubit via context.read. Activate even when the user says 'extract this to a widget', 'create a list item widget', 'build a reusable card', 'factor out this UI block', 'create a component for this', or 'this View is getting too big' without explicitly mentioning StatelessWidget or reusable components.
testing
Implements Flutter Cubit and State (View Model layer) following the project architecture. Use whenever creating or modifying a Cubit or State class, adding an async method to a Cubit, handling form submission or validation, implementing debounce search, managing loading/error/navigation states, or wiring a Cubit to a Repository or StorageService. Covers sealed States, async patterns with Result<T>, CRUD Cubits, local persistence via StorageService, navigation states, debounce, and common mistakes. Activate even when the user says "add a method", "handle the loading state", or "save locally" without explicitly mentioning Cubit or BLoC.
tools
Implements Common Services in common/services/ following the project architecture. Use whenever creating or modifying a service that abstracts device/platform resources — storage flags, feature gating, one-time actions, onboarding control, premium access checks, usage counters, biometric auth, or any local logic that doesn't belong in a Repository. Covers abstract interface + concrete implementation, StorageService delegation, DI registration as LazySingleton, and injection into Cubits. Activate even when the user says 'only allow one click', 'show onboarding once', 'check if user is premium', 'save a flag locally', 'create a service for X', 'where do I put this logic', 'gate this feature behind premium', 'track how many times the user did X', 'remember that the user already saw this', or 'limit usage without backend' without explicitly mentioning StorageService, SharedPreferences, or common/services.