jolt_surge 2.0.0-beta.2 copy "jolt_surge: ^2.0.0-beta.2" to clipboard
jolt_surge: ^2.0.0-beta.2 copied to clipboard

A lightweight reactive state container for Flutter, built on Jolt Signals and inspired by BLoC's Cubit architecture.

Jolt Surge #

CI/CD codecov jolt_surge License: MIT

A lightweight, signal-driven state management library for Flutter built on top of Jolt Signals. Jolt Surge provides a predictable state container pattern inspired by BLoC's Cubit, with fine-grained rebuild control, composable listeners, and selector-based rendering. Surge combines the simplicity and predictability of the Cubit pattern with the reactive capabilities of Jolt Signals, leveraging automatic dependency tracking to build highly efficient Flutter applications with minimal boilerplate.

Quick Start #

Define a Surge #

import 'package:jolt_surge/jolt_surge.dart';

class CounterSurge extends Surge<int> {
  CounterSurge() : super(0);

  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);

  @override
  void onChange(Change<int> change) {
    // optional: observe transitions
  }
}

Provide and consume #

SurgeProvider<CounterSurge>(
  create: (_) => CounterSurge(), // auto-disposed on unmount
  child: SurgeBuilder<CounterSurge, int>(
    builder: (context, state, surge) => Text('count: $state'),
  ),
);

Using .value (you manage lifecycle):

final surge = CounterSurge();

SurgeProvider<CounterSurge>.value(
  value: surge, // not auto-disposed
  child: SurgeBuilder<CounterSurge, int>(
    builder: (context, state, s) => Text('count: $state'),
  ),
);

Actions (emit state) #

ElevatedButton(
  onPressed: () => context.read<CounterSurge>().increment(),
  child: const Text('Increment'),
);

Core Concepts #

Surge #

A reactive state container that manages state through Jolt Signals. It provides:

  • state: Get the current state value (reactive, tracked)
  • emit(next): Emit a new state value
  • dispose(): Clean up resources
  • onChange(): Hook for observing state transitions

Widgets #

  • SurgeProvider: Provides a Surge instance to the widget tree via create or .value constructors
  • SurgeConsumer: Unified widget providing both builder and listener functionality with conditional controls
  • SurgeBuilder: Convenience widget for builder-only functionality
  • SurgeListener: Convenience widget for listener-only functionality (side effects)
  • SurgeSelector: Fine-grained rebuild control using selector functions

Tracking Semantics #

Understanding tracking behavior is crucial for optimal performance:

  • Non-tracked (untracked): builder and listener functions are executed within an untracked context, preventing unnecessary reactive dependencies
  • Tracked by default: buildWhen, listenWhen, and selector functions are tracked by default, allowing them to depend on external signals
  • Opt-out: To disable tracking, wrap your reads with untracked(() => ...) or use peek property

Comparison with Cubit #

Jolt Surge's API is ~90% similar to BLoC's Cubit, making it easy to migrate between them. If you're familiar with Cubit, you can seamlessly switch to Surge and experience a signal-powered version of Cubit. However, there are key differences that leverage Jolt's reactive capabilities:

Similarities #

Feature Cubit Surge
State container Cubit<State> Surge<State>
State access state getter state getter
State emission emit(State) emit(State)
Lifecycle hook onChange(Change) onChange(Change)
Disposal close() dispose()
Provider pattern BlocProvider SurgeProvider
Builder widget BlocBuilder SurgeBuilder
Listener widget BlocListener SurgeListener
Conditional rebuild buildWhen buildWhen

Key Differences #

  1. Reactive Foundation

    • Cubit: Built on Stream, requires explicit subscription management
    • Surge: Built on Jolt Signals, automatic dependency tracking and reactive updates
  2. State Access

    • Cubit: state is a simple getter, no automatic dependency tracking
    • Surge: state is reactive and tracked, automatically creates dependencies in Effects and Computed
  3. Signal Integration

    • Cubit: Limited ability to integrate with other reactive systems
    • Surge: Can depend on external Jolt signals in buildWhen, listenWhen, and selector functions
  4. Performance Optimizations

    • Cubit: Relies on Stream-based updates
    • Surge: Leverages Jolt's fine-grained dependency tracking for optimal rebuilds
  5. Widget Callback APIs: Surge Instance Access

    • Cubit: Widget callbacks (builder, listener, selector) only receive (context, state) - only state is accessible
    • Surge: Widget callbacks receive (context, state, surge) or (state, surge) - both state and Surge instance are accessible

    Note: If you prefer the Cubit-style API and don't need the Surge instance, you can simply ignore it by using _ as the parameter name: (context, state, _) or (state, _).

    This difference is crucial for derived values. In Cubit, when you need computed values based on state, you have two options:

    • Cache the value manually
    • Write a getter function that recalculates on every access

    With Surge, since it's built on Jolt Signals, you can define Computed properties directly in your Surge class. These computed values are automatically cached and only recompute when their dependencies change. By passing the Surge instance to callbacks, you can access these computed properties directly:

    // Cubit approach - manual caching or getter
    class CounterCubit extends Cubit<int> {
      CounterCubit() : super(0);
      void increment() => emit(state + 1);
         
      // Option 1: Manual cache
      int? _doubledCache;
      int get doubled {
        _doubledCache ??= state * 2;
        return _doubledCache!;
      }
         
      // Option 2: Getter (recalculates every time)
      int get doubled => state * 2;
    }
       
    BlocBuilder<CounterCubit, int>(
      builder: (context, state) {
        // Can only access state, not the cubit instance
        // Must use context.read<CounterCubit>() to access getters
        final cubit = context.read<CounterCubit>();
        return Text('Doubled: ${cubit.doubled}');
      },
    )
       
    // Surge approach - reactive computed
    class CounterSurge extends Surge<int> {
      CounterSurge() : super(0);
      void increment() => emit(state + 1);
         
      // Computed automatically caches and only recomputes when state changes
      late final doubled = Computed(() => state * 2);
    }
       
    // All Surge widgets pass the surge instance
    SurgeBuilder<CounterSurge, int>(
      builder: (context, state, surge) {
        // Direct access to computed properties via surge instance
        return Text('Doubled: ${surge.doubled.value}');
      },
    )
       
    SurgeSelector<CounterSurge, int, String>(
      selector: (state, surge) => surge.doubled.value.toString(),
      builder: (context, selected, surge) => Text(selected),
    )
       
    // If you prefer Cubit-style API, simply ignore the surge parameter
    SurgeBuilder<CounterSurge, int>(
      builder: (context, state, _) {
        // Works just like BlocBuilder - only using state
        return Text('Count: $state');
      },
    )
    

    This design allows you to encapsulate derived state logic within the Surge class, making it more maintainable and leveraging Jolt's reactive system for optimal performance. The Surge instance in callbacks provides a clean, type-safe way to access these computed values without needing to look up the instance from context. If you don't need computed properties, you can simply ignore the surge parameter with _ to maintain the familiar Cubit-style API.

Code Example #

The API is nearly identical, making it easy to switch between Cubit and Surge:

// Cubit
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);
  void increment() => emit(state + 1);
}

// Surge (signal-powered Cubit)
class CounterSurge extends Surge<int> {
  CounterSurge() : super(0);
  void increment() => emit(state + 1);
}

The main difference is the underlying reactive system: Cubit uses Streams, while Surge uses Jolt Signals, providing automatic dependency tracking and better performance optimizations. You can easily migrate between them and experience the benefits of signal-based state management.

Widgets #

SurgeConsumer #

  • builder: non-tracked UI build.
  • listener: non-tracked side effect (runs when effect recomputes).
  • buildWhen(prev, next, surge): tracked condition for rebuilding.
  • listenWhen(prev, next, surge): tracked condition for listener.
SurgeConsumer<CounterSurge, int>(
  buildWhen: (prev, next, s) => next.isEven, // tracked
  listenWhen: (prev, next, s) => next > prev, // tracked
  builder: (context, state, s) => Text('count: $state'),
  listener: (context, state, s) {
    // e.g., SnackBar or analytics
  },
);

Disable tracking for a condition:

buildWhen: (prev, next, s) => untracked(() => shouldRebuildSignal.value),

SurgeBuilder #

SurgeBuilder<CounterSurge, int>(
  builder: (context, state, s) => Text('count: $state'),
);

SurgeListener #

SurgeListener<CounterSurge, int>(
  listener: (context, state, s) {
    // side-effect only
  },
  child: const SizedBox.shrink(),
);

SurgeSelector #

Rebuild only when the selected value changes by equality.

SurgeSelector<CounterSurge, int, String>(
  selector: (state, s) => state.isEven ? 'even' : 'odd', // tracked by default
  builder: (context, selected, s) => Text(selected),
);

Disable selector tracking:

selector: (state, s) => untracked(() => externalSignal.valueAsLabel(state)),

Advanced Usage #

Custom State Creator #

By default, Surge uses Signal to store state. You can customize the state storage mechanism using the creator parameter:

class CustomSurge extends Surge<int> {
  CustomSurge() : super(
    0,
    creator: (state) => WritableComputed(
      () => baseSignal.value,
      (value) => baseSignal.value = value,
    ),
  );
}

This is useful when you need to derive state from other signals or implement custom reactive behavior.

SurgeObserver #

Monitor Surge lifecycle events globally using SurgeObserver:

class MyObserver extends SurgeObserver {
  @override
  void onCreate(Surge surge) {
    print('Surge created: $surge');
  }

  @override
  void onChange(Surge surge, Change change) {
    print('State changed: ${change.currentState} -> ${change.nextState}');
  }

  @override
  void onDispose(Surge surge) {
    print('Surge disposed: $surge');
  }
}

// Set global observer
SurgeObserver.observer = MyObserver();

Jolt Surge is part of the Jolt ecosystem. Explore these related packages:

Package Description
jolt Core library providing Signals, Computed, Effects, and reactive collections
jolt_flutter Flutter widgets: JoltBuilder, JoltSelector, JoltProvider
jolt_hooks Hooks API: useSignal, useComputed, useJoltEffect, useJoltWidget

Acknowledgments #

Jolt Surge is inspired by the Cubit pattern from the BLoC library. We extend our gratitude to the BLoC team for their excellent design patterns and architectural insights that have influenced the development of this library.

License #

This project is part of the Jolt ecosystem. See individual package licenses for details.

2
likes
0
points
340
downloads

Publisher

verified publishervowdemon.com

Weekly Downloads

A lightweight reactive state container for Flutter, built on Jolt Signals and inspired by BLoC's Cubit architecture.

Homepage
Repository (GitHub)
View/report issues

Topics

#jolt #state-management #signals #surge

License

unknown (license)

Dependencies

flutter, jolt, provider, shared_interfaces

More

Packages that depend on jolt_surge