Reglas de Orden de Watch
La Regla de Oro
Todas las llamadas a funciones watch deben ocurrir en el MISMO ORDEN en cada construcción.
Esta es la regla más importante en watch_it. Violarla causará errores o comportamiento inesperado.
¿Por Qué Importa el Orden?
watch_it usa un mecanismo de estado global similar a React Hooks. Cada llamada watch se asigna un índice basado en su posición en la secuencia de construcción. Cuando el widget se reconstruye, watch_it espera encontrar los mismos watches en el mismo orden.
Qué sucede si el orden cambia:
- ❌️ Errores en tiempo de ejecución
- ❌️ Datos incorrectos mostrados
- ❌️ Reconstrucciones inesperadas
- ❌️ Memory leaks
Patrón Correcto
✅ Todas las llamadas watch ocurren en el mismo orden cada vez:
// CORRECT: All watch calls in the SAME ORDER every build
// This is the golden rule of watch_it!
class GoodWatchOrderingWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
// ALWAYS call watch functions in the same order
// Even if you don't use the value, the order matters!
// Order: 1. todos
final todos = watchValue((TodoManager m) => m.todos);
// Order: 2. isLoading
final isLoading =
watchValue((TodoManager m) => m.fetchTodosCommand.isRunning);
// Order: 3. counter (local state)
final counter = createOnce(() => SimpleCounter());
final count = watch(counter).value;
callOnce((_) {
di<TodoManager>().fetchTodosCommand.run();
});
return Scaffold(
appBar: AppBar(title: const Text('✓ Good Watch Ordering')),
body: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
color: Colors.green.shade100,
child: const Text(
'✓ All watch calls in same order every build',
style:
TextStyle(color: Colors.green, fontWeight: FontWeight.bold),
),
),
ListTile(
title: const Text('Todos count'),
trailing: Text('${todos.length}'),
),
ListTile(
title: const Text('Loading'),
trailing: Text(isLoading.toString()),
),
ListTile(
title: const Text('Counter'),
trailing: Text('$count'),
),
const Spacer(),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
ElevatedButton(
onPressed: () => di<TodoManager>().fetchTodosCommand.run(),
child: const Text('Refresh Todos'),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: counter.increment,
child: const Text('Increment Counter'),
),
],
),
),
],
),
);
}
}Por qué esto es correcto:
- Línea 17: Siempre observa
todos - Línea 20: Siempre observa
isLoading - Línea 23-24: Siempre crea y observa
counter - El orden nunca cambia, incluso cuando los datos se actualizan
Violaciones Comunes
❌️ Llamadas Watch Condicionales
El error más común es poner llamadas watch dentro de declaraciones condicionales:
// ❌ WRONG: Conditional watch calls - ORDER CHANGES between builds
// This will cause errors or unexpected behavior!
class BadWatchOrderingWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final showDetails = createOnce(() => ValueNotifier(false));
final show = watch(showDetails).value;
// ❌ WRONG: Conditional watch - order changes!
// When show changes, the order of watch calls changes
if (show) {
// This watch call only happens sometimes
final todos = watchValue((TodoManager m) => m.todos);
return Scaffold(
appBar: AppBar(title: const Text('❌ Bad Ordering - Details View')),
body: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
return ListTile(title: Text(todos[index].title));
},
),
);
}
// Different watch calls in different branches
final isLoading =
watchValue((TodoManager m) => m.fetchTodosCommand.isRunning);
return Scaffold(
appBar: AppBar(title: const Text('❌ Bad Ordering - Simple View')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.all(16),
color: Colors.red.shade100,
child: const Text(
'❌ BAD: Watch call order changes based on conditions!\n'
'This violates the golden rule.',
style: TextStyle(color: Colors.red),
textAlign: TextAlign.center,
),
),
if (isLoading) const CircularProgressIndicator(),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => showDetails.value = true,
child: const Text('Toggle View (will break)'),
),
],
),
),
);
}
}Por qué esto falla:
- Cuando
showes false: observa [showDetails, isLoading] - Cuando
showes true: observa [showDetails, todos] - ¡Orden cambia = error!
❌️ Watch Dentro de Loops
// ❌ WRONG - order changes based on list length
class WatchInsideLoopsWrong extends WatchingWidget {
@override
Widget build(BuildContext context) {
final items = <Item>[Item('1', 'Apple'), Item('2', 'Banana')];
final widgets = <Widget>[];
for (final item in items) {
// DON'T DO THIS - number of watch calls changes with list length
final data = watchValue((Manager m) => m.data);
widgets.add(Text(data));
}
return Column(children: widgets);
}
}❌️ Watch en Callbacks
// ❌ WRONG - watch in button callback
class WatchInCallbacksWrong extends WatchingWidget {
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
// DON'T DO THIS - watch calls must be in build(), not callbacks
final data = watchValue((Manager m) => m.data); // Error!
print(data);
},
child: Text('Press'),
);
}
}Excepciones Seguras a la Regla
Entender Cuándo los Condicionales Son Seguros
La regla de ordenamiento solo importa cuando los watches pueden o no ser llamados en la MISMA ruta de ejecución.
- Watches condicionales al final - seguros porque no hay watches después
- Returns tempranos - siempre seguros porque crean rutas de ejecución separadas
✅ Watches Condicionales al FINAL
Los watches condicionales son perfectamente seguros cuando son los últimos watches en tu build:
class MyWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
// Estos watches siempre se ejecutan en el mismo orden
final todos = watchValue((TodoManager m) => m.todos);
final isLoading = watchValue((TodoManager m) => m.isLoading);
// ✅ Watch condicional al FINAL - ¡perfectamente seguro!
if (showDetails) {
final details = watchValue((TodoManager m) => m.selectedDetails);
return DetailView(details);
}
return ListView(/* ... */);
}
}Por qué esto es seguro:
- Los primeros dos watches siempre se ejecutan en el mismo orden
- El watch condicional es el ¡ÚLTIMO - no hay watches subsiguientes que interrumpir
- En reconstrucción: se mantiene el mismo orden
✅ Returns Tempranos Siempre Son Seguros
Los returns tempranos no afectan el orden de watch porque los watches después de ellos nunca se llaman:
class MyWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final isLoading = watchValue((DataManager m) => m.isLoading);
// ✅ Return temprano - ¡completamente seguro!
if (isLoading) {
return CircularProgressIndicator();
}
// Este watch solo se ejecuta cuando NO está cargando
final data = watchValue((DataManager m) => m.data);
if (data.isEmpty) {
return Text('No data');
}
return ListView(/* ... */);
}
}Por qué esto es seguro:
- Los watches después de returns tempranos simplemente nunca se ejecutan
- No participan en el mecanismo de ordenamiento
- No es posible interrupción del orden
Principio clave: El peligro son los watches que pueden o no ser llamados en la MISMA ruta de construcción SEGUIDOS por otros watches. Los returns tempranos crean rutas de ejecución separadas, por lo que los watches después de ellos no son parte del ordenamiento para esa ruta.
Patrones Condicionales Seguros
✅ Llama a TODOS los watches primero, LUEGO usa condiciones:
// ✓ SAFE: How to handle conditional logic while maintaining watch order
class SafeConditionalWatchWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final showDetails = createOnce(() => ValueNotifier(false));
final show = watch(showDetails).value;
// ✓ CORRECT: ALWAYS call all watch functions regardless of conditions
// The ORDER stays the same every build!
final todos = watchValue((TodoManager m) => m.todos);
final isLoading =
watchValue((TodoManager m) => m.fetchTodosCommand.isRunning);
callOnce((_) {
di<TodoManager>().fetchTodosCommand.run();
});
// Now use conditional logic AFTER all watch calls
return Scaffold(
appBar: AppBar(
title: Text(show ? '✓ Details View' : '✓ Simple View'),
),
body: show
? Column(
children: [
Container(
padding: const EdgeInsets.all(16),
color: Colors.green.shade100,
child: const Text(
'✓ SAFE: All watch calls happen in same order,\n'
'only UI changes conditionally',
style: TextStyle(color: Colors.green),
textAlign: TextAlign.center,
),
),
Expanded(
child: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(todos[index].title),
subtitle: Text(todos[index].description),
);
},
),
),
],
)
: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.all(16),
color: Colors.green.shade100,
child: const Text(
'✓ SAFE: Same watch calls every build',
style: TextStyle(color: Colors.green),
),
),
Text('Total Todos: ${todos.length}'),
const SizedBox(height: 8),
if (isLoading) const CircularProgressIndicator(),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => showDetails.value = !show,
child: Icon(show ? Icons.visibility_off : Icons.visibility),
),
);
}
}Patrón:
- Llama a todas las funciones watch en la parte superior de
build() - LUEGO usa lógica condicional con los valores
- El orden se mantiene consistente
Ejemplos de Patrones Seguros
// ✓ CORRECT - All watches before conditionals
class SafePatternConditional extends WatchingWidget {
@override
Widget build(BuildContext context) {
// Watch everything first
final data = watchValue((Manager m) => m.data);
final isLoading = watchValue((Manager m) => m.isLoading);
final error = watchValue((Manager m) => m.error);
// Then use conditionals
if (error != null) {
return ErrorWidget(error);
}
if (isLoading) {
return CircularProgressIndicator();
}
return Text(data);
}
}// ✓ CORRECT - Watch list, then iterate over values
class SafePatternListIteration extends WatchingWidget {
@override
Widget build(BuildContext context) {
// Watch the list once
final items = watchValue((Manager m) => m.items);
// Iterate over values (not watch calls)
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index]; // No watch here!
return ListTile(title: Text(item.name));
},
);
}
}Resolución de Problemas
Error: "Watch ordering violation detected!"
Mensaje de error completo:
Watch ordering violation detected!
You have conditional watch calls (inside if/switch statements) that are
causing watch_it to retrieve the wrong objects on rebuild.
Fix: Move ALL conditional watch calls to the END of your build method.
Only the LAST watch call can be conditional.Qué sucedió:
- Tienes un watch dentro de una declaración
if - Este watch está seguido por otros watches
- En la reconstrucción, la condición cambió, causando que watch_it intente recuperar el tipo incorrecto en esa posición
- Se lanzó un TypeError al intentar hacer cast de la entrada watch
Solución:
- Mueve los watches condicionales al FINAL de tu método build, O
- Haz que todos los watches sean incondicionales y usa los valores condicionalmente en su lugar
Tip: Llama a enableTracing() en tu método build para ver las ubicaciones exactas de fuente de las declaraciones watch en conflicto.
Lista de Verificación de Mejores Prácticas
✅ HACER:
- Llamar a todos los watches en la parte superior de
build()cuando sea posible - Usar llamadas watch incondicionales para watches que necesitan ejecutarse en todas las rutas
- Almacenar valores en variables, usar variables condicionalmente
- Observar la lista completa, iterar sobre valores
- Usar watches condicionales al final (después de todos los otros watches)
- Usar returns tempranos libremente - siempre son seguros
❌️ NO HACER:
- Poner watches en declaraciones
ifcuando estén seguidos por otros watches - Poner watches en loops
- Poner watches en callbacks
Avanzado: Por Qué Esto Sucede
watch_it usa una variable global _watchItState que rastrea:
- El widget actual siendo construido
- Índice de la llamada watch actual
- Lista de suscripciones watch previas
Cuando llamas a watch():
watch_itincrementa el índice- Verifica si la suscripción en ese índice existe
- Si sí, la reutiliza
- Si no, crea una nueva suscripción
Si el orden cambia:
- El índice 0 espera la suscripción A, obtiene la suscripción B
- Las suscripciones se filtran o se mezclan
- Todo se rompe
Esto es similar a las reglas de React Hooks por la misma razón.
Ver También
- Getting Started - Uso básico de
watch_it - Watch Functions - Todas las funciones watch
- Best Practices - Patrones generales
- Debugging & Troubleshooting - Problemas comunes