Skip to content

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:

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

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

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

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

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

  • createOnce crea el notifier una vez y lo dispone automáticamente
  • watch se 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:

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

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

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

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

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

dart
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
      ],
    );
  }
}
dart
class UserHeader extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    final user = watchValue((UserManager m) => m.currentUser);
    return Text(user?.name ?? 'Not logged in');
  }
}
dart
class TodoListHeader extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    final todos = watchValue((TodoManager m) => m.todos);
    return Text('${todos.length} todos');
  }
}
dart
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:

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

dart
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 de reset()
  • El widget accede a mocks vía get_it automá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

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

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

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

dart
// ❌ 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'),
    );
  }
}
dart
// ✅ 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

Publicado bajo la Licencia MIT.