juice 1.10.0
juice: ^1.10.0 copied to clipboard
Lifecycle-aware Flutter application framework built around use-case driven blocs, scoped ownership, and targeted rebuilds.
Changelog #
1.10.0 - 2026-09-29 #
Includes everything listed under 1.9.1 below, which was never published on its own; the additions make this a minor.
Added #
LevelAwareJuiceLogger— the telemetry cost follows the consumer. An optional interface (Level get minLevel): a logger declares the lowest level it keeps, and the framework does not build a per-event entry below it. A logger that onlyimplements JuiceLoggerkeeps everything, as before.JuiceLoggerConfig.minLevel(below) is a global floor on top — the effective level is the HIGHER of the two.DefaultJuiceLoggerdeclares what its filter already does. Theloggerpackage's default filter decides inside anassert: it keeps lines at or aboveLogger.levelin debug and NOTHING in profile or release. The default logger now declares exactly that, so an app that never configures a logger pays for no chatter in release — about a third of a dispatch (benchmarks §9) — and loses no line it ever printed. A caller-suppliedLoggerhas an unreadable filter and is declaredLevel.all, unchanged. One thing to check if you wrap the default logger: only the CONFIGURED logger's declaration counts. A wrapper that does not implement the interface still receives everything; one that delegatesminLevelto an innerDefaultJuiceLoggerreceives no chatter outside debug. Declare what your logger consumes.JuiceLoggerConfig.minLevel— the global floor for the framework's own per-event chatter. Every use-case execution logs a span pair and every emission logs an entry, atLevel.info; building those context maps is ~40% of a dispatch even when the logger drops them (benchmarks §4, §6: ~0.7 of ~1.7 µs per send on an iPhone 17 Pro Max). BelowminLevelthe framework does not build them — the logger is not called for that entry. DefaultLevel.all: nothing changes unless set.Level.warningin release is the whole gain for a logger that declares nothing. Never gateslogError,emission_after_close,event_ignored, or a failure emission's entry: those stay loud.DevtoolsJuiceLogger(observability ≥ 0.5.1) declaresLevel.allwhile a listener is attached, so the panel works without touching this. Pinned bytest/bloc/telemetry_level_test.dart.
1.9.1 - 2026-09-28 #
Dispatch-cost release, driven by the new benchmarks (benchmarks/,
Juice vs bloc vs Riverpod). No signatures change; one contract does:
JuiceLogger context values are now live objects (the emission's
'state' is the state, 'groups' the caller's set), not strings. A custom
logger must stringify lazily — after its own level check — and must not
retain the context map. DefaultJuiceLogger and DevtoolsJuiceLogger
already do; see the JuiceLogger class doc.
Fixed #
- Leaner telemetry on the emit path. The status emitter put the
state's
toString()and the groups'toString()into its log context on every emission, whatever the logger; it now passes the objects (loggers that print stringify them). The executor names a span's use case and event once. With the logger fix below, default-logger dispatch measured 12.54 → 6.50 µs per event (release,benchmarks/RESULTS.md). DefaultJuiceLoggerno longer formats lines it will drop. Every emission logs a context holding the state; the line was built eagerly —'$message | Context: $context', i.e. the state'stoString()— on EVERY emission, in release too, where logger's default filter then discards it. With the default printer the message is now a closure the printer evaluates only when the line passes the filter. A caller-suppliedLoggerkeeps eager strings (its printer may not evaluate function messages, e.g.LogfmtPrinter). Found by the new benchmarks (benchmarks/).
Docs #
- Measured where a dispatch's time goes (
benchmarks/RESULTS.md§4): the fresh use-case instance per event costs ~0.34 µs of a ~5 µs send; the rest is telemetry context construction and the async executor.
1.9.0 - 2026-09-26 #
Robustness release: the guarantees Juice documents now hold under stress — closing mid-flight, failing closes, stale leases, mis-registered use cases — and the core is Web/WASM-compatible.
Added #
UseCaseBuilder.typed(() => FooUseCase())— type-checked registration. The event type is inferred from the use case, so pairingtypeOfEvent: SaveEventwith aLoadUseCaseis a compile error instead of a dispatch-time cast failure. The use case's bloc type is checked when the bloc registers the builder: registered on the wrong bloc, it throwsArgumentErrorfrom that bloc's constructor. TakesconcurrencyandinitialEventBuilder. Additive; the constructor is unchanged.- Events that return a value, in core:
ResultEvent<T>(moved to its own library),OperationResult, and theResultEventOpsextension —bloc.sendForResult(event)/bloc.sendAndWaitResult(event). Promoted fromjuice_storage(2.3.0 now builds on them) so the family has one way to await an event's value. Stricter than storage's original: a closed or closing bloc throws at once, and an event whose processing ends without a terminal status (dropped by adroppablebuilder, or its use case emitted nothing) throwsStateErrorinstead of waiting out the timeout. The terminal status is captured synchronously at emit time (an internal emission tap), not from the asynchronously delivered stream. juiceTest(package:juice/testing.dart) — declarative bloc tests:build/seed/act/wait/skip/expect/errors/verify, plusrunJuiceTestto call the body directly. No fixed sleeps (actreturns thesend()futures, which complete when processing does); the bloc is closed BEFORE asserting; each emission's rebuild groups are snapshotted at emit time (they accumulate on a multi-emit event);isUpdatingStatus/isWaitingStatus/isFailureStatus/isCancelingStatusmatch kind, state and groups; an unexpected use-case error fails the test; a use case still running at close fails it (the "act didn't await" leak).BlocTesterremains. Dogfooded: juice_theme's behavior tests are ported and now assert groups with nosettle(). Addstest_api(its publicscaffolding.dart, thetestflutter_test re-exports) andmetaas dependencies, used only bytesting.dart.JuiceBloc.isClosing— true onceclose()has started, and stays true (monotonic;isClosedmarks completion). Formalizes the flagjuice_sync'sSyncBlocalready declared, which now overrides it.
Changed #
- The close fence. An emit after the bloc has closed (a use case still
running when the user left) is dropped and logged as
emission_after_close(warning) instead of throwingStateError— which surfaced as a bloc error and, underRetryableUseCaseBuilder, was retried against the closed bloc. From the momentclose()starts, new events are refused (send,sendCancellable), andsendAndWaitthrowsStateErrorat once instead of waiting out its timeout. close()is memoized: a second caller awaits the same teardown instead of returning before it finishes.RetryableUseCaseBuilderabandons the retry loop (retry_abandoned) when the bloc closes during backoff.package:juice/juice.dartre-exportspackage:logger/web.dart, notlogger.dart: everything exceptFileOutput/AdvancedFileOutput(nothing in the family uses them). logger's own web stub for those importsdart:io— in every release through 2.8.0 — which made juice WASM-incompatible. An app writing log files importspackage:logger/logger.dartdirectly.
Fixed #
- A stale lease can no longer close a replacement bloc. A lease taken
before
BlocScope.end<T>()/endFeatureand released after a new instance was created decremented the NEW instance's count and auto-closed it under the widgets using it. Leases now remember their instance. Every release also settlesLeakDetector, so a lease released during a close is no longer reported as a leak. - A
close()that throws no longer wedges its entry.BlocScopereports it (bloc_close_error), rethrows to the caller, and always clears the entry — it used to leave it "closing" forever, so every laterget/leasethrew.leaseAsyncwaits through a failed close. FeatureScope.end()can no longer hang. A feature bloc whose close threw left the end event uncompleted forever; the event now fails with the error, after the scope is still removed andScopeEndedNotificationstill published.start()/end()fall back to direct disposal when theScopeLifecycleBlocis closed or closing (its refused events never complete).- Use-case wiring runs inside the telemetry span: a wiring failure logs
its
use_case_errorEND and reachesonError(it was a START with no END, swallowed silently insequentialmode). The wrong-bloc cast now names the use case, the bloc it expects, and the bloc it was registered on. - pub.flutter-io.cn static analysis: two doc comments with bare angle brackets.
Found by the new tests:
StatelessJuiceWidget/2/3rebuilt with a differentscopekept leasing and streaming the OLD bloc while itsblocgetter read the new one (and a leased new bloc was never leased, so the getter threw). The lease holders now re-lease on a scope change — new lease first, then the old one released.JuiceAsyncBuilder: a stream closing right after an error threw "Snapshot data must not be null" (done now keeps the error); a future failing after unmount wrote to a disposed notifier, and a replaced future's late error overwrote the current snapshot (the error path now has the success path's guard).AviatorManager.navigatedropped the future of async navigation, so an unknown deep link or a failing auth/data step — on the path of everyemitUpdate(aviatorName:)— escaped as an uncaught zone error. Now logged asaviator_error.- Inline use cases navigating (
ctx.emit.failure(aviatorName: …)) emitted an extraUpdatingStatusthat overwrote the failure/waiting/ cancel and widened the groups to*(every widget rebuilt). Navigation no longer emits. CancellableEventequality is identity. The value-style==compared only the runtime type and the cancelled flag (two different orders were equal), andhashCodechanged oncancel(), losing the event in any Set/Map.TimeoutSupport's override is gone too.- A
TimeoutSupporttimer is stopped when its use case finishes; it used to fire later and mark a completed event timed out and cancelled. - Docs: the default
groups: {'*'}rebuilds onrebuildAlwaysbroadcasts only, not "on all state changes" (behavior unchanged, now pinned);JuiceWidgetState2/3.close()runs once ALL blocs have closed.
Quality #
- pana 160/160 locally (1.8.1 scored 140: platform support and static analysis).
- Core line coverage 55.8% → 94.4%, now gated at 90% in CI
(
tool/coverage_check.sh,melos run coverage:juice). The vendored, unusedBloc<Event, State>base (bloc.dart,emitter.dart,bloc_support.dart,bloc_base.dart,global_bloc_resolver.dart) is excluded and flagged for removal in 2.0.0. The suite grew from 217 to 479 tests; every exported widget is now tested (JuiceBuilder*,JuiceWidgetState*and theJuiceExceptionWidgethad none).
1.8.1 - 2026-09-25 #
Fixed #
sendAndWaitreturns instead of timing out. It awaitedsend()— which completes only after the use case has finished emitting — and only then subscribed to the non-replayingstream, so it had already missed its own result and threwTimeoutExceptionunless some unrelated emission followed. It now subscribes before sending, and matches only statuses caused by the event it sent (the patternjuice_storage'ssendAndWaitResultalready used).- Two listeners on one
select()/selectWith()stream both fire. The 1.8.0previouslived in one closure shared by every subscription, so the first listener advanced it and the second saw "equal" and never emitted.previousis now per subscription, seeded from the state when THAT listener subscribes (what the 1.8.0 doc already claimed). Still broadcast. JuiceSelector/JuiceSelectorWith:- swapping the
bloc:prop shows the new bloc's value at once — it stayed on the old bloc's value until the projection next changed (StreamBuilderappliesinitialDataonly once); - a new
selectorclosure (e.g. over a changed row id) takes effect on rebuild — the first build's closure was used forever; groupsare compared by value, so an inline{Group.x}literal no longer resubscribes on every parent rebuild, and a changed set takes effect. The widgets hold the displayed value and compare each considered emission against it directly (noStreamBuilder).
- swapping the
FeatureScopeids are a monotonic counter. They wereDateTime.now().microsecondsSinceEpoch, and equality is by id: two scopes created in the same clock tick (routine on web's millisecond clock) were EQUAL, so theirBlocIds collided — they shared blocs, and ending one closed the other's.LeakDetectorkeys leases by a sequence number, not a timestamp: two leases acquired in the same tick shared a key, the second overwrote the first, and a real leak went unreported.
Changed #
flutter_testis no longer a runtime dependency (it is a dev dependency). It was only used bypackage:juice/testing.dart'sBlocTester, which now asserts through the pure-Dartpackage:matcher(matcher/expect.dart) — the sameexpectflutter_testre-exports, soBlocTesterbehaves identically insidetestandtestWidgets. Consumer apps no longer get the test framework in their dependency graph.- Dropped the unused
cupertino_iconsdependency (template leftover). unused_catch_stackinemitter.dart(flagged by current analyzers).
Tests #
test/bloc/state_selector_test.dart: two listeners on one stream; a late listener is seeded at ITS subscription; broadcast preserved;sendAndWaitreturns its own event's status and ignores others'.test/ui/juice_selector_test.dart: bloc swap (both widgets), selector change, value-equal vs changed inline groups.test/bloc/lifecycle_ids_test.dart: 1000 back-to-back scopes are distinct; two same-tick leases are both tracked.
1.8.0 - 2026-09-21 #
Added #
JuiceSelector/JuiceSelectorWith/bloc.select/bloc.selectWithtakegroups. Select-style rebuilds now live INSIDE the rebuild-groups vocabulary instead of beside it: withgroups, an emission whosegroupsToRebuilddo not intersect them is ignored entirely (not projected, not remembered — the samedenyRebuildfilter every Juice widget uses), and only then is the selected value compared. "In this group's blast radius, only if this cell moved" — the shape for a hot cell (a list row's one field, a ticker) under a group that covers a whole section. Withoutgroupsthe previous behavior stands: every emission is compared.rebuildAlwayspasses the group filter as it does everywhere.
Fixed #
- A selector's first emission is now compared, not passed.
previousis seeded frombloc.stateat subscription, so an emission equal to the current value no longer rebuilds the widget once for nothing. The old doc claimed the stream "emits immediately with the current value"; it never did (bloc.streamdoes not replay) — the doc now says so, and the widgets keep seedinginitialDatafrom state. selectWithwith a nullable projection dedupes. The oldprevious != null &&guard skipped the comparison whenever the previous value wasnull, re-emitting on every emission.previousis now typedT, sonullparticipates.
Tests #
test/bloc/state_selector_test.dart(stream) andtest/ui/juice_selector_test.dart(widget): the selector had shipped with zero tests. Pins: no replay;==dedup; ungrouped projects every emission; grouped ignores other groups entirely;rebuildAlwaysreaches a grouped selector; nullable dedup; the widget rebuilds on its group when the value changes, and not on another group, and not on an equal value.
1.7.2 - 2026-09-16 #
Changed #
- Patch, not minor: no API or behavior change.
ScopeLifecycleBloc— the framework's own bloc — now declares itsEventConcurrencymodes like every package in the family:StartScopeEvent→sequential(mutates the scopes map; atomic today),EndScopeEvent→concurrent, explicit, keeping its per-scope singleflight (getOrCreateEndingFuture): independent scopes end in parallel, the same scope ends once.
Docs #
- README: the
skipIfSamebullet (BlocSignal tee-up item 4) and the install snippet, both of which had drifted after 1.7.1 shipped.
1.7.1 - 2026-08-21 #
- Docs: README gains the 1.7.0 telemetry-pair bullet and a current install
snippet (was
^1.4.0). No code changes.
1.7.0 - 2026-08-21 #
New Features #
Use-case telemetry pair — spans become possible
Every use-case execution now logs exactly two structured entries sharing a
process-unique executionId: the existing use_case_execution start (which
gains the id), and a new end entry — use_case_completed with
elapsedMicros on success, or the existing use_case_error (which gains
executionId + elapsedMicros) on throw. A telemetry consumer (e.g.
juice_observability's DevtoolsJuiceLogger) can now draw honest duration
spans, including when same-type events overlap under
EventConcurrency.concurrent.
Note: use_case_error has two sources sharing the type — the executor's
span-closing entry (has executionId) and BlocErrorHandler's summary
(has bloc/state). Span consumers key on executionId presence.
Additive, log-schema only; no behavior changes.
1.6.0 - 2026-06-23 #
New Features #
EntityStatus — per-item async state for collections
StreamStatus is bloc-grained: it can say "the bloc is waiting/failing", not
"row 7 of a list is in flight while the rest are fine". EntityStatus models
per-item async lifecycle (a row uploading, deleting, re-reading, retrying) as
persistent, queryable STATE.
EntityStatus(sealed:EntityIdle/EntityWaiting/EntityFailure(error)) with.when/.maybeWhen— theStreamStatusvocabulary at the entity grain.EntityStatuses<K>: an immutable key→status map forBlocState(idle == absent, so it tracks work, not collection size; value equality for clean diffing).BlocUseCase.guardEntity<K, T>: brackets async work as waiting → idle, or failure(error) on throw, with cleanup guaranteed — kills the stuck-spinner footgun.read/writeclosures keep it decoupled from any state shape (a bloc can hold several status maps).- Exported from
package:juice/juice.dart. 10 tests; example-first GUIDE + reference SPEC.
Graduated from the entity-status-prototype branch after an app trial (the
Glean review re-read flow). Additive and backward-compatible.
1.5.0 - 2026-05-28 #
New Features #
Per-event-type concurrency modes
- Added
EventConcurrency { concurrent, sequential, droppable }, set per event viaUseCaseBuilder(..., concurrency: ...)(and the other builders). sequential— same-type events queue and run one at a time to completion (including their awaits), in send order. Eliminates the "read state before an await, write a stale value after" race for that event type.droppable— a same-type event arriving while one is running is dropped. Replaces hand-rolled "busy" guard flags.concurrent(default) — unchanged behavior; fully backward-compatible.- Implemented as a thin wrapper at the
EventDispatcher(per-type FIFO tail / running flag); use cases, executor, status emitter, and state manager are untouched. Queued sequential runs are skipped afterclose()(no emit-after-close).
restartable is planned for 1.6 (needs cooperative cancellation + emit
suppression for superseded runs).
1.4.0 - 2026-04-18 #
Changed #
- Repositioned Juice as a lifecycle-aware application framework rather than a generic state-management package
- Treated
packages/juiceas the canonical implementation source in repository docs and release messaging - Tightened package hygiene for publishing, including moving
flutter_testout of runtime dependencies
Documentation #
- Refreshed README and onboarding guidance to point users toward the strongest standalone examples
- Clarified that the repository-root
example/app is a showcase, not the primary architecture reference
1.3.0 - 2025-01-17 #
Improvements #
Widget Immutability & Lifecycle Management
- StatelessJuiceWidget: Removed internal bloc storage pattern for cleaner implementation
- JuiceWidgetState: Made classes
abstract(users must subclass, not instantiate directly) - Unmodifiable groups: Widget
groupsparameter is now wrapped withSet.unmodifiable()to preserve immutability - Stricter generics:
JuiceWidgetStatenow requiresJuiceBloc<BlocState>instead of rawJuiceBlocfor better type safety
JuiceWidgetState Lifecycle
- Lease acquisition moved from lazy getter to
initState()for deterministic lifecycle - Removed
setState()call from stream filter (no side effects in predicates) - Added
_lastStatusguard soprepareForUpdate()only runs on actual status changes, not parent rebuilds - Added
scopeparameter support (scope1/scope2/scope3 for multi-bloc variants) for feature parity with StatelessJuiceWidget
Internal API
- Added
BlocScope.peekExisting()andmaybePeekExisting()(internal, marked with@internal)- Read-only access to existing bloc instances without creating or incrementing lease count
- Used by StatelessJuiceWidget for cleaner bloc access after lease holder ensures instance exists
Documentation #
- Updated
prepareForUpdate()documentation to clarify it only runs on actual status changes - Added documentation noting legacy resolver users are responsible for bloc lifecycle management
1.2.0 - 2025-01-11 #
New Features #
ScopeLifecycleBloc - Reactive Scope Lifecycle Management
- Added
ScopeLifecycleBlocas a permanent bloc that tracksFeatureScopelifecycle events - Provides stream-based notifications for scope state changes:
ScopeStartedNotification- Emitted when a scope startsScopeEndingNotification- Emitted when scope cleanup begins (includesCleanupBarrier)ScopeEndedNotification- Emitted when cleanup completes (includes success/timeout status)
- Enables reactive patterns for scope-aware features
CleanupBarrier - Deterministic Async Cleanup
- Added
CleanupBarrierfor coordinating async cleanup when scopes end - Ensures in-flight operations complete or cancel before scope fully closes
- Features:
barrier.add(Future)- Register cleanup tasks- Configurable timeout (default: 30 seconds) prevents hung cleanup
cleanupCompletedflag indicates success vs timeout
// Subscribe to lifecycle notifications
lifecycleBloc.notifications.listen((notification) {
if (notification is ScopeEndingNotification) {
// Register cleanup work on the barrier
notification.barrier.add(_cancelPendingRequests());
notification.barrier.add(_saveUnsavedData());
}
});
Improvements #
Aviator Async Support
NavigateWherenow supportsFutureOr<void>for async navigation handlers- Added
navigateAsyncmethod toAviatorManagerfor awaitable navigation - Added
navigateAsynctoUseCaseContextfor use cases that need to await navigation completion
// Sync navigation (fire-and-forget)
emitUpdate(aviatorName: 'home', aviatorArgs: {'tab': 'profile'});
// Async navigation in use case
await context.navigateAsync('authCheck', {'returnTo': '/dashboard'});
Example App #
- Added "Lifecycle Demo" showcasing ScopeLifecycleBloc capabilities:
- Spawns parallel simulated async tasks with progress tracking
- Demonstrates CleanupBarrier canceling in-flight tasks on scope end
- Visual phase indicator (Idle → Active → Cleanup → Ended)
- Color-coded event log showing all lifecycle notifications
- Toggle for slow cleanup to test barrier timeout behavior
- Fixed chat example WebSocket connection (switched to maintained echo server)
1.1.3 - 2025-01-10 #
Fixes #
- Events sent to a closed bloc are now gracefully ignored with a log message instead of throwing an error
- Fixed
JuiceWidgetState,JuiceWidgetState2,JuiceWidgetState3to useBlocScopewhenGlobalBlocResolveris not configured- Previously threw
LateInitializationErrorwhen usingBlocScope.registerpattern - Now properly acquires and releases bloc leases with lifecycle management
- Previously threw
- Resolved pub.flutter-io.cn analyzer warnings:
- Removed unnecessary imports in
bloc_scope.dart,event_subscription.dart,relay_use_case_builder.dart,bloc_tester.dart - Updated constructors in
StatelessJuiceWidgetto use Dart 3 super parameters
- Removed unnecessary imports in
- Fixed CI workflow: Updated Flutter to 3.27.1 for Dart SDK 3.5.4 compatibility
Maintenance #
- Added GitHub Sponsors funding link
- Code formatting pass across all packages
Known Issues #
- 9 analyzer hints remain for
Statetype parameter naming (shadows Flutter'sStateclass)- Will be renamed to
TStatein v2.0.0 as a breaking change
- Will be renamed to
1.1.2 - 2025-01-04 #
New Features #
Inline Use Cases
- Added
InlineUseCaseBuilderfor simple, stateless operations - Reduces boilerplate for operations that don't need dedicated class files
- Features:
InlineContext<TBloc, TState>with typed state accessInlineEmitterwith cleanemit.update/waiting/failure/cancelAPISet<Object>groups support (acceptsRebuildGroup, enums, or strings)
() => InlineUseCaseBuilder<CounterBloc, CounterState, IncrementEvent>(
typeOfEvent: IncrementEvent,
handler: (ctx, event) async {
ctx.emit.update(
newState: ctx.state.copyWith(count: ctx.state.count + 1),
groups: {CounterGroups.counter},
);
},
)
Type-Safe Rebuild Groups
- Added
RebuildGroupclass for compile-time safe rebuild groups - Prevents typos, enables IDE autocomplete, supports refactoring
- Built-in
RebuildGroup.allandRebuildGroup.optOut - Extensions:
.toStringSet()and.toSet()for conversion
abstract class CounterGroups {
static const counter = RebuildGroup('counter');
static const display = RebuildGroup('counter:display');
}
// Usage
emitUpdate(groupsToRebuild: {CounterGroups.counter}.toStringSet());
Retryable Use Cases
- Added
RetryableUseCaseBuilderfor automatic retry with configurable backoff - Wraps any use case with retry logic, eliminating boilerplate
- Features:
- Configurable
maxRetries(default: 3) - Multiple backoff strategies:
FixedBackoff,ExponentialBackoff,LinearBackoff - Custom retry conditions via
retryWhenpredicate onRetrycallback for logging/metrics- Respects
CancellableEventfor early termination
- Configurable
() => RetryableUseCaseBuilder<MyBloc, MyState, FetchDataEvent>(
typeOfEvent: FetchDataEvent,
useCaseGenerator: () => FetchDataUseCase(),
maxRetries: 3,
backoff: ExponentialBackoff(
initial: Duration(seconds: 1),
maxDelay: Duration(seconds: 30),
jitter: true,
),
)
Deprecated #
UpdateEvent.newStateparameter is now deprecated- State changes should go through dedicated use cases to maintain clean architecture
- Will be removed in v2.0.0
- Use
UpdateEventonly for navigation triggers and status resets
Documentation #
- Added comprehensive dartdoc to
UpdateEventwith usage examples - Clarified correct vs incorrect usage patterns
Tests #
- Added 13 inline use case tests
- Added 10 RebuildGroup tests
- Added 15 RetryableUseCaseBuilder tests
- Total: 131 tests
1.1.1 - 2025-01-04 #
1.1.0 - 2025-01-04 #
New Features #
BlocScope Lifecycle Management
- Introduced
BlocScopefor semantic bloc lifecycle control - Added three lifecycle options:
BlocLifecycle.permanent- App-level blocs that live for entire app lifetimeBlocLifecycle.feature- Blocs scoped to a feature, disposed together viaFeatureScopeBlocLifecycle.leased- Widget-level blocs with automatic reference-counted disposal
- Added
BlocLease<T>for safe bloc access with automatic cleanup - Added
BlocScope.diagnostics<T>()for debugging bloc state - Added
BlocScope.debugDump()for development diagnostics
Cross-Bloc Communication
- Added
EventSubscriptionfor listening to events from one bloc and forwarding to another - Added
StateRelayfor simple state-to-event transformation between blocs - Added
StatusRelayfor full StreamStatus access when reacting to state changes - Added
whenpredicate filtering for both event subscriptions and relays
Deprecated #
RelayUseCaseBuilderis now deprecated in favor ofStateRelayandStatusRelayStateRelay- Use when you only need to react to state changes (most common)StatusRelay- Use when you need to handle waiting/error states- Will be removed in v2.0.0
Bug Fixes #
- Fixed race condition in
EventSubscriptioninitialization whenclose()called before microtask executes - Fixed race condition in
RelayUseCaseBuilderinitialization - Fixed unsafe dynamic cast in
UseCaseExecutor- replaced with type-safesetBloc()method - Fixed forced non-null access in
widget_support.dartwith safe null-coalescing - Fixed inconsistent default groups in
StatelessJuiceWidget2(now uses{"*"}like other variants)
Improvements #
- Added bloc type context to relay error messages for better debugging
- Added warning log when
EventDispatcheruses unhandled event fallback - Simplified verbose error throwing patterns in
JuiceAsyncBuilderwith helper getters - Removed ambiguous
_Disposableinterface fromJuiceBloc, documenteddispose()method - Added event type to state emission logs for improved observability
Tests #
- Added comprehensive test suite for
BlocScopelifecycle management (20 tests) - Added
EventSubscriptiontests covering transformation, filtering, and race conditions (10 tests) - Added
RelayUseCaseBuildertests covering relay, error handling, and multi-relay scenarios (10 tests) - Added
StateRelayandStatusRelaytests (13 tests) - Added resource cleanup tests for bloc close, stream cleanup, and lease disposal (9 tests)
- Total: 62 new tests, 93 tests overall
Documentation #
- Updated README to use
BlocScopeinstead ofGlobalBlocResolver - Added comprehensive documentation for lifecycle management
- Added cross-bloc communication examples
- Updated Best Practices with lifecycle and communication guidelines
Migration Guide #
GlobalBlocResolver is still available for backwards compatibility, but BlocScope is now the recommended approach:
// Before (still works)
GlobalBlocResolver().resolver = BlocResolver();
// After (recommended)
BlocScope.register<MyBloc>(
() => MyBloc(),
lifecycle: BlocLifecycle.permanent,
);
1.0.4 - 2025-02-08 #
1.0.3 - 2025-02-07 #
1.0.2 - 2025-01-30 #
Documentation #
- Updated
README.mdwith various improvements. - Improved package documentation for better clarity.
- Removed misleading
copyWithmention in comments withinbloc_state.dart. - Properly escaped angle brackets in dartdoc comments
Community & Support #
- Added initial setup for
FUNDING.yamlto support sponsorship options.
Maintenance #
- Version bump in
pubspec.yamlto1.0.2.
1.0.1 - 2025-01-23 #
Enhancements #
- Added
StatusChecksextension forStreamStatus:- Includes methods for type-checking (
isUpdatingFor,isWaitingFor, etc.). - Added safe casting methods (
tryCastToUpdating,tryCastToWaiting, etc.). - Introduced a
matchmethod for pattern-matching onStreamStatustypes. - Simplified handling of
StreamStatusacross widgets and logic.
- Includes methods for type-checking (
Developer Experience #
- Improved type safety and reduced boilerplate for handling transient states.
- Enhanced readability and maintainability of
StreamStatususage.
1.0.0 - 2025-01-16 #
Core Features #
- Introduced JuiceBloc with use case-driven state management
- Implemented StreamStatus
- Added group-based widget rebuilding system for performance optimization
- Created StatelessJuiceWidget for reactive UI updates
Use Case System #
- Introduced BlocUseCase for structured business logic
- Added StatefulUseCaseBuilder for singleton use cases
- Implemented RelayUseCaseBuilder for bloc-to-bloc communication
- Added UpdateUseCase for quick state updates
Navigation #
- Implemented Aviator system for declarative navigation
- Added DeepLinkAviator for handling deep linking
- Created base AviatorBase class for custom navigation handlers
Dependency Resolution #
- Added BlocDependencyResolver interface
- Implemented GlobalBlocResolver for centralized bloc management
- Created CompositeResolver for flexible dependency injection
Widgets #
- StatelessJuiceWidget and JuiceWidgetState for single bloc binding
- StatelessJuiceWidget2 and StatelessJuiceWidget3 for multiple bloc bindings
- Added JuiceAsyncBuilder for stream handling
Logging & Error Handling #
- Implemented JuiceLogger interface
- Added DefaultJuiceLogger with configurable options
- Created structured error handling system
Developer Experience #
- Added comprehensive code documentation
- Implemented type-safe APIs throughout
- Created builder patterns for common operations
Initial Contributors #
- Kevin Ehmka
Note: This is the first stable release of Juice, a state management solution designed to provide a clean architecture plus bloc approach to Flutter applications.