asyncredux-connector-pattern
Implement the Connector pattern for separating smart and dumb widgets. Covers creating StoreConnector widgets, implementing VmFactory and Vm classes, building view-models, and optimizing rebuilds with view-model equality.
Best use case
asyncredux-connector-pattern is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Implement the Connector pattern for separating smart and dumb widgets. Covers creating StoreConnector widgets, implementing VmFactory and Vm classes, building view-models, and optimizing rebuilds with view-model equality.
Teams using asyncredux-connector-pattern 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
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/asyncredux-connector-pattern/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How asyncredux-connector-pattern Compares
| Feature / Agent | asyncredux-connector-pattern | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Implement the Connector pattern for separating smart and dumb widgets. Covers creating StoreConnector widgets, implementing VmFactory and Vm classes, building view-models, and optimizing rebuilds with view-model equality.
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
## Overview
The connector pattern separates store access logic from UI presentation. Instead of widgets directly accessing the store via `context.state` and `context.dispatch()`, a "smart" connector widget extracts store data and passes it to a "dumb" presentational widget through constructor parameters.
## Why Use the Connector Pattern?
1. **Testing simplification** - Test UI widgets without creating a Redux store by passing mock data
2. **Separation of concerns** - UI widgets focus on appearance; connectors handle business logic
3. **Reusability** - Presentational widgets function independently of Redux
4. **Code clarity** - Widget code is not cluttered with state access and transformation logic
5. **Optimized rebuilds** - Only rebuild when the view-model changes
## The Three Components
### 1. ViewModel (Vm)
Contains only the data the UI widget requires. Extends `Vm` and lists equality fields:
```dart
class CounterViewModel extends Vm {
final int counter;
final String description;
final VoidCallback onIncrement;
CounterViewModel({
required this.counter,
required this.description,
required this.onIncrement,
}) : super(equals: [counter, description]);
}
```
The `equals` list tells AsyncRedux which fields to compare when deciding whether to rebuild. Callbacks (like `onIncrement`) should NOT be included in `equals`.
### 2. VmFactory
Transforms store state into a view-model. Extends `VmFactory` and implements `fromStore()`:
```dart
class CounterFactory extends VmFactory<AppState, CounterConnector, CounterViewModel> {
CounterFactory(connector) : super(connector);
@override
CounterViewModel fromStore() => CounterViewModel(
counter: state.counter,
description: state.description,
onIncrement: () => dispatch(IncrementAction()),
);
}
```
The factory has access to:
- `state` - The store state when the factory was created
- `dispatch()` - To dispatch actions from callbacks
- `dispatchSync()` - For synchronous dispatch
- `connector` - Reference to the parent connector widget
### 3. StoreConnector
Bridges the store and UI widget:
```dart
class CounterConnector extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, CounterViewModel>(
vm: () => CounterFactory(this),
builder: (BuildContext context, CounterViewModel vm) => CounterWidget(
counter: vm.counter,
description: vm.description,
onIncrement: vm.onIncrement,
),
);
}
}
```
The "dumb" widget receives data through constructor parameters:
```dart
class CounterWidget extends StatelessWidget {
final int counter;
final String description;
final VoidCallback onIncrement;
const CounterWidget({
required this.counter,
required this.description,
required this.onIncrement,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('$counter'),
Text(description),
ElevatedButton(
onPressed: onIncrement,
child: Text('Increment'),
),
],
);
}
}
```
## Rebuild Optimization
Each time an action changes the store state, `StoreConnector` compares the new view-model with the previous one. It only rebuilds if they differ (based on the `equals` list).
To prevent rebuilds even when state changes, use `notify: false`:
```dart
dispatch(MyAction(), notify: false);
```
## Advanced Factory Techniques
### Accessing Connector Properties
Pass data from the connector widget to the factory:
```dart
class UserConnector extends StatelessWidget {
final int userId;
const UserConnector({required this.userId});
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, UserViewModel>(
vm: () => UserFactory(this),
builder: (context, vm) => UserWidget(user: vm.user),
);
}
}
class UserFactory extends VmFactory<AppState, UserConnector, UserViewModel> {
UserFactory(connector) : super(connector);
@override
UserViewModel fromStore() => UserViewModel(
// Access connector.userId here
user: state.users.firstWhere((u) => u.id == connector.userId),
);
}
```
### state vs currentState()
Inside the factory:
- `state` - The state when the factory was created (final, won't change)
- `currentState()` - The current store state at the moment of the call
These usually match, but diverge in callbacks after `dispatchSync()`:
```dart
@override
UserViewModel fromStore() => UserViewModel(
onSave: () {
dispatchSync(SaveAction());
// state still has old value
// currentState() has new value after SaveAction
},
);
```
### Using the vm Getter in Callbacks
Access already-computed view-model fields in callbacks to avoid redundant calculations:
```dart
@override
UserViewModel fromStore() => UserViewModel(
name: state.user.name,
onSave: () {
// Use vm.name instead of recalculating from state
print('Saving user: ${vm.name}');
dispatch(SaveAction(vm.name));
},
);
```
**Note:** The `vm` getter is only available after `fromStore()` completes. Use it in callbacks, not during view-model construction.
### Base Factory Pattern
Create a base factory to reduce boilerplate:
```dart
abstract class BaseFactory<T extends StatelessWidget, Model extends Vm>
extends VmFactory<AppState, T, Model> {
BaseFactory(T connector) : super(connector);
// Common getters
User get user => state.user;
Settings get settings => state.settings;
}
class MyFactory extends BaseFactory<MyConnector, MyViewModel> {
MyFactory(connector) : super(connector);
@override
MyViewModel fromStore() => MyViewModel(
user: user, // Uses inherited getter
);
}
```
## Nullable View-Models
When you cannot generate a valid view-model (e.g., data still loading), return null:
```dart
class HomeConnector extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, HomeViewModel?>( // Nullable type
vm: () => HomeFactory(this),
builder: (BuildContext context, HomeViewModel? vm) { // Nullable param
return (vm == null)
? Text("User not logged in")
: HomePage(user: vm.user);
},
);
}
}
class HomeFactory extends VmFactory<AppState, HomeConnector, HomeViewModel?> {
HomeFactory(connector) : super(connector);
@override
HomeViewModel? fromStore() { // Nullable return
return (state.user == null)
? null
: HomeViewModel(user: state.user!);
}
}
```
## Migrating from flutter_redux
If migrating from `flutter_redux`, you can use the `converter` parameter instead of `vm`:
```dart
class MyConnector extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, ViewModel>(
converter: (store) => ViewModel.fromStore(store),
builder: (context, vm) => MyWidget(name: vm.name),
);
}
}
class ViewModel extends Vm {
final String name;
final VoidCallback onSave;
ViewModel({required this.name, required this.onSave})
: super(equals: [name]);
static ViewModel fromStore(Store<AppState> store) {
return ViewModel(
name: store.state.name,
onSave: () => store.dispatch(SaveAction()),
);
}
}
```
Note: `vm` and `converter` are mutually exclusive. The `vm` approach is recommended for new code.
## Debugging Rebuilds
To observe when connectors rebuild, pass a `modelObserver` to the store:
```dart
var store = Store<AppState>(
initialState: AppState.initialState(),
modelObserver: DefaultModelObserver(),
);
```
Add `debug: this` to StoreConnector for connector type names in logs:
```dart
StoreConnector<AppState, ViewModel>(
debug: this,
vm: () => Factory(this),
builder: (context, vm) => MyWidget(vm: vm),
);
```
Override `toString()` in your ViewModel for custom diagnostic output:
```dart
class MyViewModel extends Vm {
final int counter;
MyViewModel({required this.counter}) : super(equals: [counter]);
@override
String toString() => 'MyViewModel{counter: $counter}';
}
```
Console output shows rebuild information:
```
Model D:1 R:1 = Rebuild:true, Connector:MyWidgetConnector, Model:MyViewModel{counter: 5}
```
## Testing View-Models
Use `Vm.createFrom()` to test view-models in isolation:
```dart
test('view-model properties', () {
var store = Store<AppState>(initialState: AppState(name: "Mary"));
var vm = Vm.createFrom(store, MyFactory());
expect(vm.name, "Mary");
});
test('view-model callbacks dispatch actions', () async {
var store = Store<AppState>(initialState: AppState(name: "Mary"));
var vm = Vm.createFrom(store, MyFactory());
vm.onChangeName("Bill");
await store.waitActionType(ChangeNameAction);
expect(store.state.name, "Bill");
});
```
**Important:** `Vm.createFrom()` can only be called once per factory instance. Create a new factory for each test.
## Complete Example
```dart
// State
class AppState {
final int counter;
final String description;
AppState({required this.counter, required this.description});
AppState copy({int? counter, String? description}) => AppState(
counter: counter ?? this.counter,
description: description ?? this.description,
);
}
// Action
class IncrementAction extends ReduxAction<AppState> {
@override
AppState reduce() => state.copy(counter: state.counter + 1);
}
// View-Model
class CounterViewModel extends Vm {
final int counter;
final String description;
final VoidCallback onIncrement;
CounterViewModel({
required this.counter,
required this.description,
required this.onIncrement,
}) : super(equals: [counter, description]);
}
// Factory
class CounterFactory extends VmFactory<AppState, CounterConnector, CounterViewModel> {
CounterFactory(connector) : super(connector);
@override
CounterViewModel fromStore() => CounterViewModel(
counter: state.counter,
description: state.description,
onIncrement: () => dispatch(IncrementAction()),
);
}
// Connector (Smart Widget)
class CounterConnector extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, CounterViewModel>(
vm: () => CounterFactory(this),
builder: (context, vm) => CounterWidget(
counter: vm.counter,
description: vm.description,
onIncrement: vm.onIncrement,
),
);
}
}
// Presentational Widget (Dumb Widget)
class CounterWidget extends StatelessWidget {
final int counter;
final String description;
final VoidCallback onIncrement;
const CounterWidget({
required this.counter,
required this.description,
required this.onIncrement,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('$counter', style: TextStyle(fontSize: 48)),
Text(description),
SizedBox(height: 20),
ElevatedButton(
onPressed: onIncrement,
child: Text('Increment'),
),
],
);
}
}
```
## References
URLs from the documentation:
- https://asyncredux.com/sitemap.xml
- https://asyncredux.com/flutter/connector/connector-pattern
- https://asyncredux.com/flutter/connector/store-connector
- https://asyncredux.com/flutter/connector/advanced-view-model
- https://asyncredux.com/flutter/connector/cannot-generate-view-model
- https://asyncredux.com/flutter/connector/migrating-from-flutter-redux
- https://asyncredux.com/flutter/testing/testing-the-view-model
- https://asyncredux.com/flutter/basics/using-the-store-state
- https://asyncredux.com/flutter/miscellaneous/observing-rebuildsRelated Skills
atlan-sql-connector-patterns
Select and apply the correct SQL connector implementation pattern (SDK-default minimal or source-specific custom). Use when building or extending SQL metadata/query extraction connectors.
asyncio-concurrency-patterns
Complete guide for asyncio concurrency patterns including event loops, coroutines, tasks, futures, async context managers, and performance optimization
async-python-patterns
Master Python asyncio, concurrent programming, and async/await patterns for high-performance applications. Use when building async APIs, concurrent systems, or I/O-bound applications requiring non-blocking operations.
async-patterns-guide
Guides users on modern async patterns including native async fn in traits, async closures, and avoiding async-trait when possible. Activates when users work with async code.
async-await-patterns
Use when writing JavaScript or TypeScript code with asynchronous operations
astro-patterns
Astro best practices, routing patterns, component architecture, and static site generation techniques. Use when building Astro websites, setting up routing, designing component architecture, configuring static site generation, optimizing build performance, implementing content strategies, or when user mentions Astro patterns, routing, component design, SSG, static sites, or Astro best practices.
argparse-patterns
Standard library Python argparse examples with subparsers, choices, actions, and nested command patterns. Use when building Python CLIs without external dependencies, implementing argument parsing, creating subcommands, or when user mentions argparse, standard library CLI, subparsers, argument validation, or nested commands.
architecture-patterns
Software architecture patterns and best practices
architecture-pattern-selector
Recommend architecture patterns (monolith, microservices, serverless, modular monolith) based on scale, team size, and constraints. Use when cto-architect needs to select the right architectural approach for a new system or migration.
architectural-patterns-large-react
Establish scalable architecture using modular boundaries, domain-driven design, and consistent data access patterns.
architectural-pattern-discovery
Discovers architectural and design patterns across all abstraction levels. Analyzes structural patterns, component relationships, recurring solution approaches, and design principles. Works with any technology stack without prior framework knowledge to provide comprehensive pattern understanding from code-level to system-level architecture.
arch_patterns
Architecture patterns - monolith vs microservices, layered, event-driven, CQRS.