jolt 0.0.5 copy "jolt: ^0.0.5" to clipboard
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 manual notify() 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 EffectScope for automatic cleanup of multiple effects
  • Disposed signals throw AssertionError when accessed

License #

This project is licensed under the MIT License - see the LICENSE file for details.

1
likes
160
points
148
downloads

Publisher

unverified uploader

Weekly Downloads

A lightweight reactive state management library built on alien_signals, focused on simple and efficient state handling.

Repository (GitHub)
View/report issues

Topics

#jolt #state-management #signals

Documentation

API reference

License

MIT (license)

Dependencies

meta, shared_interfaces

More

Packages that depend on jolt