Skip to content

Using Commands without watch_it

All the examples in Getting Started use watch_it, which is our recommended approach for production apps. However, commands work perfectly with plain ValueListenableBuilder if you prefer not to use watch_it or get_it.

When to Use ValueListenableBuilder

Consider using ValueListenableBuilder instead of watch_it when:

  • You're prototyping or learning and want to minimize dependencies
  • You have a simple widget that doesn't need dependency injection
  • You prefer explicit builder patterns over implicit observation
  • You're working on a project that doesn't use get_it

For production apps, we still recommend watch_it for cleaner, more maintainable code.

Simple Counter Example

Here's the basic counter example using ValueListenableBuilder:

dart
class CounterModel {
  int _count = 0;

  // Command wraps a function and acts as a ValueListenable
  late final incrementCommand = Command.createSyncNoParam<String>(
    () {
      _count++;
      return _count.toString();
    },
    initialValue: '0',
  );
}

class CounterWidget extends StatelessWidget {
  CounterWidget({super.key});

  final model = CounterModel();

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('You have pushed the button this many times:'),
        // Command is a ValueListenable - use ValueListenableBuilder
        ValueListenableBuilder<String>(
          valueListenable: model.incrementCommand,
          builder: (context, value, _) => Text(
            value,
            style: Theme.of(context).textTheme.headlineMedium,
          ),
        ),
        SizedBox(height: 16),
        // Command has a .run method - use it as tearoff for onPressed
        ElevatedButton(
          onPressed: model.incrementCommand.run,
          child: Text('Increment'),
        ),
      ],
    );
  }
}

Key points:

  • Use ValueListenableBuilder to observe the command
  • Use StatelessWidget instead of WatchingWidget
  • No need for get_it registration - service can be created directly in the widget
  • Command is still a ValueListenable, just observed differently

Async Example with Loading States

Here's the weather example showing async commands with loading indicators:

dart
class WeatherWidget extends StatelessWidget {
  WeatherWidget({super.key});

  final manager = WeatherManager();

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<bool>(
      valueListenable: manager.loadWeatherCommand.isRunning,
      builder: (context, isRunning, _) {
        // Show loading indicator while command runs
        if (isRunning) {
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                CircularProgressIndicator(),
                SizedBox(height: 16),
                Text('Loading weather data...'),
              ],
            ),
          );
        }

        // Show data when ready
        return ValueListenableBuilder<List<WeatherEntry>>(
          valueListenable: manager.loadWeatherCommand,
          builder: (context, weather, _) {
            if (weather.isEmpty) {
              return Center(
                child: ElevatedButton(
                  onPressed: () => manager.loadWeatherCommand('London'),
                  child: Text('Load Weather'),
                ),
              );
            }

            return ListView.builder(
              itemCount: weather.length,
              itemBuilder: (context, index) {
                final entry = weather[index];
                return ListTile(
                  title: Text(entry.city),
                  subtitle: Text(entry.condition),
                  trailing: Text('${entry.temperature}°F'),
                );
              },
            );
          },
        );
      },
    );
  }
}

Key points:

  • Watch isRunning with a separate ValueListenableBuilder for loading state
  • Nested builders required - one for loading state, one for data
  • More verbose than watch_it but works without additional dependencies
  • All command features (async, error handling, restrictions) still work

Comparing the Approaches

Aspectwatch_itValueListenableBuilder
DependenciesRequires get_it + watch_itNo additional dependencies
Widget BaseWatchingWidgetStatelessWidget or StatefulWidget
ObservationwatchValue((Service s) => s.command)ValueListenableBuilder(valueListenable: command, ...)
Multiple PropertiesClean - separate watchValue callsNested builders required
BoilerplateMinimalMore verbose
Recommended ForProduction appsLearning, prototyping

Observing Multiple Properties

When you need to observe both the command result AND its state (like isRunning), the difference becomes more apparent:

dart
class MyWidget extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    final data = watchValue((Service s) => s.command);
    final isLoading = watchValue((Service s) => s.command.isRunning);

    if (isLoading) return CircularProgressIndicator();
    return Text(data);
  }
}

With ValueListenableBuilder

dart
class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<bool>(
      valueListenable: command.isRunning,
      builder: (context, isLoading, _) {
        if (isLoading) return CircularProgressIndicator();

        return ValueListenableBuilder<String>(
          valueListenable: command,
          builder: (context, data, _) {
            return Text(data);
          },
        );
      },
    );
  }
}

Notice the nesting required with ValueListenableBuilder versus the clean, flat structure with watch_it.

Using CommandResult

For the cleanest ValueListenableBuilder experience, use CommandResult to observe all command state in a single builder:

dart
class MyWidget extends StatelessWidget {
  final myCommand = Command.createAsync<void, String>(
    () async => 'Hello',
    initialValue: '',
  );

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<CommandResult<void, String>>(
      valueListenable: myCommand.results,
      builder: (context, result, _) {
        if (result.isRunning) {
          return CircularProgressIndicator();
        }

        if (result.hasError) {
          return Text('Error: ${result.error}');
        }

        return Text(result.data);
      },
    );
  }
}

See Command Results for more details on using CommandResult.

Error Handling

Commands notify errors through the errors property. Here's how to handle them without watch_it:

dart
class DataWidget extends StatefulWidget {
  const DataWidget({super.key});

  @override
  State<DataWidget> createState() => _DataWidgetState();
}

class _DataWidgetState extends State<DataWidget> {
  final manager = DataManager();
  String? errorMessage;

  @override
  void initState() {
    super.initState();

    // Listen to errors
    manager.loadDataCommand.errors.listen((error, _) {
      if (error != null) {
        setState(() {
          errorMessage = error.error.toString();
        });

        // Show error dialog
        showDialog(
          context: context,
          builder: (context) => AlertDialog(
            title: Text('Error'),
            content: Text(error.error.toString()),
            actions: [
              TextButton(
                onPressed: () => Navigator.pop(context),
                child: Text('OK'),
              ),
            ],
          ),
        );
      } else {
        // Error cleared (command started again)
        setState(() {
          errorMessage = null;
        });
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        if (errorMessage != null)
          Padding(
            padding: EdgeInsets.all(8),
            child: Text(
              errorMessage!,
              style: TextStyle(color: Colors.red),
            ),
          ),
        ElevatedButton(
          onPressed: manager.loadDataCommand.run,
          child: Text('Load Data'),
        ),
        SizedBox(height: 8),
        ElevatedButton(
          onPressed: () {
            manager.shouldFail = true;
            manager.loadDataCommand.run();
          },
          child: Text('Load Data (will fail)'),
        ),
      ],
    );
  }
}

Filtering null values:

The errors property is set to null at the start of execution (without notification). To only handle actual errors:

dart
command.errors.where((e) => e != null).listen((error, _) {
  // Only called for actual errors, not null clears
  showErrorDialog(error!.error.toString());
});

When to set up listeners:

  • In initState of a StatefulWidget
  • Using registerHandler from listen_it
  • Prefer these over watching in build() to avoid recreating listeners

For watch_it error handling patterns, see Command Properties - Error Notifications.

Next Steps

Ready to learn more?

For more about watch_it and why we recommend it, see the watch_it documentation.

Released under the MIT License.