El autor está disponible para consultoría y trabajo por contratoAgenda una llamada →
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. Creado por Thomas Burkhart.