Hi Flutter folks,
I’m building Blooming , a reactive framework for Dart and Flutter, and would love to hear what you think.
What led me to Blooming
While building a Flutter UI authoring tool, I kept running into state-management problems and rebuilt the state-management layer several times. A turning point came when I explored Preact Signals and Signals.dart . I was especially drawn to the API design of computed: dependencies are connected dynamically based on the signals read during a computation, and those connections are updated when the reads change.
That idea resonated strongly with me, so I went back and redesigned the reactive pipeline I had already completed from the ground up. That redesign became Blooming.
How it works
Blooming’s core uses a two-phase push-dirty / lazy-pull graph. When a source changes, dependent nodes are marked dirty first; derived values are recomputed on demand, in dependency order. This is designed to prevent intermediate inconsistent values from being observed in diamond-shaped dependency graphs. Dependencies are declared explicitly with via(node), rather than discovered through ambient tracking.
Blooming Flutter connects the graph to Flutter’s widget tree. APIs such as bloom.build and composed let a small UI boundary observe reactive values and update independently of its surrounding widgets.
A few design choices
Three-state snapshots: Reactive values use Data, Empty, and Error states. The async UI helpers also support stale-while-revalidate behavior, preserving existing data while new data is being fetched.
Lifecycle-bound resource management: managed ties external resource creation, updates, and disposal to a reactive node’s lifecycle.
Graceful deactivation policies: When a node has no subscribers, its upstream connections and associated resources are normally torn down. Graceful controls when that deactivation and cleanup happen. Graceful.microtask defers teardown by one scheduler tick; Graceful.period(…) keeps the node and its upstream dependencies active for a specified duration.
Animations controlled from an ancestor: I built DurableAnimationController so an ancestor can own and control an animation without tying the controller to the state of the child that displays it. It can be used without StatefulWidget or TickerProviderStateMixin, with policies to resume, catch up to elapsed time, or reset when the UI becomes active again.
Transactional collection editing: BloomDraftController provides edit for reactive lists, sets, and maps. You modify a temporary draft, then commit it as an unmodifiable collection. Multiple changes produce at most one invalidation, and an edit with no net change produces none.
The workspace also includes collection extensions, lint rules, and testing packages.
Blooming is pre-1.0; its API and design are still evolving and may change substantially. I’d especially appreciate feedback on the usability and direction of the composed API, the lifecycle model, and whether this approach would fit real Flutter apps.
English isn’t my first language, so I used AI to help write this post in English. The project and the ideas behind it are mine.
Thanks for taking a look!
Have you seen BlocSignal? I also saw Signals (Rody’s package) to be a great data-dependency structure, but it was too flexible. So I added the riger of Bloc and Cubit to hold the core synchronous data in an app, relying on pure signals at the async edges.
EDIT: I reviewed your repo, and five items stood out as being useful to BlocSignal:
opened 09:10PM - 28 Oct 26 UTC
enhancement
## 🎯 Objective & Context
Handling one-shot UI side-effects (for example navigat… ion, SnackBars, bottom sheets, or analytics pings) in Flutter BLoC architectures has historically required awkward workarounds:
- Polluting the domain state with ephemeral boolean flags (`hasNavigated: true`, `isErrorHandled: true`).
- Emitting explicit reset events (`ClearNavigationFlag`).
- Converting declarative listeners into verbose `StatefulWidget`s with manual state flags or effect subscription cancellations.
This feature borrows and adapts the **Execution Quotas (`take: N`)** concept from [**Blooming (`pioneerjkf/blooming`)**](https://github.com/pioneerjkf/blooming), which allows reactive listeners (`BloomListener`, `bloom.listen`) to detach automatically after exhausting a designated execution budget.
---
## 🛠️ Proposed Enhancements
1. **Add `take` and `takeWhile` to `BlocSignalListener`**:
- `final int? take;`: Maximum number of times the listener callback may fire. Once reached, the internal signal effect or subscription severs itself cleanly and detaches.
- `final bool Function(S state)? takeWhile;`: Predicate evaluated prior to listener invocation. If it returns false, the listener execution halts and detaches permanently.
2. **Propagate to `BlocSignalConsumer`**:
- Forward `take:` and `takeWhile:` parameters to the listener delegate of `BlocSignalConsumer`.
3. **Example Usage**:
```dart
BlocSignalListener<CheckoutBloc, CheckoutState>(
listenWhen: (previous, current) => current is CheckoutSuccess,
take: 1, // Automatically disposes the listener after one execution
listener: (context, state) {
Navigator.of(context).pushReplacementNamed('/receipt');
},
child: const CheckoutForm(),
)
```
---
## 🧪 Verification Plan
1. Unit tests in `bloc_signals_flutter/test/` verifying:
- `take: 1` fires the listener on the first qualifying state change and ignores subsequent matching emissions.
- `take: 3` fires exactly three times and then detaches.
- `takeWhile` detaches as soon as the condition evaluates to false.
- Proper disposal cleanup without dangling signal effects when the widget unmounts prior to quota completion.
2. Verify 100% test coverage across `bloc_signals_flutter`.
3. Update `plugins/bloc-signals/skills/bloc-signals/flutter.md` and `CHANGELOG.md` (`## Unreleased`).
---
## 🔗 Attribution & Prior Art
- Borrowed with credit from [Blooming (`pioneerjkf/blooming`)](https://github.com/pioneerjkf/blooming) (see `BloomListener` and `bloom.listen` quota tracking).
opened 09:10PM - 28 Oct 26 UTC
enhancement
## 🎯 Objective & Context
In `BlocSignal`, event transformers (such as `debounce… ` or `droppable`) operate on the **event ingress** side inside `BlocSignal`. However, in scenarios with high-frequency state updates (for example streaming stock tickers, location/gyroscope sensors, slider dragging, or search filter emissions), the container state must update synchronously and immediately, but downstream UI side-effects (such as haptic feedback clicks, toast notifications, sound effects, or external persistence calls) should not fire on every synchronous frame emission.
This feature borrows and adapts the **UI-side listener rate-limiting** concept from [**Blooming (`pioneerjkf/blooming`)**](https://github.com/pioneerjkf/blooming), which provides built-in `BloomListener.debounce` and `BloomListener.throttle` constructors.
---
## 🛠️ Proposed Enhancements
1. **Add Named Constructors or Parameters to `BlocSignalListener`**:
- `BlocSignalListener.debounce`:
```dart
BlocSignalListener<SearchCubit, SearchState>.debounce(
duration: const Duration(milliseconds: 300),
listener: (context, state) => triggerHapticFeedback(),
child: const SearchResultsView(),
)
```
- `BlocSignalListener.throttle`:
```dart
BlocSignalListener<SensorCubit, SensorState>.throttle(
duration: const Duration(milliseconds: 100),
listener: (context, state) => updateExternalHardware(state),
child: const SensorDashboard(),
)
```
- Alternatively, support optional `Duration? debounce` and `Duration? throttle` parameters on the default `BlocSignalListener` and `BlocSignalConsumer` constructors.
2. **Lifecycle Safety**:
- Ensure internal debounce/throttle timers cancel cleanly when the listener element unmounts or when the container instance is swapped.
---
## 🧪 Verification Plan
1. Unit and widget tests in `bloc_signals_flutter/test/` verifying:
- Debounce suppresses intermediate state notifications until the quiet window elapses.
- Throttle executes the first notification immediately and suppresses subsequent events within the duration window.
- Timers cancel without memory leaks or timer exceptions upon widget disposal.
2. Verify 100% test coverage across `bloc_signals_flutter`.
3. Update `plugins/bloc-signals/skills/bloc-signals/flutter.md` and `CHANGELOG.md` (`## Unreleased`).
---
## 🔗 Attribution & Prior Art
- Borrowed with credit from [Blooming (`pioneerjkf/blooming`)](https://github.com/pioneerjkf/blooming) (see `BloomListener.debounce`, `BloomListener.throttle`, and `bloom.listen.debounce`).
opened 09:10PM - 28 Oct 26 UTC
enhancement
## 🎯 Objective & Context
When using `BlocSignalProvider` with auto-disposed con… tainers or route-bound providers, navigating between screens (for example pushing a detail route, popping back, or switching tabs in an unmounted view) often unmounts the provider widget for a few frames or seconds. If the provider tears down immediately, the container is destroyed and re-instantiated when the route remounts, triggering redundant network fetches, cache invalidations, and skeleton layout flickers.
This feature borrows and adapts the **Graceful Retention** concept from [**Blooming (`pioneerjkf/blooming`)**](https://github.com/pioneerjkf/blooming), which uses `Graceful.period(Duration)` and `Graceful.microtask` to keep dormant nodes and resources alive across short unsubscribe intervals.
---
## 🛠️ Proposed Enhancements
1. **Add `retentionPeriod` Parameter to `BlocSignalProvider`**:
- `final Duration? retentionPeriod;`: When specified (and when the provider owns the container lifecycle), unmounting the provider element does not immediately call `bloc.close()`.
- Instead, the container instance and its signal graph enter a dormant retention pool for the specified duration.
2. **Re-attach on Remount**:
- If a matching `BlocSignalProvider` with an equivalent key or type remounts within the retention window, the cached instance is reused seamlessly without re-running `create`.
- If the retention timer expires without a remount, `bloc.close()` is invoked and the container is purged from the retention pool.
3. **Example Usage**:
```dart
BlocSignalProvider<UserProfileCubit>(
create: UserProfileCubit.create,
// Retain container in memory for 10 seconds across quick route transitions
retentionPeriod: const Duration(seconds: 10),
child: const UserProfileScreen(),
)
```
---
## 🧪 Verification Plan
1. Widget tests in `bloc_signals_flutter/test/` verifying:
- Unmounting the provider defers `close()` when `retentionPeriod` is set.
- Remounting within the retention window preserves the original instance and its state.
- Allowing the timer to expire executes `close()` and frees memory.
- Provider swap or replacement respects active retention pools.
2. Verify 100% test coverage across `bloc_signals_flutter`.
3. Update `plugins/bloc-signals/skills/bloc-signals/flutter.md` and `CHANGELOG.md` (`## Unreleased`).
---
## 🔗 Attribution & Prior Art
- Borrowed with credit from [Blooming (`pioneerjkf/blooming`)](https://github.com/pioneerjkf/blooming) (see `Graceful.period` and `Graceful.microtask` retention mechanics).
opened 09:11PM - 28 Oct 26 UTC
enhancement
## 🎯 Objective & Context
In `bloc_signals_flutter`, rendering reactive state cu… rrently involves `BlocSignalBuilder<B, S>(...)` or `context.select<B, R>(...)`. While `context.value<B, S>()` is ergonomic inside existing `build()` methods, creating leaf widgets often involves wrapping boilerplate and nesting overhead.
This feature borrows and adapts the **Fluent Zero-Allocation Widget Functor** concept from [**Blooming (`pioneerjkf/blooming`)**](https://github.com/pioneerjkf/blooming), which uses Dart 3 extension types (`extension type const`) on reactive nodes (`node.build(...)`, `node.build.child(...)`) for high-performance, concise UI composition.
---
## 🛠️ Proposed Enhancements
1. **Provide `.build(...)` Extension Type on `ReadonlySignal<T>` and `BlocSignalBase<T>`**:
- Create a compile-time extension type functor wrapping reactive signals:
```dart
extension type const SignalWidgetFunctor<T>(ReadonlySignal<T> signal) {
Widget call(Widget Function(BuildContext context, T value) builder) { ... }
Widget child(
Widget child,
Widget Function(BuildContext context, T value, Widget? child) builder,
) { ... }
}
```
2. **Ergonomic Usage**:
```dart
// Direct leaf widget building:
counterCubit.state.build((context, count) => Text(
'Count: $count',
style: Theme.of(context).textTheme.headlineMedium,
))
// With child subtree optimization:
counterCubit.state.build.child(
const ComplexBackgroundIllustration(),
(context, count, child) => Stack(
children: [
child!,
Text('Count: $count'),
],
),
)
```
3. **Performance & Type Safety**:
- Zero runtime heap allocation overhead using Dart 3 extension types.
- Clean type inference for the emitted state without specifying explicit generic type parameters.
---
## 🧪 Verification Plan
1. Widget tests in `bloc_signals_flutter/test/` verifying:
- Rebuilding occurs exclusively when the underlying signal value changes.
- Child widgets passed to `.build.child` are not rebuilt unnecessarily.
- Proper disposal and unsubscription when the built widget element unmounts.
2. Verify 100% test coverage across `bloc_signals_flutter`.
3. Update `plugins/bloc-signals/skills/bloc-signals/flutter.md` and `CHANGELOG.md` (`## Unreleased`).
---
## 🔗 Attribution & Prior Art
- Borrowed with credit from [Blooming (`pioneerjkf/blooming`)](https://github.com/pioneerjkf/blooming) (see `bloom.build` zero-allocation compile-time functors).
opened 09:11PM - 28 Oct 26 UTC
enhancement
## 🎯 Objective & Context
State containers (`CubitSignal`, `BlocSignal`) frequen… tly coordinate external non-signal resources—such as `Timer`s, `StreamSubscription`s, platform channel listeners, or controller instances. Currently, managing these requires boilerplate: declaring nullable private fields, null-checking, and manually overriding `close()` to ensure all disposables are cleared. Forgetting to cancel a subscription or timer in `close()` introduces memory and process leaks.
This feature borrows and adapts the **Lifecycle Resource Management (`managed`)** concept from [**Blooming (`pioneerjkf/blooming`)**](https://github.com/pioneerjkf/blooming), which ties the lifecycle of non-signal resources directly to container activation and disposal.
---
## 🛠️ Proposed Enhancements
1. **Add Resource Management APIs to `BlocSignalBase<T>`**:
- `TResource manageResource<TResource>(TResource resource, void Function(TResource) onDispose)`: Registers an arbitrary resource to be disposed when the container closes.
- `StreamSubscription<T> manageStreamSubscription<T>(StreamSubscription<T> subscription)`: Convenience helper that cancels the subscription on container close.
- `Timer manageTimer(Timer timer)`: Convenience helper that cancels the timer on container close.
2. **Automated Teardown on `close()`**:
- During `close()`, iterate over registered managed resources in reverse order of registration, executing their respective disposal handlers within guarded `try/catch` blocks (forwarding unexpected errors to `onError()` per `SCAR-CORE-24`).
3. **Example Usage**:
```dart
class TickerCubit extends CubitSignal<int> {
TickerCubit() : super(initialState: 0) {
manageTimer(Timer.periodic(const Duration(seconds: 1), (_) {
emit(stateValue + 1);
}));
manageStreamSubscription(hardwareEvents.listen(_onEvent));
}
}
```
---
## 🧪 Verification Plan
1. Unit tests in `bloc_signals/test/` verifying:
- Registered timers and subscriptions are automatically cancelled when `close()` is invoked.
- Custom `manageResource` disposers are called in reverse order of registration.
- Calling `close()` multiple times remains idempotent.
- Exceptions thrown by a faulty resource disposer are caught and routed to `onError` without aborting subsequent disposals.
2. Verify 100% test coverage across `bloc_signals`.
3. Update `plugins/bloc-signals/skills/bloc-signals/core.md` and `CHANGELOG.md` (`## Unreleased`).
---
## 🔗 Attribution & Prior Art
- Borrowed with credit from [Blooming (`pioneerjkf/blooming`)](https://github.com/pioneerjkf/blooming) (see `managed` resource lifecycle binding).
thanks for your package!
1 Like
Thanks for taking the time to read through Blooming and for sharing BlocSignal. I’ll take a closer look at it. It’s great to hear that some of Blooming’s ideas might be useful in your project. I appreciate you pointing out the features that stood out to you, and I’ll follow the linked issues with interest. Your perspective is helpful as I continue to evolve Blooming.
1 Like