| name | implement-widget |
| description | 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. |
Implement Widget â Flutter
Leitura RĂĄpida
- Quando extrair um widget: bloco de UI maior que 20 linhas ou repetido em mais de um lugar.
- Quando usar StatelessWidget: widget apenas renderiza dados recebidos, sem estado interno.
- Quando usar StatefulWidget: widget tem controllers, timers ou animaçÔes internas.
- Quando definir parĂąmetros: prefira passar a Entity completa em vez de campos individuais.
- Quando adicionar texto visĂvel: SEMPRE use
context.l10n â nunca string hardcoded.
- Quando tiver controllers/timers: SEMPRE faça
dispose() deles.
Onde colocar o widget
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 skill implement-view.
StatelessWidget (ImutĂĄvel)
Use quando o widget apenas renderiza dados recebidos.
â
CORRETO â Entity como parĂąmetro + i18n
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:
- â
Sempre
const no construtor
- â
Todos os campos
final
- â
Recebe entity completa (nĂŁo campos individuais)
- â
Usa
Theme.of(context) para estilos
- â NĂŁo mantĂ©m estado interno
- â NĂŁo tem controllers ou listeners
StatefulWidget (MutĂĄvel)
Use 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:
- â
Use quando houver controllers, timers, animaçÔes
- â
Inicialize controllers no
initState()
- â
SEMPRE faça
dispose() de controllers
- â
Use
late final para controllers
- â
Acesse parĂąmetros via
widget.parametro
- â
Callbacks:
void Function(...) ou Future<void> Function(...)
- â NĂŁo crie
StatefulWidget apenas para receber dados
Widget de Lista (List Item)
import '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),
);
},
)
Widget com Callback
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),
],
),
),
);
}
}
Acessando o Cubit de dentro de um Widget
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. Reserve context.read<>() para widgets em content/.
| 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 |
Quando Criar Widgets
â
CRIE quando:
- Bloco de UI tem mais de 20 linhas
- CĂłdigo se repete em dois ou mais lugares
- Hå separação lógica clara (cabeçalho, card, lista, formulårio)
â NĂO crie quando:
- Ă muito simples (< 10 linhas)
- Ă usado apenas uma vez e Ă© trivial
DecisĂŁo RĂĄpida
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
Checklist
Erros Comuns
| 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