Claude-skill-registry 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.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/asyncredux-dependency-injection" ~/.claude/skills/majiayu000-claude-skill-registry-asyncredux-dependency-injection && rm -rf "$T"
manifest:
skills/data/asyncredux-dependency-injection/SKILL.mdsource content
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
getter provides compile-time checkingenv - 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