asyncredux-streams-timers
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().
Best use case
asyncredux-streams-timers is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
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().
Teams using asyncredux-streams-timers 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-streams-timers/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How asyncredux-streams-timers Compares
| Feature / Agent | asyncredux-streams-timers | 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?
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().
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
# AsyncRedux Streams and Timers
## Core Principles
Two fundamental rules for working with streams and timers in AsyncRedux:
1. **Don't send streams or timers down to widgets.** Don't declare, subscribe, or unsubscribe to them inside widgets.
2. **Don't put streams or timers in the Redux store state.** They produce state changes, but they are not state themselves.
Instead, store streams and timers in the store's **props** - a key-value container that can hold any object type.
## Store Props API
AsyncRedux provides methods for managing props in both `Store` and `ReduxAction`:
### `setProp(key, value)`
Stores an object (timer, stream subscription, etc.) in the store's props:
```dart
setProp('myTimer', Timer.periodic(Duration(seconds: 1), callback));
setProp('priceStream', priceStream.listen(onData));
```
### `prop<T>(key)`
Retrieves a property from the store:
```dart
var timer = prop<Timer>('myTimer');
var subscription = prop<StreamSubscription>('priceStream');
```
### `disposeProp(key)`
Disposes a single property by its key. Automatically cancels/closes timers, futures, and stream subscriptions:
```dart
disposeProp('myTimer'); // Cancels the timer and removes from props
```
### `disposeProps([predicate])`
Disposes multiple properties. Without a predicate, disposes all Timer, Future, and Stream-related props:
```dart
// Dispose all timers, futures, stream subscriptions
disposeProps();
// Dispose only timers
disposeProps(({Object? key, Object? value}) => value is Timer);
// Dispose props with specific keys
disposeProps(({Object? key, Object? value}) => key.toString().startsWith('temp_'));
```
## Timer Pattern
### Starting a Timer
Create an action that sets up a `Timer.periodic` and stores it in props:
```dart
class StartPollingAction extends ReduxAction<AppState> {
@override
AppState? reduce() {
// Store the timer in props
setProp('pollingTimer', Timer.periodic(
Duration(seconds: 5),
(timer) => dispatch(FetchDataAction()),
));
return null; // No state change from this action
}
}
```
### Stopping a Timer
Create an action to dispose the timer:
```dart
class StopPollingAction extends ReduxAction<AppState> {
@override
AppState? reduce() {
disposeProp('pollingTimer');
return null;
}
}
```
### Timer with Tick Count
Access the timer's tick count in callbacks:
```dart
class StartTimerAction extends ReduxAction<AppState> {
@override
AppState? reduce() {
setProp('myTimer', Timer.periodic(
Duration(seconds: 1),
(timer) => dispatch(UpdateTickAction(timer.tick)),
));
return null;
}
}
class UpdateTickAction extends ReduxAction<AppState> {
final int tick;
UpdateTickAction(this.tick);
@override
AppState? reduce() => state.copy(tickCount: tick);
}
```
## Stream Pattern
### Subscribing to a Stream
Create an action that subscribes to a stream and stores the subscription:
```dart
class StartListeningAction extends ReduxAction<AppState> {
@override
AppState? reduce() {
final subscription = myDataStream.listen(
(data) => dispatch(DataReceivedAction(data)),
onError: (error) => dispatch(StreamErrorAction(error)),
);
setProp('dataSubscription', subscription);
return null;
}
}
```
### Unsubscribing from a Stream
```dart
class StopListeningAction extends ReduxAction<AppState> {
@override
AppState? reduce() {
disposeProp('dataSubscription');
return null;
}
}
```
### Handling Stream Data
The stream callback dispatches an action with the data, which updates the state:
```dart
class DataReceivedAction extends ReduxAction<AppState> {
final MyData data;
DataReceivedAction(this.data);
@override
AppState? reduce() => state.copy(latestData: data);
}
```
## Lifecycle Management
### Screen-Specific Streams/Timers
Use `StoreConnector`'s `onInit` and `onDispose` callbacks:
```dart
class PriceScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, _Vm>(
vm: () => _Factory(),
onInit: _onInit,
onDispose: _onDispose,
builder: (context, vm) => PriceWidget(price: vm.price),
);
}
void _onInit(Store<AppState> store) {
store.dispatch(StartPriceStreamAction());
}
void _onDispose(Store<AppState> store) {
store.dispatch(StopPriceStreamAction());
}
}
```
### App-Wide Streams/Timers
Start after store creation, stop when app closes:
```dart
void main() {
final store = Store<AppState>(initialState: AppState.initialState());
// Start app-wide streams/timers
store.dispatch(StartGlobalPollingAction());
runApp(StoreProvider<AppState>(
store: store,
child: MyApp(),
));
}
// In your app's dispose logic
store.dispatch(StopGlobalPollingAction());
store.disposeProps(); // Clean up all remaining props
store.shutdown();
```
### Single Action That Toggles
Combine start/stop in one action:
```dart
class TogglePollingAction extends ReduxAction<AppState> {
final bool start;
TogglePollingAction(this.start);
@override
AppState? reduce() {
if (start) {
setProp('polling', Timer.periodic(
Duration(seconds: 5),
(_) => dispatch(RefreshDataAction()),
));
} else {
disposeProp('polling');
}
return null;
}
}
```
## Complete Example: Real-Time Price Updates
```dart
// State
class AppState {
final double price;
final bool isStreaming;
AppState({required this.price, required this.isStreaming});
static AppState initialState() => AppState(price: 0.0, isStreaming: false);
AppState copy({double? price, bool? isStreaming}) => AppState(
price: price ?? this.price,
isStreaming: isStreaming ?? this.isStreaming,
);
}
// Start streaming prices
class StartPriceStreamAction extends ReduxAction<AppState> {
@override
AppState? reduce() {
// Don't start if already streaming
if (state.isStreaming) return null;
final subscription = priceService.priceStream.listen(
(price) => dispatch(UpdatePriceAction(price)),
onError: (e) => dispatch(PriceStreamErrorAction(e)),
);
setProp('priceSubscription', subscription);
return state.copy(isStreaming: true);
}
}
// Stop streaming prices
class StopPriceStreamAction extends ReduxAction<AppState> {
@override
AppState? reduce() {
if (!state.isStreaming) return null;
disposeProp('priceSubscription');
return state.copy(isStreaming: false);
}
}
// Handle price updates
class UpdatePriceAction extends ReduxAction<AppState> {
final double price;
UpdatePriceAction(this.price);
@override
AppState? reduce() => state.copy(price: price);
}
// Handle stream errors
class PriceStreamErrorAction extends ReduxAction<AppState> {
final Object error;
PriceStreamErrorAction(this.error);
@override
AppState? reduce() {
// Stop streaming on error
disposeProp('priceSubscription');
return state.copy(isStreaming: false);
}
}
```
## Testing onInit/onDispose
Use `ConnectorTester` to test lifecycle callbacks without full widget tests:
```dart
test('starts and stops polling on screen lifecycle', () async {
var store = Store<AppState>(initialState: AppState.initialState());
var connectorTester = store.getConnectorTester(PriceScreen());
// Simulate screen entering view
connectorTester.runOnInit();
var startAction = await store.waitAnyActionTypeFinishes([StartPriceStreamAction]);
expect(store.state.isStreaming, true);
// Simulate screen leaving view
connectorTester.runOnDispose();
var stopAction = await store.waitAnyActionTypeFinishes([StopPriceStreamAction]);
expect(store.state.isStreaming, false);
});
```
## Cleanup on Store Shutdown
Call `disposeProps()` before shutting down the store to clean up all remaining timers and stream subscriptions:
```dart
// Clean up all Timer, Future, and Stream-related props
store.disposeProps();
// Shut down the store
store.shutdown();
```
The `disposeProps()` method automatically:
- Cancels `Timer` objects
- Cancels `StreamSubscription` objects
- Closes `StreamController` and `StreamSink` objects
- Ignores `Future` objects (to prevent unhandled errors)
Regular (non-disposable) props are kept unless you provide a predicate that matches them.
## References
URLs from the documentation:
- https://asyncredux.com/flutter/miscellaneous/streams-and-timers
- https://asyncredux.com/flutter/basics/store
- https://asyncredux.com/flutter/advanced-actions/redux-action
- https://asyncredux.com/flutter/testing/testing-oninit-ondispose
- https://asyncredux.com/flutter/miscellaneous/dependency-injectionRelated Skills
asyncredux-wait-fail-succeed
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
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
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
Creates AsyncRedux (Flutter) synchronous actions that update state immediately by implementing reduce() to return a new state.
asyncredux-state-access
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
Initialize, setup and configure AsyncRedux in a Flutter app. Use it whenever starting a new AsyncRedux project, or when the user requests.
asyncredux-selectors
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
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
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
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
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.
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.