Mejores Prácticas
Patrones listos para producción, tips de rendimiento y estrategias de testing para aplicaciones watch_it.
Patrones de Arquitectura
Widgets Auto-Contenidos
Los widgets deberían acceder a sus dependencias directamente desde get_it, no vía parámetros del constructor.
❌️ Malo - Pasar managers como parámetros:
// ❌ Bad - Passing managers as parameters
class PassingManagersBad extends WatchingWidget {
PassingManagersBad({required this.manager}); // DON'T DO THIS
final TodoManager manager;
@override
Widget build(BuildContext context) {
final todos = watch(manager.todos).value;
return ListView(children: todos.map((t) => Text(t.title)).toList());
}
}✅ Bueno - Acceder directamente:
// ✅ Good - Access directly
class AccessDirectlyGood extends WatchingWidget {
@override
Widget build(BuildContext context) {
final todos = watchValue((TodoManager m) => m.todos);
return ListView(children: todos.map((t) => Text(t.title)).toList());
}
}¿Por qué? Los widgets auto-contenidos son:
- Más fáciles de testear (mock de
get_it, no parámetros del constructor) - Más fáciles de refactorizar
- No exponen dependencias internas
- Pueden acceder a múltiples servicios sin explosión de parámetros
Separar Estado de UI del Estado de Negocio
Estado de UI local (entrada de formulario, expansión, selección):
// Local UI state
class ExpandableCard extends WatchingStatefulWidget {
State createState() => _ExpandableCardState();
}
class _ExpandableCardState extends State<ExpandableCard> {
bool _expanded = false; // Local UI state
@override
Widget build(BuildContext context) {
// Business state from watch_it
final data = watchValue((DataManager m) => m.data);
return ExpansionTile(
initiallyExpanded: _expanded,
onExpansionChanged: (expanded) => setState(() => _expanded = expanded),
title: Text(data),
children: [Text('Details')],
);
}
}Estado de negocio (datos de API, estado compartido):
// In manager - registered in get_it
class DataManagerExample {
final data = ValueNotifier<List<Item>>([]);
void fetchData() async {
// Simulated API call
await Future.delayed(Duration(milliseconds: 100));
data.value = [Item('Example 1'), Item('Example 2')];
}
}Estado Reactivo Local con createOnce
Para estado reactivo local del widget que no necesita registro en get_it, combina createOnce con watch:
class CounterWidget extends WatchingWidget {
const CounterWidget({super.key});
@override
Widget build(BuildContext context) {
// Create a local notifier that persists across rebuilds
final counter = createOnce(() => ValueNotifier<int>(0));
// Watch it directly (not from get_it)
final count = watch(counter).value;
return Column(
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: () => counter.value++,
child: Text('Increment'),
),
],
);
}
}Cuándo usar este patrón:
- El widget necesita su propio estado reactivo local
- El estado debería persistir a través de reconstrucciones (no recreado)
- El estado debería ser dispuesto automáticamente con el widget
- No quieres registrar en
get_it(verdaderamente local)
Beneficios clave:
createOncecrea el notifier una vez y lo dispone automáticamentewatchse suscribe a cambios y dispara reconstrucciones- No se necesita gestión manual de ciclo de vida
Optimización de Rendimiento
Observa Solo Lo Que Necesitas
Observa propiedades específicas, no objetos enteros. El enfoque depende de la estructura de tu manager:
Para managers con propiedades ValueListenable - usa watchValue():
❌️ Malo - Observar manager completo:
// ❌ Bad - Watching too much
class WatchingTooMuchBad extends WatchingWidget {
@override
Widget build(BuildContext context) {
final manager = watchIt<CounterModel>(); // Rebuilds on ANY change
final count = manager.count;
return Container();
}
}✅ Bueno - Observar propiedad ValueListenable específica:
// ✅ Good - Watch specific property
class WatchSpecificGood extends WatchingWidget {
@override
Widget build(BuildContext context) {
final todos = watchValue(
(TodoManager m) => m.todos); // Only rebuilds when todos change
return Container();
}
}Para managers ChangeNotifier - usa watchPropertyValue() para reconstruir solo cuando un valor de propiedad específica cambia:
❌️ Malo - Se reconstruye en cada llamada notifyListeners:
// ❌ Bad - Rebuilds on every settings change
class RebuildsOnEverySettingsBad extends WatchingWidget {
@override
Widget build(BuildContext context) {
final settings = watchIt<SettingsModel>();
final darkMode =
settings.darkMode; // Rebuilds even when other settings change
return Container();
}
}✅ Bueno - Se reconstruye solo cuando el valor de darkMode cambia:
// ✅ Good - Rebuilds only when darkMode changes
class RebuildsOnlyDarkModeGood extends WatchingWidget {
@override
Widget build(BuildContext context) {
final darkMode = watchPropertyValue((SettingsModel s) => s.darkMode);
return Container();
}
}Dividir Widgets Grandes
No observes todo en un widget gigante. Divide en widgets más pequeños que observen solo lo que necesitan. Esto asegura que solo los widgets más pequeños se reconstruyan cuando sus datos cambien.
❌️ Malo - Un widget observa todo:
// ❌ Bad - One widget watches everything
class OneWidgetWatchesEverythingBad extends WatchingWidget {
@override
Widget build(BuildContext context) {
final user = watchValue((UserManager m) => m.currentUser);
final todos = watchValue((TodoManager m) => m.todos);
final settings = watchPropertyValue((SettingsModel m) => m.darkMode);
return Column(
children: [
// When ANYTHING changes, ENTIRE dashboard rebuilds
Text(user?.name ?? ''),
Text('${todos.length} todos'),
Text('Dark mode: $settings'),
],
);
}
}✅ Bueno - Cada widget observa sus propios datos:
class SplitWidgetsGoodDashboard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
UserHeader(), // Only rebuilds when user changes
TodoListHeader(), // Only rebuilds when todos change
SettingsPanelHeader(), // Only rebuilds when settings change
],
);
}
}class UserHeader extends WatchingWidget {
@override
Widget build(BuildContext context) {
final user = watchValue((UserManager m) => m.currentUser);
return Text(user?.name ?? 'Not logged in');
}
}class TodoListHeader extends WatchingWidget {
@override
Widget build(BuildContext context) {
final todos = watchValue((TodoManager m) => m.todos);
return Text('${todos.length} todos');
}
}class SettingsPanelHeader extends WatchingWidget {
@override
Widget build(BuildContext context) {
final darkMode = watchPropertyValue((SettingsModel m) => m.darkMode);
return Text('Dark mode: $darkMode');
}
}Constructores Const
Usa const con tus watching widgets
Los constructores const funcionan con todos los tipos de widget de watch_it: WatchingWidget, WatchingStatefulWidget, y widgets usando WatchItMixin. Flutter puede optimizar widgets const para mejor rendimiento de reconstrucción.
Testing
Testea Lógica de Negocio Por Separado
Mantén tu lógica de negocio (managers, servicios) separada de los widgets y testéala independientemente:
Unit test del manager:
test('TodoManager filters completed todos', () {
final manager = TodoManager();
manager.addTodo('Task 1');
manager.addTodo('Task 2');
manager.todos[0].complete();
expect(manager.completedTodos.length, 1);
expect(manager.activeTodos.length, 1);
});Sin dependencias de Flutter = tests rápidos.
Testea Widgets con Dependencias Mockeadas
Para widget tests, usa scopes para aislar dependencias. Crítico: Debes registrar cualquier objeto que tu widget observe ANTES de llamar a pumpWidget:
testWidgets('TodoListWidget displays todos', (tester) async {
// Usa un scope para aislamiento de tests
await GetIt.I.pushNewScope();
// Registra mocks ANTES de pumpWidget
final mockManager = MockTodoManager();
when(mockManager.todos).thenReturn([
Todo('Task 1'),
Todo('Task 2'),
]);
GetIt.I.registerSingleton<TodoManager>(mockManager);
// Ahora crea el widget
await tester.pumpWidget(MaterialApp(home: TodoListWidget()));
expect(find.text('Task 1'), findsOneWidget);
expect(find.text('Task 2'), findsOneWidget);
// Limpiar scope
await GetIt.I.popScope();
});Puntos clave:
- Registra objetos observados ANTES de
pumpWidget- el widget intentará acceder a ellos durante la primera construcción - Usa
pushNewScope()para aislamiento de tests en lugar dereset() - El widget accede a mocks vía
get_itautomáticamente - Los widgets auto-contenidos son más fáciles de testear - no se necesitan parámetros del constructor
Para estrategias comprehensivas de testing con get_it, ver la Testing Guide.
Organización del Código
Estructura del Widget
// Widget structure
class TodoListStructure extends WatchingWidget {
@override
Widget build(BuildContext context) {
// 1. One-time initialization
callOnce((_) {
di<TodoManagerStructure>().fetchCommand.run();
});
// 2. Register handlers
registerHandler(
select: (TodoManagerStructure m) => m.createCommand,
handler: _onTodoCreated,
);
// 3. Watch reactive state
final todos = watchValue((TodoManagerStructure m) => m.todos);
final isLoading = watchValue((TodoManagerStructure m) => m.isLoading);
// 4. Build UI
if (isLoading) return CircularProgressIndicator();
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) => Text(todos[index].title),
);
}
void _onTodoCreated(
BuildContext context, Todo? value, void Function() cancel) {
if (value != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Todo created!')),
);
}
}
}Anti-Patrones
❌️ No Accedas a get_it en Constructores
❌️ Malo - Acceder en el constructor:
// ❌ BAD - Accessing get_it in constructor
class DontAccessGetItConstructorsBad extends WatchingWidget {
DontAccessGetItConstructorsBad() {
// DON'T DO THIS - constructor runs before widget is attached to tree
di<Manager>().loadMoreCommand.run(); // Will fail or cause issues
}
@override
Widget build(BuildContext context) {
return Container();
}
}✅ Bueno - Usa callOnce:
// ✅ GOOD - Use callOnce
class DontAccessGetItConstructorsGood extends WatchingWidget {
@override
Widget build(BuildContext context) {
callOnce((_) {
di<Manager>().loadMoreCommand.run(); // Do this instead
});
return Container();
}
}¿Por qué? Los constructores se ejecutan antes de que el widget esté adjunto al árbol, y se llamarán nuevamente cada vez que el widget se recree. Usa callOnce() para asegurar que la inicialización suceda solo una vez cuando el widget realmente se construya.
❌️ No Violes Reglas de Orden de Watch
El Orden de Watch es Crítico
Todas las llamadas watch*, callOnce, createOnce, y registerHandler deben estar en el mismo orden en cada construcción. Esta es una restricción fundamental del diseño de watch_it.
Ver Watch Ordering Rules para detalles completos y excepciones seguras.
❌️ No Hagas Await de Commands
// ❌ Don't await commands - BAD
class DontAwaitExecuteBadAnti extends WatchingWidget {
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () async {
await di<TodoManager>().createTodoCommand.runAsync(
CreateTodoParams(title: 'New', description: '')); // Blocks UI!
},
child: Text('Submit'),
);
}
}// ✅ Don't await commands - GOOD
class DontAwaitExecuteGoodAnti extends WatchingWidget {
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => di<TodoManager>().createTodoCommand.run(CreateTodoParams(
title: 'New', description: '')), // Returns immediately
child: Text('Submit'),
);
}
}❌️ No Pongas Llamadas Watch en Callbacks
Ver Watch Ordering Rules - las llamadas watch deben estar en build(), no en callbacks.
Debugging
Habilita tracing con enableTracing() o WatchItSubTreeTraceControl para entender el comportamiento de reconstrucción. Para técnicas de debugging detalladas y resolución de problemas comunes, ver Debugging & Troubleshooting.
Ver También
- Watch Ordering Rules - Restricciones CRÍTICAS
- Debugging & Troubleshooting - Problemas comunes
- Observing Commands - Integración con command_it
- Testing - Testing con
get_it