jolt 0.0.5
jolt: ^0.0.5 copied to clipboard
A lightweight reactive state management library built on alien_signals, focused on simple and efficient state handling.
Jolt #
Jolt is a lightweight reactive state management library for Dart and Flutter.
It offers signals, computed values, effects, async states, and reactive collections.
Features #
- ๐ High Performance: Built on alien_signals for optimal performance
- ๐ฏ Fine-Grained Reactivity: Precise dependency tracking and updates
- ๐ฆ Collection Support: Reactive List, Map, Set, and Iterable
- ๐ Async Operations: Seamless Future and Stream handling
- ๐ ๏ธ Practical Utilities: Type conversion and persistence helpers
- ๐จ Framework Agnostic: Works with any Dart/Flutter application
- ๐งน Memory Efficient: Automatic cleanup and disposal
- ๐ง Rich Extensions: Comprehensive extension methods
Jolt provides a complete reactive programming solution with signals, computed values, effects, and reactive collections. It's designed for building responsive applications with automatic dependency tracking and efficient updates.
For full API reference and detailed usage examples, check the inline comments in the source code and the Documentation.
Quick Start #
import 'package:jolt/jolt.dart';
void main() {
// Create reactive state
final count = Signal(0);
final doubled = Computed(() => count.value * 2);
// React to changes
Effect(() {
print('Count: ${count.value}, Doubled: ${doubled.value}');
});
count.value = 5; // Prints: "Count: 5, Doubled: 10"
}
Core Features #
Signals #
Reactive containers that hold values and notify subscribers when changed:
final counter = Signal(0);
counter.value = 1; // Automatically notifies subscribers
// Access without creating dependency
final currentValue = counter.peek;
// Force notification without changing value
counter.notify();
Computed Values #
Derived values that update automatically when dependencies change:
final firstName = Signal('John');
final lastName = Signal('Doe');
final fullName = Computed(() => '${firstName.value} ${lastName.value}');
// Computed values are cached and only recompute when dependencies change
print(fullName.value); // "John Doe"
firstName.value = 'Jane';
print(fullName.value); // "Jane Doe" - automatically updated
Effects #
Side-effect functions that run when their dependencies change:
final count = Signal(0);
final List<int> values = [];
Effect(() {
values.add(count.value); // Creates dependency on count
});
count.value = 1; // Effect runs again
count.value = 2; // Effect runs again
// values = [0, 1, 2]
Async Operations #
Handle asynchronous operations with built-in state management:
// From Future
final userSignal = AsyncSignal.fromFuture(fetchUser());
// From Stream
final dataSignal = AsyncSignal.fromStream(dataStream);
// Handle states
Effect(() {
final state = userSignal.value;
if (state.isLoading) print('Loading...');
if (state.isSuccess) print('User: ${state.data}');
if (state.isError) print('Error: ${state.error}');
});
// Using map for cleaner code
final displayText = Computed(() =>
userSignal.value.map(
loading: () => 'Loading...',
success: (data) => 'User: $data',
error: (error, stackTrace) => 'Error: $error',
) ?? 'Unknown'
);
Reactive Collections #
Work with reactive lists, sets, and maps:
final items = ListSignal(['apple', 'banana']);
final tags = SetSignal({'dart', 'flutter'});
final userMap = MapSignal({'name': 'Alice', 'age': 30});
// All mutations trigger reactive updates automatically
items.add('cherry');
items.insert(0, 'orange');
items.removeAt(1);
tags.add('reactive');
tags.remove('dart');
userMap['city'] = 'New York';
userMap.remove('age');
// Direct collection access
final list = items.value; // Get underlying List
items.value = ['new', 'list']; // Replace entire collection
Batching Updates #
Batch multiple updates to prevent intermediate notifications:
final signal1 = Signal(1);
final signal2 = Signal(2);
final List<int> values = [];
Effect(() {
values.add(signal1.value + signal2.value);
});
// Without batching: values = [3, 12, 30] (3 updates)
signal1.value = 10;
signal2.value = 20;
// With batching: values = [3, 30] (2 updates)
batch(() {
signal1.value = 10;
signal2.value = 20;
});
Untracked Access #
Access values without creating dependencies:
final signal1 = Signal(1);
final signal2 = Signal(2);
Effect(() {
final tracked = signal1.value; // Creates dependency
final untracked = untracked(() => signal2.value); // No dependency
print('$tracked + $untracked');
});
signal1.value = 10; // Effect runs
signal2.value = 20; // Effect doesn't run
Effect Scopes #
Manage effect lifecycles with automatic cleanup:
final scope = EffectScope((scope) {
Effect(() => print('Reactive effect'));
// Effects are automatically disposed when scope is disposed
});
scope.dispose(); // Cleans up all effects in scope
Watchers #
Watch for changes without creating effects:
final signal = Signal(0);
final List<int> changes = [];
Watcher(
() => signal.value, // Source function
(value, previousValue) {
changes.add(value);
print('Changed from $previousValue to $value');
},
immediately: true, // Run immediately
);
Extension Methods #
Convert existing types to reactive equivalents:
// Convert collections
final reactiveList = [1, 2, 3].toListSignal();
final reactiveMap = {'key': 'value'}.toMapSignal();
final reactiveSet = {1, 2, 3}.toSetSignal();
// Convert async sources
final asyncSignal = someFuture.toAsyncSignal();
final streamSignal = someStream.toStreamSignal();
Advanced Features #
Type-Converting Signals #
Create signals that convert between different types:
final count = Signal(0);
final textCount = ConvertComputed(
count,
decode: (int value) => value.toString(),
encode: (String value) => int.parse(value),
);
textCount.value = "42"; // Updates count to 42
print(count.value); // 42
Persistent Signals #
Signals that automatically persist to storage:
final theme = PersistSignal(
initialValue: () => 'light',
read: () => SharedPreferences.getInstance()
.then((prefs) => prefs.getString('theme') ?? 'light'),
write: (value) => SharedPreferences.getInstance()
.then((prefs) => prefs.setString('theme', value)),
lazy: false, // Load immediately
writeDelay: Duration(milliseconds: 100), // Debounce writes
);
theme.value = 'dark'; // Automatically saved to storage
Important Notes #
Collection Signals #
- Collection signals automatically notify on mutations
- Direct collection access (
signal.value.add()) requires manualnotify()call - Use collection methods (
signal.add()) for automatic notifications
Async Signals #
- Use
AsyncSignal.fromFuture()for single async operations - Use
AsyncSignal.fromStream()for continuous data streams - Always handle all states: loading, success, error, refreshing
Effect Dependencies #
- Effects only re-run when tracked dependencies change
- Use
untracked()to access values without creating dependencies - Effects run immediately when created
Memory Management #
- Always dispose signals and effects when no longer needed
- Use
EffectScopefor automatic cleanup of multiple effects - Disposed signals throw
AssertionErrorwhen accessed
License #
This project is licensed under the MIT License - see the LICENSE file for details.