Just Signals
A high-performance signal-driven state management package for just_game_engine. Designed for 60 FPS game loops with zero garbage collection pressure.
Features
- Core Signals: Reactive primitives (
Signal,Computed,Effect) with surgical precision updates - Memory Layer: Zero-GC memory pooling with typed arrays (
MemoryArena,ObjectPool) - Flutter Widgets: Efficient rebuilding with
SignalBuilderandSignalConsumer - Async Support:
AsyncSignal,StreamSignal, andFutureSignalfor async operations
Installation
Add to your pubspec.yaml:
dependencies:
just_signals: ^1.0.2
Quick Start
Basic Signal Usage
import 'package:just_signals/just_signals.dart';
// Create a signal
final count = Signal(0);
// Read value (registers dependency in computed/effect context)
print(count.value); // 0
// Update value (notifies listeners)
count.value++;
// Update with function
count.update((c) => c * 2);
Computed Values
final firstName = Signal('John');
final lastName = Signal('Doe');
// Computed automatically tracks dependencies
final fullName = Computed(() => '${firstName.value} ${lastName.value}');
print(fullName.value); // 'John Doe'
firstName.value = 'Jane';
print(fullName.value); // 'Jane Doe' (automatically recomputed)
Effects (Side Effects)
final count = Signal(0);
// Effect runs when dependencies change
final effect = Effect(() {
print('Count changed to: ${count.value}');
return () => print('Cleanup'); // Optional cleanup
});
count.value = 1; // Prints: "Cleanup" then "Count changed to: 1"
effect.dispose(); // Prints: "Cleanup"
Batching Updates
final x = Signal(0);
final y = Signal(0);
// Batch multiple updates - single notification at the end
batch(() {
x.value = 10;
y.value = 20;
});
Flutter Widgets
SignalBuilder
Rebuilds only when the signal changes:
SignalBuilder<int>(
signal: count,
builder: (context, value, child) => Text('Count: $value'),
);
SignalConsumer (Multiple Signals)
SignalConsumer(
signals: [firstName, lastName],
builder: (context, child) => Text('${firstName.value} ${lastName.value}'),
);
SignalSelector (Partial Rebuilds)
Only rebuilds when the selected portion changes:
SignalSelector<User, String>(
signal: userSignal,
selector: (user) => user.name,
builder: (context, name, child) => Text(name),
);
Memory Layer (Zero-GC)
MemoryArena
Pre-allocated typed arrays for entity data:
// Pre-allocate space for 1000 entities
final arena = MemoryArena(capacity: 1000);
// Allocate a slot
final slot = arena.allocate();
// Modify in place - no allocations!
arena.setPosition(slot, 100.0, 200.0);
arena.setVelocity(slot, 50.0, -30.0);
// Apply physics
arena.applyVelocity(slot, deltaTime);
// Free when done
arena.free(slot);
ObjectPool
Reuse objects to avoid GC:
final bulletPool = ObjectPool<Bullet>(
create: () => Bullet(),
reset: (b) => b.reset(),
initialSize: 100, // Pre-warm
);
// Acquire from pool
final bullet = bulletPool.acquire();
// Use bullet...
// Return to pool
bulletPool.release(bullet);
ECS Integration
The reactive ECS classes — ComponentSignal, EntitySignal, WorldSignal,
ReactiveSystem, and ReactiveComponent — are provided by the just_game_engine
package, which depends on just_signals. Import them from there:
import 'package:just_game_engine/just_game_engine.dart';
ComponentSignal
Reactive access to component properties:
final transform = entity.getComponent<TransformComponent>()!;
final transformSignals = TransformSignals(transform);
// Reactive position updates
SignalBuilder<double>(
signal: transformSignals.x,
builder: (_, x, __) => Text('X: $x'),
);
// Update triggers rebuild
transformSignals.x.value = 100;
WorldSignal
Track entity and system changes:
final worldSignal = world.toSignal();
// React to entity count
SignalBuilder<int>(
signal: worldSignal.entityCount,
builder: (_, count, __) => Text('Entities: $count'),
);
// Query entities reactively
final players = worldSignal.query([TransformComponent, TagComponent]);
ReactiveSystem
Only process entities with dirty components:
class PlayerMovementSystem extends ReactiveSystem {
@override
List<Type> get requiredComponents => [TransformComponent, VelocityComponent];
@override
void processEntity(Entity entity, double deltaTime) {
final transform = entity.getComponent<TransformComponent>()!;
final velocity = entity.getComponent<VelocityComponent>()!;
transform.translate(velocity.velocity * deltaTime);
}
}
Async Support
AsyncSignal
Handles loading/error/data states:
final userSignal = AsyncSignal<User>();
// Load data
await userSignal.load(() => api.fetchUser(userId));
// Use in widget
SignalBuilder(
signal: userSignal,
builder: (_, snapshot, __) {
if (snapshot.isLoading) return CircularProgressIndicator();
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
return Text('Hello ${snapshot.data!.name}');
},
);
StreamSignal
Wraps streams with proper lifecycle:
final messages = StreamSignal(messageStream);
SignalBuilder(
signal: messages,
builder: (_, snapshot, __) => Text(snapshot.data?.content ?? ''),
);
Best Practices
- Batch related updates to minimize rebuilds
- Use selectors when you only need part of a signal's data
- Pre-allocate arenas during level loading, not during gameplay
- Use ReactiveSystem for entities that don't change every frame
- Dispose signals when no longer needed to prevent memory leaks
Architecture Overview
┌────────────────────────────────────────────────────────┐
│ Flutter UI Layer │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │SignalBuilder│ │SignalConsumer│ │ SignalSelector │ │
│ └──────┬──────┘ └──────┬───────┘ └────────┬────────┘ │
└─────────┼───────────────┼──────────────────┼───────────┘
│ │ │
┌─────────┴───────────────┴──────────────────┴───────────┐
│ just_signals — Signals Layer │
│ ┌──────┐ ┌────────┐ ┌──────┐ ┌───────────────┐ │
│ │Signal│ │Computed│ │Effect│ │ AsyncSignal │ │
│ └──┬───┘ └───┬────┘ └──┬───┘ └───────────────┘ │
└─────┼───────────┼───────────┼──────────────────────────┘
│ │ │
┌─────┴───────────┴───────────┴──────────────────────────┐
│ just_game_engine — ECS Integration │
│ ┌───────────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ComponentSignal│ │EntitySignal│ │ReactiveSystem│ │
│ └───────────────┘ └────────────┘ └──────────────┘ │
└────────────────────────────────────────────────────────┘
│
┌─────┴──────────────────────────────────────────────────┐
│ just_signals — Memory Layer (Zero-GC) │
│ ┌───────────┐ ┌──────────┐ ┌───────────────┐ │
│ │MemoryArena│ │ObjectPool│ │ PoolManager │ │
│ └───────────┘ └──────────┘ └───────────────┘ │
└────────────────────────────────────────────────────────┘
License
BSD 3-Clause License - see LICENSE file for details.
Libraries
- just_signals
- A high-performance signal-driven state management library for Flutter.