.claude/skills/asyncredux-dependency-injection/SKILL.md
Inject dependencies into actions using the environment, dependencies, and configuration pattern. Covers creating an Environment enum, a Dependencies class, passing them to the Store, accessing them from actions and widgets, and using dependency injection for testability.
npx skillsauth add marcglasberg/async_redux asyncredux-dependency-injectionInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
AsyncRedux provides dependency injection through three Store parameters:
environment: Specifies if the app is running in production, staging, development, testing, etc. Should be immutable and not change during app execution. Accessible from both actions and widgets.dependencies: A container for injected services (repositories, APIs, etc.), created via a factory that receives the Store, so it can vary based on the environment. Usually not accessible from widgets.configuration: For feature flags and other configuration values. Accessible from both actions and widgets.Create an enum (or class) specifying the app's running context:
enum Environment {
production,
staging,
testing;
bool get isProduction => this == Environment.production;
bool get isStaging => this == Environment.staging;
bool get isTesting => this == Environment.testing;
}
Create an abstract class with a factory that returns different implementations based on the environment:
abstract class Dependencies {
factory Dependencies(Store store) {
if (store.environment == Environment.production) {
return DependenciesProduction();
} else if (store.environment == Environment.staging) {
return DependenciesStaging();
} else {
return DependenciesTesting();
}
}
ApiClient get apiClient;
AuthService get authService;
int limit(int value);
}
class DependenciesProduction implements Dependencies {
@override
ApiClient get apiClient => RealApiClient();
@override
AuthService get authService => FirebaseAuthService();
@override
int limit(int value) => min(value, 5);
}
class DependenciesTesting implements Dependencies {
@override
ApiClient get apiClient => MockApiClient();
@override
AuthService get authService => MockAuthService();
@override
int limit(int value) => min(value, 1000); // Higher limit in tests
}
class Config {
bool isABtestingOn = false;
bool showAdminConsole = false;
}
When creating the store, pass the environment, dependencies factory, and configuration factory:
void main() {
var store = Store<AppState>(
initialState: AppState.initialState(),
environment: Environment.production,
dependencies: (store) => Dependencies(store),
configuration: (store) => Config(),
);
runApp(
StoreProvider<AppState>(
store: store,
child: MyApp(),
),
);
}
The dependencies and configuration parameters are factories that receive the Store, so they can read store.environment to vary their behavior.
Define a base action class with typed getters for dependencies, environment, and configuration:
abstract class Action extends ReduxAction<AppState> {
Dependencies get dependencies => super.store.dependencies as Dependencies;
Environment get environment => super.store.environment as Environment;
Config get config => super.store.configuration as Config;
}
Now use them in your actions:
class FetchUserAction extends Action {
final String userId;
FetchUserAction(this.userId);
@override
Future<AppState?> reduce() async {
final user = await dependencies.apiClient.fetchUser(userId);
return state.copy(user: user);
}
}
class IncrementAction extends Action {
final int amount;
IncrementAction({required this.amount});
@override
AppState reduce() {
int newState = state.counter + amount;
int limitedState = dependencies.limit(newState);
return state.copy(counter: limitedState);
}
}
Create a BuildContext extension. The environment and configuration are available via getEnvironment and getConfiguration. Note: dependencies should usually NOT be accessed from widgets.
extension BuildContextExtension on BuildContext {
AppState get state => getState<AppState>();
R select<R>(R Function(AppState state) selector) =>
getSelect<AppState, R>(selector);
/// Access the environment from widgets (does not trigger rebuilds).
Environment get environment => getEnvironment<AppState>() as Environment;
/// Access the configuration from widgets (does not trigger rebuilds).
Config get config => getConfiguration<AppState>() as Config;
}
Use in widgets:
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final env = context.environment;
int counter = context.state;
return Scaffold(
appBar: AppBar(title: const Text('Dependency Injection Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Use the environment to change the UI.
Text('Running in ${env}.', textAlign: TextAlign.center),
Text('$counter', style: const TextStyle(fontSize: 30)),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => dispatch(IncrementAction(amount: 1)),
child: const Icon(Icons.add),
),
);
}
}
If you use StoreConnector, extend VmFactory with typed getters:
abstract class AppFactory<T extends Widget?, Model extends Vm>
extends VmFactory<AppState, T, Model> {
AppFactory([T? connector]) : super(connector);
Dependencies get dependencies => store.dependencies as Dependencies;
Environment get environment => store.environment as Environment;
Config get config => store.configuration as Config;
}
The pattern makes testing straightforward by injecting test implementations:
void main() {
group('IncrementAction', () {
test('increments counter with test dependencies', () async {
var store = Store<AppState>(
initialState: AppState(counter: 0),
environment: Environment.testing,
dependencies: (store) => Dependencies(store), // Returns DependenciesTesting
);
await store.dispatchAndWait(IncrementAction(amount: 5));
// DependenciesTesting has limit of 1000, so value is 5
expect(store.state.counter, 5);
});
test('production dependencies limit counter', () async {
var store = Store<AppState>(
initialState: AppState(counter: 3),
environment: Environment.production,
dependencies: (store) => Dependencies(store), // Returns DependenciesProduction
);
await store.dispatchAndWait(IncrementAction(amount: 10));
// DependenciesProduction limits to 5
expect(store.state.counter, 5);
});
});
}
import 'dart:math';
import 'package:async_redux/async_redux.dart';
import 'package:flutter/material.dart';
late Store<int> store;
void main() {
store = Store<int>(
initialState: 0,
environment: Environment.production,
dependencies: (store) => Dependencies(store),
);
runApp(MyApp());
}
enum Environment {
production,
staging,
testing;
bool get isProduction => this == Environment.production;
bool get isStaging => this == Environment.staging;
bool get isTesting => this == Environment.testing;
}
abstract class Dependencies {
factory Dependencies(Store store) {
if (store.environment == Environment.production) {
return DependenciesProduction();
} else if (store.environment == Environment.staging) {
return DependenciesStaging();
} else {
return DependenciesTesting();
}
}
int limit(int value);
}
class DependenciesProduction implements Dependencies {
@override
int limit(int value) => min(value, 5);
}
class DependenciesStaging implements Dependencies {
@override
int limit(int value) => min(value, 25);
}
class DependenciesTesting implements Dependencies {
@override
int limit(int value) => min(value, 1000);
}
abstract class Action extends ReduxAction<int> {
Dependencies get dependencies => super.store.dependencies as Dependencies;
}
class IncrementAction extends Action {
final int amount;
IncrementAction({required this.amount});
@override
int reduce() {
int newState = state + amount;
int limitedState = dependencies.limit(newState);
return limitedState;
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StoreProvider<int>(
store: store,
child: MaterialApp(home: MyHomePage()),
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final env = context.environment;
int counter = context.state;
return Scaffold(
appBar: AppBar(title: const Text('Dependency Injection Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Running in ${env}.', textAlign: TextAlign.center),
const Text(
'You have pushed the button this many times:\n'
'(limited by the environment)',
textAlign: TextAlign.center,
),
Text('$counter', style: const TextStyle(fontSize: 30)),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => dispatch(IncrementAction(amount: 1)),
child: const Icon(Icons.add),
),
);
}
}
extension BuildContextExtension on BuildContext {
int get state => getState<int>();
int read() => getRead<int>();
R select<R>(R Function(int state) selector) => getSelect<int, R>(selector);
R? event<R>(Evt<R> Function(int state) selector) => getEvent<int, R>(selector);
Environment get environment => getEnvironment<int>() as Environment;
}
environment identifies the running context, dependencies provides services, configuration holds feature flagsdependencies and configuration factories receive the Store, allowing them to vary based on environmentURLs from the documentation:
data-ai
Show loading states and handle action failures in widgets. Covers `isWaiting(ActionType)` for spinners, `isFailed(ActionType)` for error states, `exceptionFor(ActionType)` for error messages, and `clearExceptionFor()` to reset failure states.
data-ai
Use `waitCondition()` inside actions to pause execution until state meets criteria. Covers waiting for price thresholds, coordinating between actions, and implementing conditional workflows.
testing
Handle user-facing errors with UserException. Covers throwing UserException from actions, setting up UserExceptionDialog, customizing error dialogs with `onShowUserExceptionDialog`, and using UserExceptionAction for non-interrupting error display.
tools
Implement undo/redo functionality using state observers. Covers recording state history with stateObserver, creating a RecoverStateAction, implementing undo for the full state or partial state, and managing history limits.