jolt_setup 3.0.0 copy "jolt_setup: ^3.0.0" to clipboard
jolt_setup: ^3.0.0 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 declarative hooks for managing common Flutter resources such as controllers, focus nodes, and lifecycle states with automatic cleanup.

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'),
          ),
        ],
      ),
    );
  }
}

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 fundamentally different models. Avoid mixing them to prevent confusion.

Setup Widget provides a composition API similar to Vue's Composition API for building Flutter widgets. The key difference from React hooks: the setup function executes only once when the widget is created, not on every rebuild. This provides better performance and a more predictable execution model.

SetupBuilder #

The simplest way to use Setup Widget is with SetupBuilder:

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 #

💡 No Right or Wrong Choice

There's no single "correct" way to build widgets in Jolt. SetupWidget, SetupMixin, and traditional Flutter patterns (StatelessWidget, StatefulWidget) are all first-class citizens. Each shines in different scenarios—what matters is knowing when to use which, keeping your code clear and maintainable.

The Setup API itself is entirely optional. If your team is comfortable with standard Flutter patterns and they're working well, there's no need to change. You can also use Riverpod, flutter_hooks, or any other state management solution you prefer, even mixing them in the same project.

When you need composition-based logic, reactive state, or Vue/Solid-style patterns, the Setup API is there to give you that extra power—without forcing you to rewrite existing code.

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
  • Prefer cleaner, more concise code
  • 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:

💡 About Using Hooks

For reactive objects like Signal and Computed, you can create them directly without hooks if they'll be garbage collected when the widget unmounts (e.g., local variables in the setup function). The main purpose of hooks is to ensure proper cleanup and state preservation during widget unmount or hot reload.

setup(context, props) {
  // Using hooks - Recommended, automatic lifecycle management
  final count = useSignal(0);
  
  // Without hooks - Also fine, gets GC'd after widget unmounts
  final temp = Signal(0);
  
  return () => Text('Count: ${count.value}');
}
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
useSignal.persist(...) Create a persisted 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
useComputed.convert(source, decode, encode) Create a type-converting computed value

Effect Hooks #

Hook Description
useEffect(fn) Create an effect
useEffect.lazy(fn) Create an immediately-executing effect
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
useMemoized(creator, [disposer]) Memoize value with optional cleanup
useAutoDispose(creator) Auto-dispose resource
useHook(hook) Use a custom hook

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. Explore these related packages:

Package Description
jolt Core library providing Signals, Computed, Effects, and reactive collections
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.

0
likes
160
points
176
downloads

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

Documentation

API reference

License

MIT (license)

Dependencies

flutter, jolt_flutter, meta, shared_interfaces

More

Packages that depend on jolt_setup