.agents/skills/implement-widget/SKILL.md
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.
npx skillsauth add andrelucassvt/CleanMacForDevsWeb implement-widgetInstall 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.
context.l10n — nunca string hardcoded.dispose() deles.lib/
├── presentation/<feature>/
│ ├── widgets/ # Widgets REUTILIZÁVEIS da feature
│ │ ├── <feature>_card.dart
│ │ ├── <feature>_list_item.dart
│ │ └── <feature>_form.dart
│ └── content/ # Auxiliares de UI ESPECÍFICOS de uma View (não reutilizáveis)
│ └── <feature>_content.dart
│
└── common/
└── widgets/ # Widgets COMPARTILHADOS entre features
├── app_button.dart
├── app_input.dart
└── app_card.dart
| Critério | widgets/ | content/ | common/widgets/ |
|---|---|---|---|
| Reutilizável dentro da feature? | ✅ Sim | ❌ Não | — |
| Usado em várias features? | ❌ Não | ❌ Não | ✅ Sim |
| Auxiliar específico de uma View? | ❌ Não | ✅ Sim | ❌ Não |
| Exemplo | ProfileCard, HomeItemList | RecursosContent, HomeEmptySection | AppButton, AppCard |
Regra: comece sempre em widgets/; mova para content/ se for específico demais para uma única View, para common/widgets/ apenas quando outra feature precisar.
Para entender por que não usar
Widget _buildXxx()na View, ver skillimplement-view.
Use quando o widget apenas renderiza dados recebidos.
import 'package:base_app/domain/entities/user_entity.dart';
import 'package:base_app/l10n/l10n.dart';
import 'package:flutter/material.dart';
class ProfileCard extends StatelessWidget {
const ProfileCard({
required this.user, // ✅ Entity completa
super.key,
});
final UserEntity user;
@override
Widget build(BuildContext context) {
final l10n = context.l10n; // ✅ Obtém traduções
return Card(
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(user.name, style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 8),
Text(user.email),
const SizedBox(height: 4),
Text(l10n.ageLabel(user.age)), // ✅ i18n com parâmetro
],
),
),
);
}
}
Regras:
const no construtorfinalTheme.of(context) para estilosUse quando o widget tem estado interno (controllers, animações, timers).
import 'package:base_app/domain/entities/user_entity.dart';
import 'package:flutter/material.dart';
class ProfileForm extends StatefulWidget {
const ProfileForm({
required this.user,
required this.onSave,
super.key,
});
final UserEntity user;
final void Function(String name, String email) onSave;
@override
State<ProfileForm> createState() => _ProfileFormState();
}
class _ProfileFormState extends State<ProfileForm> {
late final TextEditingController _nameController;
late final TextEditingController _emailController;
final _formKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.user.name);
_emailController = TextEditingController(text: widget.user.email);
}
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
super.dispose();
}
void _handleSave() {
if (_formKey.currentState?.validate() ?? false) {
widget.onSave(_nameController.text, _emailController.text);
}
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _nameController,
decoration: InputDecoration(labelText: l10n.nameLabel),
validator: (v) => v?.isEmpty ?? true ? l10n.nameRequired : null,
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: InputDecoration(labelText: l10n.emailLabel),
validator: (v) => v?.isEmpty ?? true ? l10n.emailRequired : null,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _handleSave,
child: Text(l10n.saveButton),
),
],
),
);
}
}
Regras:
initState()dispose() de controllerslate final para controllerswidget.parametrovoid Function(...) ou Future<void> Function(...)StatefulWidget apenas para receber dadosimport 'package:base_app/domain/entities/product_entity.dart';
import 'package:base_app/l10n/l10n.dart';
import 'package:flutter/material.dart';
class ProductListItem extends StatelessWidget {
const ProductListItem({
required this.product,
required this.onTap,
super.key, // Sempre repasse a key — permite ao Flutter reconciliar corretamente em listas
});
final ProductEntity product;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return ListTile(
leading: CircleAvatar(backgroundImage: NetworkImage(product.imageUrl)),
title: Text(product.name),
subtitle: Text(l10n.currencyLabel(product.price)),
trailing: const Icon(Icons.chevron_right),
onTap: onTap,
);
}
}
Uso na View:
ListView.builder(
itemCount: state.products.length,
itemBuilder: (context, index) {
final product = state.products[index];
return ProductListItem(
key: ValueKey(product.id), // ✅ Use ValueKey com ID único em listas dinâmicas
product: product,
onTap: () => _cubit.selectProduct(product),
);
},
)
class ItemCard extends StatelessWidget {
const ItemCard({
required this.item,
required this.onEdit,
required this.onDelete,
super.key,
});
final ItemEntity item;
final VoidCallback onEdit;
final VoidCallback onDelete;
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(
title: Text(item.title),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(icon: const Icon(Icons.edit), onPressed: onEdit),
IconButton(icon: const Icon(Icons.delete), onPressed: onDelete),
],
),
),
);
}
}
Widgets em content/ são específicos de uma View e podem chamar métodos do Cubit via context.read<>() — desde que a View envolva o subtree com BlocProvider.value. Isso evita a necessidade de repassar callbacks em cadeia.
// lib/presentation/profile/content/profile_save_bar.dart
class ProfileSaveBar extends StatelessWidget {
const ProfileSaveBar({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton(
// ✅ context.read — não causa rebuild; só despacha a ação
onPressed: () => context.read<ProfileCubit>().saveProfile(),
child: Text(context.l10n.saveButton),
),
);
}
}
Widgets em
widgets/devem ser genéricos e reutilizáveis: prefira receber callbacks como parâmetros em vez de acessar o Cubit diretamente. Reservecontext.read<>()para widgets emcontent/.
| Local | Acessa Cubit via context.read? | Recebe callback como parâmetro? |
|---|---|---|
| widgets/ (reutilizável) | ❌ Não — acoplaria o widget ao Cubit | ✅ Sim |
| content/ (específico da View) | ✅ Sim — via BlocProvider.value na View | Opcional |
| common/widgets/ | ❌ Não | ✅ Sim |
Precisa de controller/timer/animação?
├─ SIM → StatefulWidget
└─ NÃO → StatelessWidget
Será usado em várias features?
├─ SIM → common/widgets/
└─ NÃO → É específico de uma única View (não reutilizável)?
├─ SIM → presentation/<feature>/content/
└─ NÃO → presentation/<feature>/widgets/
Tem entity relacionada?
├─ SIM → prefira passar a entity completa
└─ NÃO → passe parâmetros primitivos
context.l10n.<chave>dispose() implementado para controllers/timerspresentation/<feature>/widgets/ ou common/widgets/| Erro | Correto |
|---|---|
| UserCard(name: user.name, email: user.email) | UserCard(user: user) |
| StatefulWidget sem estado interno | StatelessWidget |
| TextEditingController sem dispose() | Implementar dispose() com _controller.dispose() |
| Widget com 3 linhas extraído desnecessariamente | Use Text(...) inline |
| Strings hardcoded Text('Nome:') | Text(l10n.nameLabel) |
Ú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.
tools
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.
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.