Flutter Widgets
When building reactive UIs in Flutter, Jolt provides core Widgets: JoltBuilder, JoltSelector, and JoltWatchBuilder. These Widgets are all implemented based on FlutterEffect, triggering only one rebuild per frame, automatically tracking dependency changes, and rebuilding Widgets when dependencies change, keeping the UI synchronized with data state.
When you access signals (Signal), computed values (Computed), or reactive collections in the builder function, these Widgets automatically establish dependencies. When tracked dependencies change, Widgets automatically rebuild, keeping the UI synchronized with data state.
This design eliminates the need for developers to manually manage subscriptions and unsubscriptions, or worry about when Widgets need to rebuild. You just naturally access reactive data in builder, and the rest is handled automatically. Each Widget has its specific use cases—let's explore them one by one.
JoltBuilder
JoltBuilder is the most general-purpose reactive Widget, automatically tracking all signals accessed in the builder function. When any tracked signal changes, the Widget automatically rebuilds.
Basic Usage
import 'package:flutter/material.dart';
import 'package:jolt_flutter/jolt_flutter.dart';
final counter = Signal(0);
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return JoltBuilder(
builder: (context) => Column(
children: [
Text('Count: ${counter.value}'),
ElevatedButton(
onPressed: () => counter.value++,
child: Text('Increment'),
),
],
),
);
}
}Multiple Signal Tracking
JoltBuilder can track multiple signals simultaneously, triggering rebuilds when any of them change:
final firstName = Signal('John');
final lastName = Signal('Doe');
JoltBuilder(
builder: (context) => Text(
'Hello ${firstName.value} ${lastName.value}',
),
);JoltSelector
JoltSelector provides finer-grained rebuild control, using a selector function to select the value to track. The Widget only rebuilds when the value returned by selector changes (via == comparison). This is particularly useful for complex objects or scenarios requiring data filtering, avoiding unnecessary rebuilds and improving performance.
Basic Usage
final user = Signal(User(name: 'John', age: 30));
// Only listen to name, not age
JoltSelector(
selector: (prev) => user.value.name,
builder: (context, name) => Text('Hello $name'),
);
// Rebuilds when user.name changes
// Does not rebuild when user.age changesMultiple Signal Selection
final firstName = Signal('John');
final lastName = Signal('Doe');
JoltSelector(
selector: (prev) => '${firstName.value} ${lastName.value}',
builder: (context, fullName) => Text('Hello $fullName'),
);Using Previous Value
The selector function receives the previously selected value (null on first call), which can be used for comparison or optimization:
JoltSelector(
selector: (prev) {
final current = computeValue();
// If value is the same, return the same instance to avoid rebuild
if (prev != null && prev == current) {
return prev;
}
return current;
},
builder: (context, value) => Text('$value'),
);JoltWatchBuilder
JoltWatchBuilder is a reactive Widget that tracks a single Readable value and rebuilds when that value changes. It's particularly useful when you want to watch a specific signal or computed value.
Basic Usage
import 'package:flutter/material.dart';
import 'package:jolt_flutter/jolt_flutter.dart';
final counter = Signal(0);
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return JoltWatchBuilder<int>(
readable: counter,
builder: (context, value) => Text('Count: $value'),
);
}
}Using the watch Extension
For convenience, you can use the watch extension method directly on a Readable:
import 'package:flutter/material.dart';
import 'package:jolt_flutter/jolt_flutter.dart';
import 'package:jolt_flutter/extension.dart';
final counter = Signal(0);
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
counter.watch((value) => Text('Count: $value')),
ElevatedButton(
onPressed: () => counter.value++,
child: Text('Increment'),
),
],
);
}
}When to Use JoltWatchBuilder vs JoltBuilder
- JoltWatchBuilder: Use when you want to track a single specific
Readablevalue. More explicit and easier to understand dependencies. - JoltBuilder: Use when you want to track multiple signals or when dependencies are complex and dynamic.
JoltProvider
⚠️ Deprecated:
JoltProvideris deprecated and will be removed in a future version. For dependency injection, use Flutter's built-in solutions likeProvider,Riverpod, or other DI packages.
JoltProvider was used to provide and manage reactive resources in the Widget tree, supporting complete lifecycle management. It combined dependency injection patterns with reactive programming, allowing you to share state in the component tree while automatically handling resource creation and disposal.
Migration Guide
Replace JoltProvider with Flutter's dependency injection solutions:
// Before
JoltProvider<MyStore>(
create: (context) => MyStore(),
builder: (context, store) => Text('${store.counter.value}'),
)
// After - Using Provider package
Provider<MyStore>(
create: (_) => MyStore(),
child: Builder(
builder: (context) {
final store = Provider.of<MyStore>(context);
return Text('${store.counter.value}');
},
),
)Using create
Using the create constructor automatically manages resource lifecycle, automatically calling onUnmount and dispose() when the Widget is unmounted:
class MyStore extends JoltState {
final counter = Signal(0);
@override
void onMount(BuildContext context) {
print('Store mounted');
}
@override
void onUnmount(BuildContext context) {
print('Store unmounted');
}
}
JoltProvider<MyStore>(
create: (context) => MyStore(),
builder: (context, store) => Text('${store.counter.value}'),
);Using .value
When using the .value constructor, resource lifecycle needs manual management. Provider will not call onMount, onUnmount, or dispose():
final store = MyStore();
JoltProvider<MyStore>.value(
value: store,
builder: (context, store) => Text('${store.counter.value}'),
);Accessing from Descendant Widgets
JoltProvider<MyStore>(
create: (context) => MyStore(),
builder: (context, store) => ChildWidget(),
);
// In ChildWidget
class ChildWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final store = JoltProvider.of<MyStore>(context);
return Text('Count: ${store.counter.value}');
}
}Optional Access
final store = JoltProvider.maybeOf<MyStore>(context);
if (store != null) {
// Use store
}JoltState
JoltState is an abstract class for resources that need lifecycle management. When resources are used in JoltProvider with the create constructor, if the resource implements JoltState, lifecycle callbacks are automatically called.
Lifecycle
- onMount: Called after resource creation and Widget mounting, used for resource initialization (such as starting timers, subscribing to streams, etc.)
- onUnmount: Called when Widget is unmounted or resource is replaced, used for resource cleanup (such as canceling timers, unsubscribing, etc.)
Example
class MyStore extends JoltState {
final counter = Signal(0);
Timer? _timer;
@override
void onMount(BuildContext context) {
super.onMount(context);
_timer = Timer.periodic(Duration(seconds: 1), (_) {
counter.value++;
});
}
@override
void onUnmount(BuildContext context) {
super.onUnmount(context);
_timer?.cancel();
_timer = null;
}
}If resources don't need lifecycle management, you don't need to extend JoltState:
class SimpleStore {
final counter = Signal(0);
}