impulse 0.1.0
impulse: ^0.1.0 copied to clipboard
Minimalist state management and dependency injection.
Impulse #
Easy and simple state management solution that mainly functions as a dependency injection service and integrates well with other state management solutions.
Impulse provides a lightweight way to manage shared state and dependencies using a central Store and type-safe References.
The package is currently being implemented in some production-level code to validate its real-world use. It will hit
1.0after this is complete.
Features #
- Type-safe Dependency Injection: Define your objects and their dependencies using various reference types.
- Singleton & Factory Support: Cache objects globally or create fresh instances every time.
- Parameterized Dependencies: Use
FamilyRefto create dependencies based on external arguments. - Reactivity Integration: Built-in support for
Listenableobjects and custom reactivity delegates. - Lifecycle Management: Automatic disposal of
Disposableobjects and custom disposal logic.
Getting Started #
Add impulse to your pubspec.yaml:
dependencies:
impulse: latest
Core Concepts #
1. The Store #
The Store is the central container for all your state objects. You can use the global $store or create your own instances.
import 'package:impulse/impulse.dart';
// Use the global store
$store
// Or create your own
final myStore = Store();
I prefer to prefix anything related to global state with
$.
2. References #
References define how an object is created.
Ref<T> (Singleton)
Creates a single instance that is cached in the store. Subsequent reads return the same instance.
final authServiceRef = Ref((store) => AuthService());
// Access it anywhere
final auth = authServiceRef.get($store);
FactoryRef<T> (Factory)
Creates a new instance every time it is retrieved.
final uuidRef = FactoryRef((store) => Uuid().v4());
final id1 = uuidRef.get($store);
final id2 = uuidRef.get($store); // id1 != id2
FamilyRef<T, R> (Parameterized)
Creates a unique instance for each unique argument provided.
final userProfileRef = FamilyRef<UserProfile, String>((store, userId) {
return UserProfile(userId);
});
final userA = userProfileRef.get(store, 'A');
final userA_again = userProfileRef.get(store, 'A'); // Same instance
final userB = $store.get(userProfileRef('B')); // New instance
3. Reactivity & Watching #
Impulse can watch for changes in your objects. If an object implements ImpulseListenable (like ValueNotifier), the watch method will trigger whenever it notifies.
final counterRef = Ref((store) => ValueNotifier(0));
final unwatch = $store.watch(counterRef(), (notifier) {
print('Counter changed to: ${notifier.value}');
});
// Later, to stop watching:
unwatch();
4. Lifecycle & Disposal #
Objects that implement Disposable are automatically disposed of when they are dropped from the store or when the store is reset. You can also provide a custom dispose callback in the reference definition.
final databaseRef = Ref(
(store) => Database(),
dispose: (store, db) => db.close(),
);
To remove an object from the store, you can use drop:
store.drop(myRef());
5. Testing #
When testing dependency injection, you usually want to swap out dependencies on the fly. To do this, we can use store.override.
main(){
test((){
$store.override(someRef, (store) => MyMock());
// To go back to the original constructor:
$store.removeOverride(someRef);
})
}
Advanced Usage #
Pluggable Reactivity Adapters #
Impulse uses a pluggable adapter system to handle how different types of objects are bound and disposed. By default, it supports ImpulseListenable (like ValueNotifier) and Disposable.
You can easily extend Impulse to support other patterns (like BLoC, Streams, or custom state types) by adding a ReactivityAdapter.
Example: BLoC Integration
class BlocAdapter implements ReactivityAdapter {
@override
void Function()? onBind(dynamic value, void Function() notify) {
if (value is Bloc) {
// Listen to the bloc's state changes
final subscription = value.stream.listen((_) => notify());
return () => subscription.cancel();
}
return null;
}
@override
void onDispose(Store store, dynamic value) {
if (value is Bloc) {
// Automatically close the bloc when it's dropped from the store
value.close();
}
}
}
// Register the adapter on the store
$store.reactivity.addAdapter(BlocAdapter());
Once registered, any reference that produces a Bloc will automatically be "watched" and "disposed" correctly by the store.
Scoping References #
To have a reference automatically be cleaned up after an operation, this package adds a helper function called withScope.
withScope(
() async {
// Perform operation
},
store: $store,
refs: [
refA(),
refB(),
refC(),
]
)
This runs the operation and cleans up all the refs after it is done. If there are multiple scopes using the same ref, it is cleaned up after the last scope stops using it.
See also #
- impulse_flutter for a flutter integration using this package.
- impulse_signals for a signals addon to this package.
- API reference for a detailed description of all API points.
License #
This project is licensed under the MIT License.