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.
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:
npx skills add marcglasberg/asyncredux-dependency-injection下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer