Skip to content

Mejores Prácticas

Patrones listos para producción, anti-patrones, y guías para usar command_it efectivamente.

Cuándo Usar Commands

✅ Usa Commands Para

Operaciones async con feedback de UI:

dart
late final loadDataCommand = Command.createAsyncNoParam<List<Data>>(
  () => api.fetchData(),
  initialValue: [],
);
// Automatic isRunning, error handling, UI integration

Operaciones que pueden fallar:

dart
late final saveCommand = Command.createAsyncNoResult<Data>(
  (data) => api.save(data),
  errorFilter: PredicatesErrorFilter([
    (e, _) => errorFilter<ApiException>(e, ErrorReaction.localHandler),
  ]),
);

Acciones disparadas por usuario:

dart
late final submitCommand = Command.createAsyncNoResult<FormData>(
  (data) => api.submit(data),
  restriction: formValid.map((valid) => !valid),
);

Operaciones que necesitan tracking de estado:

  • Estados de carga de botones
  • Pull-to-refresh
  • Envío de formularios
  • Peticiones de red
  • I/O de archivos

✅ Usa Commands Sync para Input con Operadores

Cuando necesitas aplicar operadores (debounce, map, where) a input de usuario antes de disparar otras operaciones, ver Encadenamiento de Commands para patrones usando pipeToCommand y operadores de listen_it.

❌️️ No Uses Commands Para

Simples getters/setters (sin operadores o encadenamiento):

dart
// ignore_for_file: unused_field, unused_local_variable
String _name = '';

// ❌ Overkill
late final getNameCommand = Command.createSyncNoParam<String>(
  () => _name,
  initialValue: '',
);

// ✅ Just use a ValueNotifier
final name = ValueNotifier<String>('');

Cómputos puros sin efectos secundarios:

dart
// ❌ Unnecessary
late final calculateCommand = Command.createSync<int, int>(
  (n) => n * 2,
  initialValue: 0,
);

// ✅ Just use a function
int calculate(int n) => n * 2;

Cambios de estado inmediatos:

dart
bool _enabled = false;

// ❌ Overcomplicated
late final toggleCommand = Command.createSyncNoParam<bool>(
  () => !_enabled,
  initialValue: false,
);

// ✅ Use ValueNotifier directly
final enabled = ValueNotifier<bool>(false);
void toggle() => enabled.value = !enabled.value;

Patrones de Organización

Patrón 1: Commands en Managers

dart
class TodoManager {
  final ApiClient api;
  final Database db;

  TodoManager(this.api, this.db);

  // Group related commands
  late final loadTodosCommand = Command.createAsyncNoParam<List<Todo>>(
    () => api.fetchTodos(),
    initialValue: [],
  );

  late final addTodoCommand = Command.createAsyncNoResult<Todo>(
    (todo) async {
      await api.saveTodo(todo);
      loadTodosCommand.run(); // Reload after add
    },
  );

  late final deleteTodoCommand = Command.createAsyncNoResult<String>(
    (id) async {
      await api.deleteTodo(id);
      loadTodosCommand.run(); // Reload after delete
    },
    restriction: loadTodosCommand.isRunningSync, // Can't delete while loading
  );

  void dispose() {
    loadTodosCommand.dispose();
    addTodoCommand.dispose();
    deleteTodoCommand.dispose();
  }
}

Beneficios:

  • Lógica de negocio centralizada
  • Fácil testing
  • Reutilizable entre widgets
  • Propiedad clara

Patrón 2: Organización Basada en Features

dart
// features/authentication/auth_manager.dart
class AuthManager {
  final ApiClient _api = ApiClient();

  // Expose login state as a simple ValueNotifier for restrictions
  final isLoggedIn = ValueNotifier<bool>(false);

  late final loginCommand = Command.createAsync<LoginCredentials, User>(
    (data) async {
      final user = await _api.login(data.username, data.password);
      isLoggedIn.value = true;
      return user;
    },
    initialValue: User.empty(),
  );

  late final logoutCommand = Command.createAsyncNoParamNoResult(
    () async {
      await _api.logout();
      isLoggedIn.value = false;
    },
  );
}

// features/profile/profile_manager.dart
class ProfileManager {
  final AuthManager auth;
  final ApiClient _api = ApiClient();

  ProfileManager(this.auth);

  late final loadProfileCommand = Command.createAsyncNoParam<Profile>(
    () => _api.loadProfile(),
    initialValue: Profile.empty(),
    // restriction: true = disabled, so negate isLoggedIn
    restriction: auth.isLoggedIn.map((logged) => !logged),
  );
}

Patrón 3: Commands en Data Proxies

Los Commands también pueden vivir en objetos de datos que gestionan sus propias operaciones async. Esto es útil cuando cada item de datos necesita estado de carga independiente:

dart
/// Data proxy that owns commands for lazy loading.
/// Each instance manages its own async operations.
class PodcastProxy {
  PodcastProxy({required this.feedUrl, required PodcastService podcastService})
      : _podcastService = podcastService {
    // Each proxy owns its fetch command
    fetchEpisodesCommand = Command.createAsyncNoParam<List<Episode>>(
      () async {
        if (_episodes != null) return _episodes!;
        _episodes = await _podcastService.fetchEpisodes(feedUrl);
        return _episodes!;
      },
      initialValue: [],
    );
  }

  final String feedUrl;
  final PodcastService _podcastService;
  List<Episode>? _episodes;

  late final Command<void, List<Episode>> fetchEpisodesCommand;

  /// Fetches episodes if not cached, then starts playback.
  late final playEpisodesCommand = Command.createAsyncNoResult<int>((
    startIndex,
  ) async {
    if (_episodes == null) {
      await fetchEpisodesCommand.runAsync();
    }
    if (_episodes != null && _episodes!.isNotEmpty) {
      // Start playback at index...
    }
  });

  List<Episode> get episodes => _episodes ?? [];
}

/// Manager creates and caches proxies
class PodcastManager {
  PodcastManager(this._podcastService);

  final PodcastService _podcastService;
  final _proxyCache = <String, PodcastProxy>{};

  PodcastProxy getOrCreateProxy(String feedUrl) {
    return _proxyCache.putIfAbsent(
      feedUrl,
      () => PodcastProxy(feedUrl: feedUrl, podcastService: _podcastService),
    );
  }
}

Beneficios:

  • Cada item tiene estado de carga/error independiente
  • La lógica de caching vive con los datos
  • La UI puede observar estado de item individual
  • El manager se mantiene simple (solo crea/cachea proxies)

Cuándo Usar runAsync()

Como se explica en Fundamentos de Command, el patrón central de command es dispara-y-olvida: llama run() y deja que tu UI observe cambios de estado reactivamente. Sin embargo, hay casos legítimos donde usar runAsync() es apropiado y más expresivo que las alternativas.

✅ Usa runAsync() Para Flujos de Trabajo Secuenciales

Cuando los commands son parte de un flujo de trabajo async más grande mezclado con otras operaciones async:

dart
class PaymentManager {
  late final validatePaymentCommand = Command.createAsync<PaymentInfo, bool>(
    (info) => api.validatePayment(info),
    initialValue: false,
  );

  late final processPaymentCommand = Command.createAsync<PaymentInfo, Receipt>(
    (info) => api.processPayment(info),
    initialValue: Receipt.empty(),
  );

  // Complex async workflow
  Future<Receipt> completeCheckout(Cart cart, PaymentInfo payment) async {
    // Step 1: Validate inventory (not a command, just async call)
    final available = await api.checkInventory(cart.items);
    if (!available) throw InsufficientInventoryException();

    // Step 2: Validate payment (command)
    final isValid = await validatePaymentCommand.runAsync(payment);
    if (!isValid) throw InvalidPaymentException();

    // Step 3: Process payment (command)
    final receipt = await processPaymentCommand.runAsync(payment);

    // Step 4: Update inventory (not a command)
    await api.updateInventory(cart.items);

    return receipt;
  }
}

¿Por qué runAsync() aquí? El command es parte de una función async más grande que mezcla ejecución de commands con llamadas async regulares. Usar runAsync() mantiene el código lineal y legible.

✅ Usa runAsync() Para APIs que Requieren Futures

Cuando interactúas con APIs que requieren un Future:

dart
class RefreshExample extends StatelessWidget {
  final Command<void, List<Data>> updateCommand;

  const RefreshExample({super.key, required this.updateCommand});

  @override
  Widget build(BuildContext context) {
    // RefreshIndicator requires Future<void>
    return RefreshIndicator(
      onRefresh: () => updateCommand.runAsync(),
      child: ListView(),
    );
  }
}

❌️ No Uses runAsync() para Actualizaciones de UI Simples

dart
class BadExample extends StatelessWidget {
  final Command<void, List<Data>> loadDataCommand;

  const BadExample({super.key, required this.loadDataCommand});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // ❌ BAD: Blocking UI thread waiting for result
        ElevatedButton(
          onPressed: () async {
            final result = await loadDataCommand.runAsync();
            // Do nothing with result - just waiting
          },
          child: Text('Load Bad'),
        ),

        // ✅ GOOD: Fire and forget, let UI observe
        ElevatedButton(
          onPressed: loadDataCommand.run,
          child: Text('Load Good'),
        ),
      ],
    );
  }
}

Resumen

Usa runAsync() cuando:

  • ✅ Los commands son parte de un flujo de trabajo async más grande
  • ✅ Una API requiere que se retorne un Future
  • ✅ El flujo secuencial es más claro con await que con .listen()

No uses runAsync() cuando:

  • ❌️ Disparando commands desde interacciones de UI (usa run())
  • ❌️ Solo quieres observar resultados (usa watchValue() o ValueListenableBuilder)
  • ❌️ El async/await no agrega valor sobre dispara-y-olvida

Mejores Prácticas de Rendimiento

Debounce de Input de Texto

dart
class SearchManagerDebounce {
  late final searchTextCommand = Command.createSync<String, String>(
    (text) => text,
    initialValue: '',
  );

  late final searchCommand = Command.createAsync<String, List<Result>>(
    (query) => api.search(query),
    initialValue: [],
  );

  SearchManagerDebounce() {
    // Debounce text changes
    searchTextCommand.debounce(Duration(milliseconds: 300)).listen((text, _) {
      if (text.isNotEmpty) {
        searchCommand(text);
      }
    });
  }
}

Dispose de Commands Apropiadamente

dart
class DataManager {
  late final command = Command.createAsyncNoParam<Data>(
    () => api.fetchData().then((list) => list.first),
    initialValue: Data.empty(),
  );

  // ✅ Always dispose in cleanup
  void dispose() {
    command.dispose();
  }
}

// With StatefulWidget
class MyWidget extends StatefulWidget {
  const MyWidget({super.key});

  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  final manager = DataManager();

  @override
  void dispose() {
    manager.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Container();
}

// With get_it scopes
void registerWithGetIt() {
  getIt.registerLazySingleton<DataManager>(
    () => DataManager(),
    dispose: (manager) => manager.dispose(),
  );
}

Evitar Rebuilds Innecesarios

dart
class RebuildExample extends StatelessWidget {
  final Command<void, String> command;

  const RebuildExample({super.key, required this.command});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // ❌ Rebuilds on every command property change
        ValueListenableBuilder(
          valueListenable: command.results,
          builder: (context, result, _) => Text(result.data?.toString() ?? ''),
        ),

        // ✅ Only rebuilds when value changes
        ValueListenableBuilder(
          valueListenable: command,
          builder: (context, data, _) => Text(data.toString()),
        ),
      ],
    );
  }
}

Mejores Prácticas de Restricciones

Usa isRunningSync para Dependencias de Commands

dart
late final loadCommand = Command.createAsyncNoParam<List<Data>>(
  () => api.fetchData(),
  initialValue: [],
);

// ✅ Correct: Synchronous restriction
late final saveCommandGood = Command.createAsyncNoResult<Data>(
  (data) => api.save(data),
  restriction: loadCommand.isRunningSync, // Prevents race conditions
);

// ❌ Wrong: Async update can cause races
late final saveCommandBad = Command.createAsyncNoResult<Data>(
  (data) => api.save(data),
  restriction: loadCommand.isRunning, // Race condition possible!
);

La Lógica de Restricción Está Invertida

dart
final isLoggedIn = ValueNotifier<bool>(false);

// ❌ Common mistake: Restriction logic backwards
late final commandBad = Command.createAsyncNoParam<Data>(
  () => api.fetchData().then((list) => list.first),
  initialValue: Data.empty(),
  restriction: isLoggedIn, // WRONG: Disabled when logged in!
);

// ✅ Correct: Negate the condition
late final commandGood = Command.createAsyncNoParam<Data>(
  () => api.fetchData().then((list) => list.first),
  initialValue: Data.empty(),
  restriction:
      isLoggedIn.map((logged) => !logged), // Disabled when NOT logged in
);

Anti-Patrones Comunes

❌️️ No Escuchar Errores

dart
// ❌ BAD: Errors go nowhere
late final commandNoListener = Command.createAsyncNoParam<Data>(
  () => api.fetchData().then((list) => list.first),
  initialValue: Data.empty(),
  errorFilter: const LocalErrorFilter(),
);
// No error listener! Assertions in debug mode

// ✅ GOOD: Always listen to errors when using localHandler
void setupGoodErrorListener() {
  commandNoListener.errors.listen((error, _) {
    if (error != null) showError(error.error);
  });
}

void showError(Object error) {
  debugPrint('Error: $error');
}

❌️️ Try/Catch Dentro de Commands

No uses try/catch dentro de funciones de commands - derrota el sistema de manejo de errores de command_it:

dart
// ❌ BAD: try/catch inside command function
class DataManagerBad {
  late final loadCommand = Command.createAsyncNoParam<Data>(
    () async {
      try {
        final list = await api.fetchData();
        return list.first;
      } catch (e) {
        // Manual error handling defeats command_it's error system
        debugPrint('Error: $e');
        rethrow;
      }
    },
    initialValue: Data.empty(),
  );
}

// ✅ GOOD: Let command handle errors, use ..errors.listen()
class DataManagerGood {
  late final loadCommand = Command.createAsyncNoParam<Data>(
    () async {
      final list = await api.fetchData();
      return list.first;
    },
    initialValue: Data.empty(),
  )..errors.listen((error, _) {
      if (error != null) {
        debugPrint('Error: ${error.error}');
      }
    });
}

Ver También

Publicado bajo la Licencia MIT.