asyncredux-dependency-injection

Inject dependencies into actions using the environment pattern. Covers creating an Environment class, passing it to the Store, accessing `env` from actions, and using dependency injection for testability.

16 stars

Best use case

asyncredux-dependency-injection is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Inject dependencies into actions using the environment pattern. Covers creating an Environment class, passing it to the Store, accessing `env` from actions, and using dependency injection for testability.

Teams using asyncredux-dependency-injection should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/asyncredux-dependency-injection/SKILL.md --create-dirs "https://raw.githubusercontent.com/diegosouzapw/awesome-omni-skill/main/skills/design/asyncredux-dependency-injection/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/asyncredux-dependency-injection/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How asyncredux-dependency-injection Compares

Feature / Agentasyncredux-dependency-injectionStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Inject dependencies into actions using the environment pattern. Covers creating an Environment class, passing it to the Store, accessing `env` from actions, and using dependency injection for testability.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Dependency Injection with Environment

AsyncRedux provides dependency injection through the Store's `environment` parameter. Dependencies stored in the environment are accessible throughout actions, widgets, and view-model factories, and are automatically disposed when the store is disposed.

## Step 1: Define an Environment Interface

Create an abstract class defining your injectable services:

```dart
abstract class Environment {
  // Define your injectable services as abstract methods/getters
  ApiClient get apiClient;
  AuthService get authService;
  Analytics get analytics;

  int incrementer(int value, int amount);
  int limit(int value);
}
```

## Step 2: Implement the Environment

Create concrete implementations for different contexts (production, staging, test):

```dart
class ProductionEnvironment implements Environment {
  @override
  ApiClient get apiClient => RealApiClient();

  @override
  AuthService get authService => FirebaseAuthService();

  @override
  Analytics get analytics => MixpanelAnalytics();

  @override
  int incrementer(int value, int amount) => value + amount;

  @override
  int limit(int value) => min(value, 100);
}

class TestEnvironment implements Environment {
  @override
  ApiClient get apiClient => MockApiClient();

  @override
  AuthService get authService => MockAuthService();

  @override
  Analytics get analytics => NoOpAnalytics();

  @override
  int incrementer(int value, int amount) => value + amount;

  @override
  int limit(int value) => value; // No limit in tests
}
```

## Step 3: Pass Environment to the Store

When creating the store, pass your environment instance:

```dart
void main() {
  var store = Store<AppState>(
    initialState: AppState.initialState(),
    environment: ProductionEnvironment(),
  );

  runApp(
    StoreProvider<AppState>(
      store: store,
      child: MyApp(),
    ),
  );
}
```

## Step 4: Access Environment from Actions

Extend `ReduxAction` to provide typed access to your environment:

```dart
/// Base action class with typed environment access
abstract class Action extends ReduxAction<AppState> {
  @override
  Environment get env => super.env as Environment;
}
```

Now use `env` in your actions:

```dart
class FetchUserAction extends Action {
  final String userId;
  FetchUserAction(this.userId);

  @override
  Future<AppState?> reduce() async {
    // Access injected dependencies via env
    final user = await env.apiClient.fetchUser(userId);
    env.analytics.logEvent('user_fetched');

    return state.copy(user: user);
  }
}

class IncrementAction extends Action {
  final int amount;
  IncrementAction({required this.amount});

  @override
  AppState reduce() {
    // Use environment methods in reducer
    final newCount = env.incrementer(state.counter, amount);
    return state.copy(counter: env.limit(newCount));
  }
}
```

## Step 5: Access Environment from Widgets

Create a `BuildContext` extension to access the environment in widgets:

```dart
extension BuildContextExtension on BuildContext {
  AppState get state => getState<AppState>();

  R select<R>(R Function(AppState state) selector) =>
      getSelect<AppState, R>(selector);

  Environment get env => getEnvironment<AppState>() as Environment;
}
```

Use in widgets:

```dart
class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Access environment
    final env = context.env;

    // Use environment logic in selectors
    final counter = context.select((state) => env.limit(state.counter));

    return Scaffold(
      body: Center(
        child: Text('Counter: $counter'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => dispatch(IncrementAction(amount: 1)),
        child: const Icon(Icons.add),
      ),
    );
  }
}
```

## Testing with Different Environments

The environment pattern makes testing straightforward by allowing you to inject test doubles:

```dart
void main() {
  group('IncrementAction', () {
    test('increments counter using environment', () async {
      // Create store with test environment
      var store = Store<AppState>(
        initialState: AppState(counter: 0),
        environment: TestEnvironment(),
      );

      await store.dispatchAndWait(IncrementAction(amount: 5));

      // TestEnvironment has no limit, so value is 5
      expect(store.state.counter, 5);
    });

    test('production environment limits counter', () async {
      var store = Store<AppState>(
        initialState: AppState(counter: 95),
        environment: ProductionEnvironment(),
      );

      await store.dispatchAndWait(IncrementAction(amount: 10));

      // ProductionEnvironment limits to 100
      expect(store.state.counter, 100);
    });
  });
}
```

## Complete Working Example

```dart
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: EnvironmentImpl(),
  );
  runApp(MyApp());
}

/// Abstract environment interface
abstract class Environment {
  int incrementer(int value, int amount);
  int limit(int value);
}

/// Production implementation
class EnvironmentImpl implements Environment {
  @override
  int incrementer(int value, int amount) => value + amount;

  @override
  int limit(int value) => min(value, 5); // Limit counter at 5
}

/// Base action with typed env access
abstract class Action extends ReduxAction<int> {
  @override
  Environment get env => super.env as Environment;
}

/// Action using environment
class IncrementAction extends Action {
  final int amount;
  IncrementAction({required this.amount});

  @override
  int reduce() => env.incrementer(state, amount);
}

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.env;
    final counter = context.select((state) => env.limit(state));

    return Scaffold(
      appBar: AppBar(title: const Text('Dependency Injection Example')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Counter (limited to 5):'),
            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>();
  R select<R>(R Function(int state) selector) => getSelect<int, R>(selector);
  Environment get env => getEnvironment<int>() as Environment;
}
```

## Key Benefits

- **Testability**: Swap implementations for testing without changing action code
- **Separation of concerns**: Business logic lives in environment, actions orchestrate
- **Automatic disposal**: Dependencies are disposed when the store is disposed
- **Type safety**: The typed `env` getter provides compile-time checking
- **Scoped dependencies**: Each store instance has its own environment, preventing test contamination

## References

URLs from the documentation:
- https://asyncredux.com/sitemap.xml
- https://asyncredux.com/flutter/miscellaneous/dependency-injection
- https://asyncredux.com/flutter/testing/mocking
- https://asyncredux.com/flutter/basics/store
- https://asyncredux.com/flutter/advanced-actions/redux-action
- https://asyncredux.com/flutter/connector/store-connector
- https://asyncredux.com/flutter/testing/store-tester
- https://asyncredux.com/flutter/testing/dispatch-wait-and-expect
- https://github.com/marcglasberg/async_redux/blob/master/example/lib/main_environment.dart

Related Skills

asyncredux-wait-fail-succeed

16
from diegosouzapw/awesome-omni-skill

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.

asyncredux-wait-condition

16
from diegosouzapw/awesome-omni-skill

Use `waitCondition()` inside actions to pause execution until state meets criteria. Covers waiting for price thresholds, coordinating between actions, and implementing conditional workflows.

asyncredux-user-exceptions

16
from diegosouzapw/awesome-omni-skill

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.

asyncredux-sync-actions

16
from diegosouzapw/awesome-omni-skill

Creates AsyncRedux (Flutter) synchronous actions that update state immediately by implementing reduce() to return a new state.

asyncredux-streams-timers

16
from diegosouzapw/awesome-omni-skill

Manage Streams and Timers with AsyncRedux. Covers creating actions to start/stop streams, storing stream subscriptions in store props, dispatching actions from stream callbacks, and proper cleanup with disposeProps().

asyncredux-state-access

16
from diegosouzapw/awesome-omni-skill

Access store state in widgets using `context.state`, `context.select()`, and `context.read()`. Covers when to use each method, setting up BuildContext extensions, and optimizing widget rebuilds with selective state access.

asyncredux-setup

16
from diegosouzapw/awesome-omni-skill

Initialize, setup and configure AsyncRedux in a Flutter app. Use it whenever starting a new AsyncRedux project, or when the user requests.

asyncredux-selectors

16
from diegosouzapw/awesome-omni-skill

Create and cache selectors for efficient state access. Covers writing selector functions, caching with `cache1` and `cache2`, the reselect pattern, and avoiding repeated computations in widgets.

asyncredux-persistence

16
from diegosouzapw/awesome-omni-skill

Implement local state persistence using Persistor. Covers creating a custom Persistor class, implementing `readState()`, `persistDifference()`, `deleteState()`, using LocalPersist helper, throttling saves, and pausing/resuming persistence with app lifecycle.

asyncredux-optimistic-update-mixin

16
from diegosouzapw/awesome-omni-skill

Add the OptimisticUpdate mixin for instant UI feedback before server confirmation. Covers immediate state changes, automatic rollback on failure, and optionally notifying users of rollback.

asyncredux-navigation

16
from diegosouzapw/awesome-omni-skill

Handle navigation through actions using NavigateAction. Covers setting up the navigator key, dispatching NavigateAction for push/pop/replace, and testing navigation in isolation.

asyncredux-events

16
from diegosouzapw/awesome-omni-skill

Use the Event class to interact with Flutter's stateful widgets (TextField, ListView, etc.). Covers creating Event objects in state, consuming events with `context.event()`, scrolling lists, changing text fields, and the event lifecycle.