Skip to content

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:

dart
// 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:

dart
// ❌ 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 show es false: observa [showDetails, isLoading]
  • Cuando show es true: observa [showDetails, todos]
  • ¡Orden cambia = error!

❌️ Watch Dentro de Loops

dart
// ❌ 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

dart
// ❌ 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:

dart
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:

dart
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:

dart
// ✓ 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:

  1. Llama a todas las funciones watch en la parte superior de build()
  2. LUEGO usa lógica condicional con los valores
  3. El orden se mantiene consistente

Ejemplos de Patrones Seguros

dart
// ✓ 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);
  }
}
dart
// ✓ 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:

  1. Mueve los watches condicionales al FINAL de tu método build, O
  2. 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 if cuando 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():

  1. watch_it incrementa el índice
  2. Verifica si la suscripción en ese índice existe
  3. Si sí, la reutiliza
  4. 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

Publicado bajo la Licencia MIT.