askill
asyncredux-dependency-injection

asyncredux-dependency-injectionSafety 100Repository

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.

0 stars
1.2k downloads
Updated 2/5/2026

Package Files

Loading files...
SKILL.md

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:

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):

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:

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:

/// 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:

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:

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:

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:

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

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:

Install

Download ZIP
Requires askill CLI v1.0+

AI Quality Score

95/100Analyzed 2/10/2026

A comprehensive and well-structured guide for implementing dependency injection in AsyncRedux using the Environment pattern, featuring clear steps, testing strategies, and a complete code example.

100
100
90
100
100

Metadata

Licenseunknown
Version-
Updated2/5/2026
Publishermajiayu000

Tags

ci-cdgithubtesting