Skip to content

Observing Commands with watch_it

WARNING

This content is AI generated and is currently under review.

One of the most powerful combinations in the flutter_it ecosystem is using watch_it to observe command_it commands. This pattern provides reactive, declarative state management for async operations with automatic loading states, error handling, and result updates.

Why watch_it + command_it?

Commands encapsulate async operations and track their execution state (isRunning, value, errors). watch_it allows your widgets to reactively rebuild when these states change, creating a seamless user experience without manual state management.

Benefits:

  • Automatic loading states - No need to manually track isLoading booleans
  • Reactive results - UI updates automatically when command completes
  • Built-in error handling - Commands track errors, watch_it displays them
  • Clean separation - Business logic in commands, UI logic in widgets
  • No boilerplate - No setState, no StreamBuilder, no manual listeners

Watching Command Execution State

The most common pattern is watching isRunning to show loading indicators:

dart
class TodoLoadingWidget extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    // Watch command's isRunning property to show loading state
    // This is the most common pattern for reactive loading indicators
    final isLoading =
        watchValue((TodoManager m) => m.fetchTodosCommand.isRunning);
    final todos = watchValue((TodoManager m) => m.todos);

    // Load data on first build
    callOnce((_) {
      di<TodoManager>().fetchTodosCommand.run();
    });

    return Scaffold(
      appBar: AppBar(title: const Text('Watch Command - Loading State')),
      body: Column(
        children: [
          // Show loading indicator when command is executing
          if (isLoading)
            const LinearProgressIndicator()
          else
            const SizedBox(height: 4),
          Expanded(
            child: isLoading && todos.isEmpty
                ? const Center(child: CircularProgressIndicator())
                : todos.isEmpty
                    ? const Center(child: Text('No todos'))
                    : ListView.builder(
                        itemCount: todos.length,
                        itemBuilder: (context, index) {
                          final todo = todos[index];
                          return ListTile(
                            title: Text(todo.title),
                            subtitle: Text(todo.description),
                          );
                        },
                      ),
          ),
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: ElevatedButton(
              // Disable button while loading
              onPressed: isLoading
                  ? null
                  : () => di<TodoManager>().fetchTodosCommand.run(),
              child: const Text('Refresh'),
            ),
          ),
        ],
      ),
    );
  }
}

Key points:

  • command.isRunning is a ValueListenable<bool>
  • Widget rebuilds automatically when command starts/stops
  • Button disables during execution
  • Progress indicator shows while loading

Watching Command Results

Watch the command's value property to display results:

dart
class WeatherResultWidget extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    // Get the command from the manager
    final manager = di<WeatherManager>();

    // Watch the command to get its result value
    final weather = watch(manager.fetchWeatherCommand).value;
    final isLoading = watch(manager.fetchWeatherCommand.isRunning).value;

    callOnce((_) {
      di<WeatherManager>().fetchWeatherCommand.run();
    });

    return Scaffold(
      appBar: AppBar(title: const Text('Watch Command - Result Value')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            if (isLoading)
              const CircularProgressIndicator()
            else if (weather != null) ...[
              Text(
                weather.location,
                style: Theme.of(context).textTheme.headlineMedium,
              ),
              const SizedBox(height: 16),
              Text(
                '${weather.temperature}°C',
                style: Theme.of(context).textTheme.displayMedium,
              ),
              const SizedBox(height: 8),
              Text(
                weather.condition,
                style: Theme.of(context).textTheme.titleLarge,
              ),
            ] else
              const Text('No weather data'),
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: isLoading
                  ? null
                  : () => di<WeatherManager>().fetchWeatherCommand.run(),
              child: const Text('Refresh Weather'),
            ),
          ],
        ),
      ),
    );
  }
}

Pattern:

dart
// Get the command
void watchValuePattern(BuildContext context) {
  final manager = di<WeatherManager>();

  // Watch its value
  final weather = watch(manager.fetchWeatherCommand).value;
  final isLoading = watch(manager.fetchWeatherCommand.isRunning).value;
}

Watching Command Errors

Display errors by watching the command's errors property:

dart
class CommandErrorWidget extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    final manager = di<TodoManager>();

    // Watch command's errors property to display error messages
    final error = watch(manager.fetchTodosCommand.errors).value;
    final isLoading = watch(manager.fetchTodosCommand.isRunning).value;
    final todos = watchValue((TodoManager m) => m.todos);

    callOnce((_) {
      di<TodoManager>().fetchTodosCommand.run();
    });

    return Scaffold(
      appBar: AppBar(title: const Text('Watch Command - Errors')),
      body: Column(
        children: [
          // Display error banner when command fails
          if (error != null)
            Container(
              color: Colors.red.shade100,
              padding: const EdgeInsets.all(16),
              child: Row(
                children: [
                  const Icon(Icons.error, color: Colors.red),
                  const SizedBox(width: 8),
                  Expanded(
                    child: Text(
                      'Error: ${error.toString()}',
                      style: const TextStyle(color: Colors.red),
                    ),
                  ),
                  IconButton(
                    icon: const Icon(Icons.close),
                    onPressed: () {
                      // Clear error by executing again
                      manager.fetchTodosCommand.run();
                    },
                  ),
                ],
              ),
            ),
          if (isLoading)
            const LinearProgressIndicator()
          else
            const SizedBox(height: 4),
          Expanded(
            child: todos.isEmpty
                ? Center(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        if (error != null) ...[
                          const Icon(Icons.error_outline,
                              size: 64, color: Colors.red),
                          const SizedBox(height: 16),
                          const Text('Failed to load todos'),
                          const SizedBox(height: 16),
                          ElevatedButton(
                            onPressed: () =>
                                manager.fetchTodosCommand.run(),
                            child: const Text('Retry'),
                          ),
                        ] else if (isLoading)
                          const CircularProgressIndicator()
                        else
                          const Text('No todos'),
                      ],
                    ),
                  )
                : ListView.builder(
                    itemCount: todos.length,
                    itemBuilder: (context, index) {
                      final todo = todos[index];
                      return ListTile(
                        title: Text(todo.title),
                        subtitle: Text(todo.description),
                      );
                    },
                  ),
          ),
        ],
      ),
    );
  }
}

Error handling patterns:

  • Show error banner at top of screen
  • Display error message inline
  • Provide retry button
  • Clear errors on retry

Using Handlers for Side Effects

While watch is for rebuilding UI, use registerHandler for side effects like navigation or showing toasts:

Success Handler

dart
class CreateTodoWithHandlerWidget extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    final titleController = createOnce(() => TextEditingController());
    final descController = createOnce(() => TextEditingController());

    // Use registerHandler to handle successful command completion
    // This is perfect for navigation, showing success messages, etc.
    registerHandler(
      select: (TodoManager m) => m.createTodoCommand,
      handler: (context, result, _) {
        if (result != null) {
          // Show success snackbar
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text('Created: ${result.title}'),
              backgroundColor: Colors.green,
            ),
          );
          // Navigate back with result
          Navigator.of(context).pop(result);
        }
      },
    );

    final isCreating =
        watchValue((TodoManager m) => m.createTodoCommand.isRunning);

    return Scaffold(
      appBar: AppBar(title: const Text('Command Handler - Success')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            const Text(
              'This example uses registerHandler to navigate on success',
              style: TextStyle(fontStyle: FontStyle.italic),
            ),
            const SizedBox(height: 24),
            TextField(
              controller: titleController,
              decoration: const InputDecoration(
                labelText: 'Title',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: descController,
              decoration: const InputDecoration(
                labelText: 'Description',
                border: OutlineInputBorder(),
              ),
              maxLines: 3,
            ),
            const SizedBox(height: 24),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: isCreating
                    ? null
                    : () {
                        final params = CreateTodoParams(
                          title: titleController.text,
                          description: descController.text,
                        );
                        di<TodoManager>().createTodoCommand.run(params);
                      },
                child: isCreating
                    ? const SizedBox(
                        height: 20,
                        width: 20,
                        child: CircularProgressIndicator(strokeWidth: 2),
                      )
                    : const Text('Create Todo'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Common success side effects:

  • Navigate to another screen
  • Show success snackbar/toast
  • Trigger another command
  • Log analytics event

Error Handler

dart
class CommandErrorHandlerWidget extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    // Use registerHandler to handle command errors
    // Shows error dialog or snackbar when command fails
    registerHandler(
      select: (TodoManager m) => m.fetchTodosCommand.errors,
      handler: (context, error, _) {
        if (error != null) {
          // Show error dialog
          showDialog(
            context: context,
            builder: (context) => AlertDialog(
              title: const Text('Error'),
              content: Text(error.toString()),
              actions: [
                TextButton(
                  onPressed: () => Navigator.of(context).pop(),
                  child: const Text('OK'),
                ),
                TextButton(
                  onPressed: () {
                    Navigator.of(context).pop();
                    di<TodoManager>().fetchTodosCommand.run();
                  },
                  child: const Text('Retry'),
                ),
              ],
            ),
          );
        }
      },
    );

    final isLoading =
        watchValue((TodoManager m) => m.fetchTodosCommand.isRunning);
    final todos = watchValue((TodoManager m) => m.todos);

    callOnce((_) {
      di<TodoManager>().fetchTodosCommand.run();
    });

    return Scaffold(
      appBar: AppBar(title: const Text('Command Handler - Errors')),
      body: Column(
        children: [
          const Padding(
            padding: EdgeInsets.all(16.0),
            child: Text(
              'This example uses registerHandler to show error dialogs',
              style: TextStyle(fontStyle: FontStyle.italic),
            ),
          ),
          if (isLoading) const LinearProgressIndicator(),
          Expanded(
            child: isLoading && todos.isEmpty
                ? const Center(child: CircularProgressIndicator())
                : todos.isEmpty
                    ? const Center(child: Text('No todos'))
                    : ListView.builder(
                        itemCount: todos.length,
                        itemBuilder: (context, index) {
                          final todo = todos[index];
                          return ListTile(
                            title: Text(todo.title),
                            subtitle: Text(todo.description),
                          );
                        },
                      ),
          ),
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: ElevatedButton(
              onPressed: isLoading
                  ? null
                  : () => di<TodoManager>().fetchTodosCommand.run(),
              child: const Text('Refresh'),
            ),
          ),
        ],
      ),
    );
  }
}

Common error side effects:

  • Show error dialog
  • Show error snackbar
  • Log error to crash reporting
  • Retry logic

Loading Button Pattern

A complete pattern for buttons that show loading state:

dart
class LoadingButtonWidget extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    final manager = di<TodoManager>();

    // Watch command execution to show inline loading state in button
    final isRunning = watch(manager.fetchTodosCommand.isRunning).value;
    final todos = watchValue((TodoManager m) => m.todos);

    return Scaffold(
      appBar: AppBar(title: const Text('Command Loading Button')),
      body: Column(
        children: [
          Expanded(
            child: todos.isEmpty
                ? const Center(child: Text('No todos - click button to load'))
                : ListView.builder(
                    itemCount: todos.length,
                    itemBuilder: (context, index) {
                      final todo = todos[index];
                      return ListTile(
                        title: Text(todo.title),
                        subtitle: Text(todo.description),
                      );
                    },
                  ),
          ),
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                // Disable button while executing
                onPressed: isRunning
                    ? null
                    : () => manager.fetchTodosCommand.run(),
                child: isRunning
                    ? const Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          SizedBox(
                            height: 20,
                            width: 20,
                            child: CircularProgressIndicator(
                              strokeWidth: 2,
                              valueColor:
                                  AlwaysStoppedAnimation<Color>(Colors.white),
                            ),
                          ),
                          SizedBox(width: 12),
                          Text('Loading...'),
                        ],
                      )
                    : const Text('Load Todos'),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

This pattern:

  • Disables button during execution (onPressed: isRunning ? null : ...)
  • Shows inline loading indicator
  • Provides visual feedback to user
  • Prevents double-submission

Watching Multiple Command States

You can watch different aspects of the same command:

dart
class MultipleCommandStatesWidget extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    final manager = di<TodoManager>();

    // Watch multiple aspects of the same command
    final isCreating = watch(manager.createTodoCommand.isRunning).value;
    final createResult = watch(manager.createTodoCommand).value;
    final createError = watch(manager.createTodoCommand.errors).value;

    // Watch another command's states
    final isFetching = watch(manager.fetchTodosCommand.isRunning).value;
    final todos = watchValue((TodoManager m) => m.todos);

    callOnce((_) {
      manager.fetchTodosCommand.run();
    });

    return Scaffold(
      appBar: AppBar(title: const Text('Multiple Command States')),
      body: Column(
        children: [
          // Status indicators for multiple commands
          Container(
            padding: const EdgeInsets.all(16),
            color: Colors.grey.shade100,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Row(
                  children: [
                    const Text('Fetch Status: ',
                        style: TextStyle(fontWeight: FontWeight.bold)),
                    if (isFetching)
                      const Row(
                        children: [
                          SizedBox(
                            height: 16,
                            width: 16,
                            child: CircularProgressIndicator(strokeWidth: 2),
                          ),
                          SizedBox(width: 8),
                          Text('Loading...'),
                        ],
                      )
                    else
                      Text('${todos.length} todos loaded'),
                  ],
                ),
                const SizedBox(height: 8),
                Row(
                  children: [
                    const Text('Create Status: ',
                        style: TextStyle(fontWeight: FontWeight.bold)),
                    if (isCreating)
                      const Row(
                        children: [
                          SizedBox(
                            height: 16,
                            width: 16,
                            child: CircularProgressIndicator(strokeWidth: 2),
                          ),
                          SizedBox(width: 8),
                          Text('Creating...'),
                        ],
                      )
                    else if (createError != null)
                      const Text('Error', style: TextStyle(color: Colors.red))
                    else if (createResult != null)
                      const Text('Success',
                          style: TextStyle(color: Colors.green))
                    else
                      const Text('Idle'),
                  ],
                ),
              ],
            ),
          ),
          Expanded(
            child: todos.isEmpty
                ? const Center(child: Text('No todos'))
                : ListView.builder(
                    itemCount: todos.length,
                    itemBuilder: (context, index) {
                      final todo = todos[index];
                      return ListTile(
                        title: Text(todo.title),
                        subtitle: Text(todo.description),
                      );
                    },
                  ),
          ),
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Row(
              children: [
                Expanded(
                  child: ElevatedButton(
                    onPressed: isFetching
                        ? null
                        : () => manager.fetchTodosCommand.run(),
                    child: const Text('Refresh'),
                  ),
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: ElevatedButton(
                    onPressed: isCreating
                        ? null
                        : () {
                            final params = CreateTodoParams(
                              title: 'Quick Todo ${todos.length + 1}',
                              description: 'Created at ${DateTime.now()}',
                            );
                            manager.createTodoCommand.run(params);
                          },
                    child: const Text('Create'),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Watch multiple properties:

  • command.isRunning - Is it running?
  • command.value - What's the result?
  • command.errors - Did it fail?
  • command.canRun - Can it run now?

Chaining Commands

Use handlers to chain commands together:

dart
class CommandChainingWidget extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    final manager = di<TodoManager>();

    // Use registerHandler to chain commands
    // When create succeeds, automatically refresh the list
    registerHandler(
      select: (TodoManager m) => m.createTodoCommand,
      handler: (context, result, _) {
        if (result != null) {
          // Chain: after creating, fetch the updated list
          manager.fetchTodosCommand.run();

          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text('Created "${result.title}" and refreshed list'),
              backgroundColor: Colors.green,
            ),
          );
        }
      },
    );

    final isCreating = watch(manager.createTodoCommand.isRunning).value;
    final isFetching = watch(manager.fetchTodosCommand.isRunning).value;
    final todos = watchValue((TodoManager m) => m.todos);

    callOnce((_) {
      manager.fetchTodosCommand.run();
    });

    return Scaffold(
      appBar: AppBar(title: const Text('Command Chaining')),
      body: Column(
        children: [
          Container(
            padding: const EdgeInsets.all(16),
            color: Colors.blue.shade50,
            child: const Text(
              'This example chains commands: Create → Refresh List',
              style: TextStyle(fontStyle: FontStyle.italic),
            ),
          ),
          if (isFetching)
            const LinearProgressIndicator()
          else
            const SizedBox(height: 4),
          Expanded(
            child: todos.isEmpty
                ? Center(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        if (isFetching)
                          const CircularProgressIndicator()
                        else
                          const Text('No todos'),
                      ],
                    ),
                  )
                : ListView.builder(
                    itemCount: todos.length,
                    itemBuilder: (context, index) {
                      final todo = todos[index];
                      return ListTile(
                        title: Text(todo.title),
                        subtitle: Text(todo.description),
                        trailing: IconButton(
                          icon: const Icon(Icons.delete),
                          onPressed: () {
                            manager.deleteTodoCommand.run(todo.id);
                            // Chain: after deleting, refresh
                            Future.delayed(
                              const Duration(milliseconds: 100),
                              () => manager.fetchTodosCommand.run(),
                            );
                          },
                        ),
                      );
                    },
                  ),
          ),
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              children: [
                SizedBox(
                  width: double.infinity,
                  child: ElevatedButton(
                    onPressed: isCreating
                        ? null
                        : () {
                            final params = CreateTodoParams(
                              title: 'New Todo ${todos.length + 1}',
                              description: 'Created at ${DateTime.now()}',
                            );
                            manager.createTodoCommand.run(params);
                          },
                    child: isCreating
                        ? const Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: [
                              SizedBox(
                                height: 20,
                                width: 20,
                                child:
                                    CircularProgressIndicator(strokeWidth: 2),
                              ),
                              SizedBox(width: 12),
                              Text('Creating & Refreshing...'),
                            ],
                          )
                        : const Text('Create Todo (will auto-refresh)'),
                  ),
                ),
                const SizedBox(height: 8),
                SizedBox(
                  width: double.infinity,
                  child: OutlinedButton(
                    onPressed: isFetching
                        ? null
                        : () => manager.fetchTodosCommand.run(),
                    child: const Text('Manual Refresh'),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Chaining patterns:

  • Create → Refresh list
  • Login → Navigate to home
  • Delete → Refresh
  • Upload → Process → Notify

Best Practices

1. Watch vs Handler

Use watch when:

  • You need to rebuild the widget
  • Showing loading indicators
  • Displaying results
  • Showing error messages inline

Use registerHandler when:

  • Navigation after success
  • Showing dialogs/snackbars
  • Logging/analytics
  • Triggering other commands
  • Any side effect that doesn't require rebuild

2. Don't Await run()

dart
// ✓ GOOD - Non-blocking, UI stays responsive
class DontAwaitExecuteGood extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () => di<TodoManager>().createTodoCommand.run(
            CreateTodoParams(title: 'New todo', description: 'Description'),
          ),
      child: Text('Submit'),
    );
  }
}
dart
// ❌ BAD - Blocks UI thread
class DontAwaitExecuteBad extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () async {
        await di<TodoManager>().createTodoCommand.runAsync(
              CreateTodoParams(title: 'New todo', description: 'Description'),
            );
      },
      child: Text('Submit'),
    );
  }
}

Why? Commands handle async internally. Just call run() and let watch_it update the UI reactively.

3. Watch Execution State for Loading

dart
// ✓ GOOD - Watch isRunning
class WatchExecutionStateGood extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    final command = createOnce(() => Command());
    final isLoading = watch(command.isRunning).value;

    if (isLoading) {
      return CircularProgressIndicator();
    }

    return Container();
  }
}

Avoid manual tracking: Don't use setState and boolean flags. Let commands and watch_it handle state reactively.

4. Handle Errors Gracefully

dart
// ✓ GOOD - Watch errors and display them
class HandleErrorsGoodWatch extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    final command = createOnce(() => Command());
    final error = watch(command.errors).value;

    if (error != null) {
      return Text('Error: $error');
    }

    return Container();
  }
}
dart
// ✓ ALSO GOOD - Use handler for error dialog
class HandleErrorsGoodHandler extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    registerHandler(
      select: (TodoManager m) => m.createTodoCommand.errors,
      handler: (context, error, _) {
        if (error != null) {
          showDialog(
            context: context,
            builder: (_) => AlertDialog(
              title: Text('Error'),
              content: Text('$error'),
            ),
          );
        }
      },
    );
    return Container();
  }
}

5. Only Show Loading on Initial Load

dart
// Show spinner only when no data yet
class InitialLoadPattern extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    final command = di<TodoManager>().fetchTodosCommand;
    final isLoading = watch(command.isRunning).value;
    final data = watch(command).value;

    // Show spinner only when no data yet
    if (isLoading && data.isEmpty) {
      return CircularProgressIndicator();
    }

    // Show data even while refreshing
    return ListView(
      children: [
        if (isLoading) LinearProgressIndicator(), // Subtle indicator
        ...data.map((item) => ListTile(title: Text(item.title))),
      ],
    );
  }
}

Common Patterns

Form Submission

dart
// Form submission pattern
class FormSubmissionPattern extends WatchingWidget {
  final GlobalKey<FormState> formKey = GlobalKey<FormState>();
  final formData = FormData('Example');

  @override
  Widget build(BuildContext context) {
    final manager = di<Manager>();
    final isSubmitting = watch(manager.submitCommand.isRunning).value;
    final canSubmit = formKey.currentState?.validate() ?? false;

    return ElevatedButton(
      onPressed: canSubmit && !isSubmitting
          ? () => manager.submitCommand.run()
          : null,
      child: isSubmitting ? CircularProgressIndicator() : Text('Submit'),
    );
  }
}

Pull to Refresh

dart
// Pull to refresh pattern
class PullToRefreshPattern extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    final manager = di<TodoManager>();

    return RefreshIndicator(
      onRefresh: () async {
        manager.fetchTodosCommand.run();
        await manager.fetchTodosCommand.runAsync();
      },
      child: ListView(children: []),
    );
  }
}

Retry on Error

dart
// Retry on error pattern
class RetryOnErrorPattern extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    final command = createOnce(() => Command());
    final error = watch(command.errors).value;

    if (error != null) {
      return Column(
        children: [
          Text('Error: $error'),
          ElevatedButton(
            onPressed: () => command.run(),
            child: Text('Retry'),
          ),
        ],
      );
    }

    return Container();
  }
}

See Also

Released under the MIT License.