jolt_setup 3.2.3 copy "jolt_setup: ^3.2.3" to clipboard
jolt_setup: ^3.2.3 copied to clipboard

Setup Widget API and Flutter hooks for building reactive widgets with Jolt signals, featuring automatic resource cleanup and lifecycle management.

Jolt Setup #

CI/CD codecov jolt_setup License: MIT

Setup Widget API and Flutter hooks for Jolt. Provides a composition API similar to Vue's Composition API for building Flutter widgets, along with hooks for managing Flutter resources such as controllers, focus nodes, and lifecycle states with automatic cleanup.

Why Setup Widget? #

Setup Widget uses a composition-style API for Flutter widgets. It runs setup once at creation time and handles hook cleanup automatically.

Key Features #

  • Composition-based logic
  • Automatic resource cleanup
  • setup runs once instead of on every rebuild
  • Built on Jolt signals
  • Hook APIs for controllers, focus nodes, animations, and lifecycle
  • Works with SetupWidget, SetupMixin, and SetupBuilder

Comparison #

The example below shows the same widget implemented with SetupWidget and with a StatefulWidget.

With Setup Widget:

class HookExample extends SetupWidget<HookExample> {
  HookExample({super.key});

  @override
  setup(context, props) {
    useAutomaticKeepAlive(true);

    final scrollController = useScrollController();
    useListenable(scrollController, () {
      print('scrollController.offset: ${scrollController.offset}');
    });

    final loadingFuture =
        useFuture(Future.delayed(Duration(seconds: 3), () => true));

    useAppLifecycleState(
      onChange: (state) {
        if (state == AppLifecycleState.resumed) {
          print('app resumed');
        } else if (state == AppLifecycleState.paused) {
          print('app paused');
        }
      },
    );

    return () => SingleChildScrollView(
        controller: scrollController,
        child: switch (loadingFuture.hasData) {
          false => Center(child: CircularProgressIndicator()),
          true => Column(
              children: [
                for (var i = 0; i < 100; i++) Text('Item $i'),
              ],
            ),
        });
  }
}

Traditional StatefulWidget:

class NormalExample extends StatefulWidget {
  const NormalExample({super.key});

  @override
  State<NormalExample> createState() => _NormalExampleState();
}

class _NormalExampleState extends State<NormalExample>
    with AutomaticKeepAliveClientMixin, WidgetsBindingObserver, RouteAware {
  late final ScrollController scrollController;
  late final Future<bool> loadingFuture;

  @override
  bool get wantKeepAlive => true;

  @override
  void initState() {
    super.initState();
    scrollController = ScrollController();
    loadingFuture = Future.delayed(Duration(seconds: 3), () => true);
    scrollController.addListener(_listener);
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      print('app resumed');
    } else if (state == AppLifecycleState.paused) {
      print('app paused');
    }
  }

  @override
  void dispose() {
    scrollController.removeListener(_listener);
    scrollController.dispose();
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  void _listener() {
    print('scrollController.offset: ${scrollController.offset}');
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return SingleChildScrollView(
        controller: scrollController,
        child: FutureBuilder(
            future: loadingFuture,
            builder: (context, snapshot) {
              return switch (snapshot.hasData) {
                false => Center(child: CircularProgressIndicator()),
                true => Column(
                    children: [
                      for (var i = 0; i < 100; i++) Text('Item $i'),
                    ],
                  ),
              };
            }));
  }
}

Differences in this example:

  • Fewer lifecycle methods
  • No manual listener disposal in the widget code
  • Logic is grouped inside setup

Quick Start #

import 'package:flutter/material.dart';
import 'package:jolt_setup/jolt_setup.dart';

class MyWidget extends SetupWidget {
  @override
  setup(context) {
    final textController = useTextEditingController('Hello');
    final focusNode = useFocusNode();
    final count = useSignal(0);
    
    return () => Scaffold(
      body: Column(
        children: [
          TextField(
            controller: textController,
            focusNode: focusNode,
          ),
          Text('Count: ${count.value}'),
          ElevatedButton(
            onPressed: () => count.value++,
            child: Text('Increment'),
          ),
        ],
      ),
    );
  }
}

Use with jolt_lint #

jolt_lint adds static checks and assists for setup and hook usage:

# analysis_options.yaml

plugins:
  jolt_lint: ^3.0.0

jolt_lint includes:

  • Hook rule checks
  • Compile-time diagnostics for invalid async/callback hook usage
  • Code assists for common conversions

Without jolt_lint, some hook placement errors are only detected at runtime.

See jolt_lint documentation for setup and configuration.

Setup Widget #

⚠️ Important Note

Setup Widget and its hooks are not part of the flutter_hooks ecosystem. If you need flutter_hooks-compatible APIs, use the jolt_hooks package instead.

Key Execution Difference:

  • Setup Widget: The setup function runs once when the widget is created (like Vue / SolidJS), then rebuilds are driven by the reactive system
  • flutter_hooks: Hook functions run on every build (like React Hooks)

These are different execution models. Mixing them in the same component usually makes hook behavior harder to reason about.

Setup Widget provides a composition API similar to Vue's Composition API for building Flutter widgets. The key difference from React hooks is that setup executes only once when the widget is created, not on every rebuild.

SetupBuilder #

SetupBuilder is the smallest entry point for the API:

import 'package:jolt_setup/setup.dart';

SetupBuilder(
  setup: (context) {
    final count = useSignal(0);
    
    return () => Column(
      children: [
        Text('Count: ${count.value}'),
        ElevatedButton(
          onPressed: () => count.value++,
          child: Text('Click'),
        ),
      ],
    );
  },
)

SetupWidget vs SetupMixin #

Before diving into each API, understand their differences:

Feature SetupWidget SetupMixin
Base class Extends Widget Mixin for State<T>
Mutability Like StatelessWidget, immutable Mutable State class
this reference ❌ Not available ✅ Full access
Instance methods/fields ❌ Should not use ✅ Can define freely
Setup signature setup(context, props) setup(context)
Reactive props access props().property props.property
Non-reactive props access props.peek.property widget.property
Lifecycle methods Via hooks only Both hooks + State methods
Use case Simple immutable widgets Need State capabilities

SetupWidget #

Create custom widgets by extending SetupWidget:

class CounterWidget extends SetupWidget<CounterWidget> {
  final int initialValue;
  
  const CounterWidget({super.key, this.initialValue = 0});

  @override
  setup(context, props) {
    // Use props.peek for one-time initialization (non-reactive)
    final count = useSignal(props.peek.initialValue);
    
    // Use props() for reactive access
    final displayText = useComputed(() => 
      'Count: ${count.value}, Initial: ${props().initialValue}'
    );
    
    return () => Column(
      children: [
        Text(displayText.value),
        ElevatedButton(
          onPressed: () => count.value++,
          child: const Text('Increment'),
        ),
      ],
    );
  }
}

Important Notes:

  • setup receives two parameters:

    • context: Standard Flutter BuildContext
    • props: PropsReadonlyNode<YourWidgetType>, provides reactive access to widget instance
  • Props Access Methods:

    • props() / props.value / props.get() - Reactive access, establishes dependencies
    • props.peek - Non-reactive access, for one-time initialization
  • Like StatelessWidget: The widget class should be immutable and not hold mutable state or define instance methods

SetupMixin #

Add composition API support to existing StatefulWidgets:

class CounterWidget extends StatefulWidget {
  final int initialValue;
  
  const CounterWidget({super.key, this.initialValue = 0});

  @override
  State<CounterWidget> createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget>
    with SetupMixin<CounterWidget> {
  
  @override
  setup(context) {
    // Use widget.property for one-time initialization (non-reactive)
    final count = useSignal(widget.initialValue);
    
    // Use props.property for reactive access
    final displayText = useComputed(() => 
      'Count: ${count.value}, Initial: ${props.initialValue}'
    );
    
    return () => Column(
      children: [
        Text(displayText.value),
        ElevatedButton(
          onPressed: () => count.value++,
          child: const Text('Increment'),
        ),
      ],
    );
  }
}

Key Differences:

  • setup receives only one parameter: context (no props parameter)
  • Provides a props getter for reactive widget property access
  • Compatible with traditional State lifecycle methods (initState, dispose, etc.)

Two Ways to Access Widget Properties:

setup(context) {
  // 1. widget.property - Non-reactive (equivalent to props.peek in SetupWidget)
  //    For one-time initialization, won't trigger updates on changes
  final initial = widget.initialValue;
  
  // 2. props.property - Reactive (equivalent to props() in SetupWidget)
  //    Use inside computed/effects to react to property changes
  final reactive = useComputed(() => props.initialValue * 2);
  
  return () => Text('${reactive.value}');
}

State Context and this Reference:

Unlike SetupWidget (which is analogous to StatelessWidget), SetupMixin runs within a State class, giving you full access to this and mutable state:

class _CounterWidgetState extends State<CounterWidget>
    with SetupMixin<CounterWidget> {
  
  // ✅ Allowed: Define instance fields in State
  final _controller = TextEditingController();
  int _tapCount = 0;
  
  // ✅ Allowed: Define instance methods
  void _handleTap() {
    setState(() => _tapCount++);
  }
  
  @override
  void initState() {
    super.initState();
    // Traditional State initialization
  }
  
  @override
  setup(context) {
    final count = useSignal(0);
    
    // ✅ Access 'this' and instance members
    onMounted(() {
      _controller.text = 'Initial: ${widget.initialValue}';
    });
    
    return () => Column(
      children: [
        TextField(controller: _controller),
        Text('Taps: $_tapCount'),
        ElevatedButton(
          onPressed: _handleTap,
          child: Text('Count: ${count.value}'),
        ),
      ],
    );
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

Key Point: SetupWidget is like StatelessWidget - the widget class itself should be immutable. SetupMixin works within a State class where you can freely use this, define methods, maintain fields, and leverage the full capabilities of stateful widgets.

Choosing the Right Pattern #

SetupWidget, SetupMixin, and standard Flutter widget patterns solve different constraints. Use the one that matches the widget shape and lifecycle needs of the code you are writing.

When to Use SetupWidget:

  • Creating simple, immutable widgets (like StatelessWidget)
  • Want a pure composition-based API
  • No need for instance methods, mutable fields, or this reference
  • All logic can be expressed through reactive hooks

When to Use SetupMixin:

  • Need instance methods, fields, or access to this
  • Need to use existing State mixins, special State base classes, or State extensions
  • Want to combine composition API with imperative logic
  • Need full control over State lifecycle methods (initState, dispose, didUpdateWidget, etc.)
  • Working with complex widget logic that benefits from both approaches

Available Hooks #

Reactive State Hooks #

Setup Widget provides hooks for all Jolt reactive primitives:

Signal(...) vs useSignal(...)

Both can be used inside setup(), but they solve different lifecycle problems:

API What it does Lifecycle behavior
Signal(...) / Computed(...) Directly creates a reactive node You own the lifecycle. If the node becomes unreachable after unmount, GC can eventually reclaim it, but there is no explicit dispose() boundary and hot reload recreates a new instance.
useSignal(...) / useComputed(...) Creates the node through a Setup hook Bound to the SetupWidget / SetupMixin element lifecycle. The hook explicitly disposes the node on unmount or hook replacement, and preserves a stable reference across matching hot reloads.

In practice, useSignal(value) is the hook-managed version of:

final count = useAutoDispose(() => Signal(value));

That explicit disposal still matters even if the widget-local object could eventually be garbage collected. It gives deterministic teardown when the widget leaves the tree, instead of waiting for GC, and it is what makes hook-owned signals hot-reload friendly.

Use useSignal by default for widget-owned state. Reach for Signal(...) directly only when you intentionally want manual lifecycle control.

Reactive State Hooks #

Hook Description
useSignal(initial) Create a reactive Signal
useSignal.lazy<T>() Create a lazy-loaded Signal
useSignal.list(initial) Create a reactive list
useSignal.map(initial) Create a reactive Map
useSignal.set(initial) Create a reactive Set
useSignal.iterable(getter) Create a reactive Iterable
useSignal.async(source) Create an async Signal

Computed Value Hooks #

Hook Description
useComputed(fn) Create a computed value
useComputed.withPrevious(getter) Create a computed value with access to previous value
useComputed.writable(getter, setter) Create a writable computed value
useComputed.writableWithPrevious(getter, setter) Create a writable computed value with access to previous value

Effect Hooks #

Hook Description
useEffect(fn) Create an effect
useEffect.lazy(fn) Create a deferred effect (call run() to start)
useWatcher(sourcesFn, fn) Create a watcher
useWatcher.immediately(...) Create an immediately-executing watcher
useWatcher.once(...) Create a one-time watcher

Lifecycle Hooks #

Hook Description
onMounted(fn) Callback when widget mounts
onUnmounted(fn) Callback when widget unmounts
onDidUpdateWidget(fn) Callback when widget updates
onDidChangeDependencies(fn) Callback when dependencies change
onActivated(fn) Callback when widget activates
onDeactivated(fn) Callback when widget deactivates

Utility Hooks #

Hook Description
useContext() Get BuildContext
useSetupContext() Get JoltSetupContext
useEffectScope() Create an effect scope
useJoltStream(value) Create a stream from reactive value
useUntil(source, predicate) Wait for a reactive value to satisfy a condition
useUntil.when(source, value) Wait for a reactive value to equal a specific value
useUntil.changed(source) Wait for a reactive value to change from its current value
useMemoized(creator, [disposer]) Memoize value with optional cleanup
useAutoDispose(creator) Auto-dispose resource
useHook(hook) Use a custom hook

Creating Custom Hooks #

There are four common ways to create custom hooks:

class Counter {
  Counter({required this.initialValue}) : raw = Signal(initialValue);

  final int initialValue;
  final Signal<int> raw;

  void increment() => raw.value++;
  void decrement() => raw.value--;
  void reset() => raw.value = initialValue;
  int get() => raw.value;
  void set(int value) => raw.value = value;
  void dispose() => raw.dispose();
}

typedef CounterCompositionHook = ({
  Signal<int> counter,
  void Function() increment,
  void Function() decrement,
  void Function() reset,
  int Function() get,
  void Function(int value) set,
});

1. Composition Hook

Build a reusable hook directly from other hooks:

import 'package:jolt_setup/jolt_setup.dart';

@defineHook
CounterCompositionHook useCounterHookWithoutClass([int initialValue = 0]) {
  final counter = useSignal(0);
  void increment() => counter.value++;
  void decrement() => counter.value--;
  void reset() => counter.value = initialValue;
  int get() => counter.value;
  void set(int value) => counter.value = value;
  return (
    counter: counter,
    increment: increment,
    decrement: decrement,
    reset: reset,
    get: get,
    set: set,
  );
}

// Usage in setup
class CounterExample extends SetupWidget<CounterExample> {
  @override
  setup(context, props) {
    final counter = useCounterHookWithoutClass(10);

    return () => Text('Count: ${counter.get()}');
  }
}

2. Composition Hook Generated from Existing Logic

If you already have a reusable state object, you can wrap it with a composition hook and let setup manage disposal:

import 'package:jolt_setup/jolt_setup.dart';

@defineHook
Counter useCounterHookWithoutClass2([int initialValue = 0]) {
  final counter = useMemoized(
    () => Counter(initialValue: initialValue),
    (counter) => counter.dispose(),
  );

  return counter;
}

3. Class-based Hook

For more complex hooks, extend SetupHook directly:

import 'package:jolt_setup/jolt_setup.dart';

@defineHook
CounterHook useCounterHookClass([int initialValue = 0]) =>
    useHook(CounterHook(initialValue: initialValue));

class CounterHook extends SetupHook<CounterHook> {
  final int initialValue;

  CounterHook({required this.initialValue});

  late Signal<int> raw;
  void increment() => raw.value++;
  void decrement() => raw.value--;
  void reset() => raw.value = initialValue;
  int get() => raw.value;
  void set(int value) => raw.value = value;

  @override
  CounterHook build() {
    raw = Signal(initialValue);
    return this;
  }

  @override
  void unmount() => raw.dispose();
}

4. Class-based Hook Generated from an Existing Class

If you already have a reusable class, you can keep that class as the state and wrap it with a hook:

import 'package:jolt_setup/jolt_setup.dart';

@defineHook
Counter useCounterHookClass2([int initialValue = 0]) =>
    useHook(CounterHook2(initialValue: initialValue));

class CounterHook2 extends SetupHook<Counter> {
  final int initialValue;

  CounterHook2({required this.initialValue});

  @override
  Counter build() => Counter(initialValue: initialValue);

  @override
  void unmount() => state.dispose();
}

Using @defineHook for Lint Checking:

The @defineHook annotation is used to indicate that a function is a hook for lint checking purposes. It helps ensure proper hook usage patterns:

@defineHook
CounterHook useCounterHookClass([int initialValue = 0]) =>
    useHook(CounterHook(initialValue: initialValue));

Add @defineHook to custom hook entry points so lint rules can recognize them as hooks.

What useHook does

useHook(...) is the primitive API that registers a SetupHook instance into the current setup context.

  • It lets setup cache and reuse the hook across rebuilds
  • It connects the hook to the setup lifecycle, including mount, unmount, and hot reload reassemble
  • It returns the hook's state

In practice, most custom class-based hooks are just a thin entry function around useHook(...).

@defineHook
CounterHook useCounterHookClass([int initialValue = 0]) =>
    useHook(CounterHook(initialValue: initialValue));

What build() and state mean in class-based hooks

In a SetupHook<T>:

  • useHook(...) calls build() when the hook is first created
  • build() creates the hook's state
  • setup keeps that state alive and reuses it until unmount

That means there are two common shapes:

class CounterHook extends SetupHook<CounterHook> {
  late Signal<int> raw;

  @override
  CounterHook build() {
    raw = Signal(0);
    return this;
  }
}

Here, state is the hook object itself.

class CounterHook2 extends SetupHook<Counter> {
  @override
  Counter build() => Counter(initialValue: 0);
}

Here, state is the separate Counter object returned by build().

So build() is the creation step, and state is the value created by build() and then kept alive by setup.

Guidelines:

  • Use composition hooks when you are mostly composing existing hooks
  • Use useMemoized(..., disposer) when you already have reusable state logic and only need setup-managed ownership
  • Use class-based hooks when the hook itself needs explicit lifecycle structure
  • Use an existing class as hook state when your domain logic is already modeled as a reusable object
  • Add @defineHook annotation to enable lint checking and enforce hook rules

Hook Rules:

Hooks must follow these rules to work correctly:

DO: Call hooks synchronously

setup(context) {
  final count = useSignal(0);  // ✅ Correct - synchronous call
  return () => Text('${count.value}');
}

DON'T: Call hooks in async functions

setup(context) {
  Future<void> loadData() async {
    final data = useSignal([]);  // ❌ Wrong - inside async function
  }
  return () => Text('...');
}

DON'T: Call hooks in callbacks

setup(context) {
  ElevatedButton(
    onPressed: () {
      final count = useSignal(0);  // ❌ Wrong - inside callback
    },
  );
  return () => Text('...');
}

DON'T: Call hooks outside setup/hook context

void regularFunction() {
  final count = useSignal(0);  // ❌ Wrong - outside setup context
}

DO: Call hooks at the top level of setup or inside another hook

setup(context) {
  final count = useSignal(0);  // ✅ Correct
  final doubled = useComputed(() => count.value * 2);  // ✅ Correct
  
  onMounted(() {
    // ❌ Don't call hooks here - this is a callback
    print('Mounted');
  });
  
  return () => Text('${doubled.value}');
}

Usage Example:

setup: (context) {
  // Signals
  final count = useSignal(0);
  final name = useSignal('Flutter');
  
  // Computed values
  final doubled = useComputed(() => count.value * 2);
  
  // Reactive collections
  final items = useSignal.list(['apple', 'banana']);
  final userMap = useSignal.map({'name': 'John', 'age': 30});
  
  // Effects
  useEffect(() {
    print('Count changed: ${count.value}');
  });
  
  // Lifecycle callbacks
  onMounted(() {
    print('Widget mounted');
  });
  
  onUnmounted(() {
    print('Widget unmounted');
  });
  
  return () => Text('Count: ${count.value}');
}

Automatic Resource Cleanup:

All hooks automatically clean up their resources when the widget unmounts, ensuring proper cleanup and preventing memory leaks:

setup: (context) {
  final timer = useSignal<Timer?>(null);
  
  onMounted(() {
    timer.value = Timer.periodic(Duration(seconds: 1), (_) {
      print('Tick');
    });
  });
  
  onUnmounted(() {
    timer.value?.cancel();
  });
  
  return () => Text('Timer running');
}

Flutter Hooks #

Declarative hooks for managing common Flutter resources such as controllers, focus nodes, and lifecycle states with automatic cleanup.

Listenable Hooks #

Hook Description Returns
useValueNotifier<T>(initialValue) Creates a value notifier ValueNotifier<T>
useValueListenable<T>(...) Listens to a value notifier and triggers rebuilds void
useListenable<T>(...) Listens to any listenable and triggers rebuilds void
useListenableSync<T, C>(...) Bidirectional sync between Signal and Listenable void
useChangeNotifier<T>(creator) Generic ChangeNotifier hook T extends ChangeNotifier

Animation Hooks #

Hook Description Returns
useSingleTickerProvider() Creates a single ticker provider TickerProvider
useTickerProvider() Creates a ticker provider that supports multiple tickers TickerProvider
useAnimationController({...}) Creates an animation controller AnimationController

Focus Hooks #

Hook Description Returns
useFocusNode({...}) Creates a focus node FocusNode
useFocusScopeNode({...}) Creates a focus scope node FocusScopeNode

Lifecycle Hooks #

Hook Description Returns
useAppLifecycleState([initialState]) Listens to app lifecycle state ReadonlySignal<AppLifecycleState?>

Scroll Hooks #

Hook Description Returns
useScrollController({...}) Creates a scroll controller ScrollController
useTrackingScrollController({...}) Creates a tracking scroll controller TrackingScrollController
useTabController({...}) Creates a tab controller TabController
usePageController({...}) Creates a page controller PageController
useFixedExtentScrollController({...}) Creates a fixed extent scroll controller FixedExtentScrollController
useDraggableScrollableController() Creates a draggable scrollable controller DraggableScrollableController
useCarouselController({...}) Creates a carousel controller CarouselController

Text Hooks #

Hook Description Returns
useTextEditingController([text]) Creates a text editing controller TextEditingController
useTextEditingController.fromValue([value]) Creates a text editing controller from value TextEditingController
useRestorableTextEditingController([value]) Creates a restorable text editing controller RestorableTextEditingController
useSearchController() Creates a search controller SearchController
useUndoHistoryController({...}) Creates an undo history controller UndoHistoryController

Controller Hooks #

Hook Description Returns
useTransformationController([value]) Creates a transformation controller TransformationController
useWidgetStatesController([value]) Creates a widget states controller WidgetStatesController
useExpansibleController() Creates an expansible controller ExpansibleController
useTreeSliverController() Creates a tree sliver controller TreeSliverController
useOverlayPortalController({...}) Creates an overlay portal controller OverlayPortalController
useSnapshotController({...}) Creates a snapshot controller SnapshotController
useCupertinoTabController({...}) Creates a Cupertino tab controller CupertinoTabController
useContextMenuController({...}) Creates a context menu controller ContextMenuController
useMenuController() Creates a menu controller MenuController
useMagnifierController({...}) Creates a magnifier controller MagnifierController

Async Hooks #

Hook Description Returns
useFuture<T>(future, {...}) Creates a reactive future signal AsyncSnapshotFutureSignal<T>
useStream<T>(stream, {...}) Creates a reactive stream signal AsyncSnapshotStreamSignal<T>
useStreamController<T>(...) Creates a stream controller StreamController<T>
useStreamSubscription<T>(...) Manages a stream subscription void

Keep Alive Hook #

Hook Description Returns
useAutomaticKeepAlive(wantKeepAlive) Manages automatic keep alive with reactive signal void

Jolt Setup is part of the Jolt ecosystem:

Package Description
jolt Core library providing Signals, Computed, Effects, and reactive collections
jolt_flutter Flutter widgets: JoltBuilder, JoltSelector, and ValueNotifier integration
jolt_hooks Hooks API: useSignal, useComputed, useJoltEffect, useJoltWidget
jolt_surge Signal-powered Cubit pattern: Surge, SurgeProvider, SurgeConsumer
jolt_lint Custom lint and code assists: Wrap widgets, convert to/from Signals, Hook conversions

License #

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

1
likes
160
points
508
downloads

Documentation

API reference

Publisher

verified publishervowdemon.com

Weekly Downloads

Setup Widget API and Flutter hooks for building reactive widgets with Jolt signals, featuring automatic resource cleanup and lifecycle management.

Homepage
Repository (GitHub)
View/report issues

Topics

#jolt #state-management #signals #flutter

License

MIT (license)

Dependencies

flutter, jolt_flutter, meta, shared_interfaces

More

Packages that depend on jolt_setup