flutter_query_client 2.0.2
flutter_query_client: ^2.0.2 copied to clipboard
A TanStack Query-inspired server state management library for Flutter. Handles caching, background refetching, pagination, mutations, and network-aware fetching out of the box.
2.0.2 #
Fixes #
-
Fixed:
StaleListenerHandlememory leak for void-params queries —unregister()had an extra&& _listenedParams != nullguard that prevented cleanup whenparamswasnull(the common case for controllers with no params). Every parameterless controller leaked a stale callback onunregister(). Guard removed — only callback presence is checked now. -
Fixed:
_refetchInternalrace condition — params were read from_serializedParamsafterawait, so a concurrentsetParams()call could silently write results to the wrong cache key and emit state for a different query. Both controllers now capture_serializedParamsbeforeawaitand abort if it has changed after the fetch completes. -
Fixed:
copyWithstale fields bleeding across state transitions —QueryController._refetchInternal,InfiniteQueryController._executeFirstPage,InfiniteQueryController._refetchInternal, andMutationController.mutateall usedstate.copyWith(status: ...)on the error path. Because Freezed'scopyWithpreserves unmentioned fields, previousisPlaceholderData,error, orfetchStatusvalues could bleed into the new error state. All error emits now use freshQueryState<T>(...)constructors with every field set explicitly. -
Fixed:
InfiniteQueryController.setParamscache-hit not registering stale listener or refetch interval — whensetParamsfound a fresh cache hit it returned early without calling_registerStaleListener()or_startRefetchInterval(), so the stale callback and polling interval were never set up for the new params. Both are now called before the early return. -
Fixed:
_shouldPauseevaluated eagerly in_execute—shouldAbortwas constructed as_shouldPause ? () => true : null(evaluated once at call time), so if the controller was paused after the network call started, the abort flag was invisible to the retry loop. Changed to() => _shouldPause(re-evaluated on each retry iteration). -
Fixed:
ensureDatamissing params staleness guard — after theawaitinensureData,_serializedParamswas not rechecked, so a concurrentsetParams()could cause stale data to be returned and emitted for the wrong params. Added the same capture-and-check pattern used in_refetchInternal. -
Fixed:
QueryClient.clear()not clearing_staleCallbacks—clear()cancelled stale timers but left_staleCallbackspopulated, so callbacks for already-cleared entries could fire if a timer somehow ran before cancellation, or persist as a memory leak for long-running apps that callclear()between sessions. -
Fixed:
_onConnectivityChangepermanently removing throwing callbacks — when a reconnect callback threw, it was caught and added to a pruning set, permanently unregistering a live controller's reconnect callback on the first transient error. Changed to log-and-continue; stale callbacks are handled atunregisterReconnectCallbacktime. -
Fixed:
InfiniteQueryController.loadMoreerror not resettingfetchStatustoidle— on aloadMorefailure,fetchStatusstayed asfetching, leaving the controller in a stuck state where the UI could never trigger anotherloadMorecall.fetchStatus: FetchStatus.idleis now set explicitly in the error emit. -
Fixed:
handleRemountnot restarting the refetch interval —handleRemountcalled_refetch()but not_startRefetchInterval(), so polling stopped permanently after the first widget hide/show cycle. The interval is now restarted alongside the refetch. Note:handleRemount(and thereforerefetchOnMount) has no effect when the controller's provider is mounted at the root level — root providers are never unmounted, so the hidden→visible transition never fires.
2.0.1 #
Fixes #
- Fixed: original error lost after retry exhaustion —
retryWithBackoffwas wrapping the user's error in aQueryException('Operation failed after N attempt(s)'), burying the original API/service error.transformError,onMutationError,onQueryError, andstate.errorall received theQueryExceptionwrapper instead of the actual error thrown byqueryFnormutationFn. Now the original error is rethrown with its original stack trace after retries are exhausted, so it flows through the entire error pipeline unchanged. TheQueryExceptionabort case (network offline) is unaffected.
Example #
- Updated global
transformErrorinmain.dartto demonstrate the corrected pattern — checking for the app'sApiExceptiontype instead ofQueryException - Added Error Handling demo section to the Widgets showcase screen with a deliberately failing mutation that shows
state.errorvalue and type, proving the original error flows throughtransformErrorintact
2.0.0 #
Breaking changes #
-
MultiQueryProvider.providerstype changed fromList<Widget Function(Widget child)>toList<QueryProviderWidget>— callers must replace builder functions with plain provider instances:// Before (1.2.0) MultiQueryProvider( providers: [ (child) => QueryProvider<PostsController, List<Post>>( create: (_) => PostsController(), child: child, ), ], child: HomeScreen(), ) // After (2.0.0) MultiQueryProvider( providers: [ QueryProvider<PostsController, List<Post>>( create: (_) => PostsController(), ), ], child: HomeScreen(), ) -
MutationController<T>→MutationController<T, P>— mutations now take a typed params genericP, matching theQueryController<T, P>pattern. Subclasses must overridemutationFn(P params)instead of passing a closure tomutate(). For mutations that don't need params, usevoidas the second type argument.// Before (1.2.0) class CreatePostMutation extends MutationController<Post> { Future<void> create(String title) async { await mutate(() => postService.createPost(title: title)); } } // After (2.0.0) class CreatePostMutation extends MutationController<Post, ({String title})> { @override Future<Post> mutationFn(({String title}) params) { return postService.createPost(title: params.title); } } // Usage: context.query<CreatePostMutation>().mutate((title: 'Hello'))
New features #
QueryProviderWidget— new public abstract base class that bothQueryProviderandInfiniteQueryProviderextend, enablingMultiQueryProvidercomposition; custom provider wrappers can also extend it to participate inMultiQueryProviderQueryClientProvider(observer:)— register aQueryObserverdirectly inQueryClientProvideralongside defaults and logging, keeping all global setup in one place; the constructor callsQueryClient.setObserverinternally.QueryClient.setObserverremains available as a lower-level escape hatch for registering outside the widget treeQueryDefaults.initialPageParam— global default first-page parameter for everyInfiniteQueryController; defaults to0. Set to1for one-indexed APIs or any custom value for cursor-based APIs — no per-controller override needed unless that controller differs from the global defaultQueryDefaults.limit— global default page size for everyInfiniteQueryController; defaults to20. Set once inQueryClientProviderinstead of repeating@override int get limit => Nin every subclassInfiniteQueryController.initialPageParam— no longer abstract; falls back toQueryDefaults.initialPageParamcast toPageParam. Must still be overridden when the controller'sPageParamtype or starting value differs from the global defaultInfiniteQueryController.limit— now falls back toQueryDefaults.limitrather than a hardcoded20; override per-controller when needed
Performance & memory optimizations #
- Fixed:
NetworkConnectivityObserverStreamController leak — the broadcastStreamControllerwas eagerly allocated and never closed (singleton lifetime). It is now created lazily and tracks active listeners viaonListen/onCancelcallbacks. AddedlistenerCountgetter for diagnostics - Fixed:
QueryClientsingleton had no diagnostic visibility — addedactiveStaleTimerCount,activeGcTimerCount,activeInvalidateCallbackCount,activeReconnectCallbackCount, andcacheEntryCountgetters for debugging timer and callback leaks - Fixed: dangling callback references in
QueryClient— if a controller was garbage-collected withoutclose(), its_onInvalidateand_onReconnectcallbacks persisted forever._notifyInvalidateCallbacksand_onConnectivityChangenow catch exceptions from stale callbacks and auto-prune them - Fixed: unnecessary deep copies in
InfiniteQueryController._saveToCache— replacedList<T>.from(p)(O(n) element-by-element copy) withUnmodifiableListView<T>(p)fromdart:collection(O(1) zero-copy wrapper)._restoreFromCacheandhandleRemountuseList<T>.of()instead ofList<T>.from()to skip per-element type checks.handleRemountno longer allocates a flat list just to compare lengths - Fixed: flat-cache thrashing on optimistic updates —
updateItemnow patches_flatCachein-place via_flatIndexOf()instead of nulling and rebuilding the entire flat list.prependItemandappendIteminsert/add directly into_flatCache. OnlyremoveItem(which changes list length) invalidates the cache - Fixed: redundant stale-listener re-registration —
StaleListenerHandle.register()now returns early when params are unchanged, avoiding unnecessarySet.remove()+ closure allocation +Set.add()on every fetch. AddedisRegisteredandregisteredParamsgetters - Fixed: redundant
isStalecache lookups on reconnect —_handleReconnect()in bothQueryControllerandInfiniteQueryControllerno longer performs a redundantclient.get(key)?.isStalelookup;state.isStale(maintained by the stale-timer callback) is sufficient
Notes #
QueryClient.updateInfiniteQuery<T>—- Preferred (controller reachable via
BuildContext) — call the controller's own helpers directly:prependItem,appendItem,updateItem, orremoveItem. These patch_flatCachein O(1) and emit a new state immediately.updateInfiniteQueryis then optional (use it when provider access is not present in the context - and you will see changes when navigated to desired page).
- Preferred (controller reachable via
Example #
- Added Issues tab to the example app with interactive before/after benchmarks for every optimization above; full documentation extracted to
example/lib/features/inefficiency_demos/OPTIMIZATIONS.md - Updated Posts form screen to use
MutationController<T, P>typed params,MultiQueryListener, andQueryClient.instance.updateto patch the flat'posts'cache from within mutation listeners — noflutter_blocimport required - Updated Products form screen to use
MutationController<T, P>typed params andMultiQueryProvider/MultiQueryListener; the paginated list screen demonstratesupdateInfiniteQuery+prependItemto sync both the cache and the live controller after a create - Updated Posts list screen to sync the live
PostsQueryControllerfrom the already-patched cache viaupdateCache((posts) => posts)after a create, avoiding a double-prepend
1.2.0 #
New features #
MultiQueryProvider— nest multipleQueryProviderandInfiniteQueryProviderwidgets without deep indentation; providers are applied top-to-bottomQueryConsumer<C, T>— combinesQueryBuilder+QueryListenerin a single widget, eliminating the need to nest them; works with bothQueryControllerandMutationControllerInfiniteQueryConsumer<C, T>— same asQueryConsumerfor infinite queries; theList<T>wrapper is baked into the type so only the item type is requiredQuerySelector<C, T, S>— aBlocSelectorscoped toQueryState<T>; rebuilds only when the selected derived valueSchanges, ideal for counters, flags, and other narrow slices of stateInfiniteQuerySelector<C, T, S>— same asQuerySelectorfor infinite queries;List<T>is baked inInfiniteQueryListener<C, T>— mirrorsInfiniteQueryBuilderfor the listener side; eliminates the verboseQueryListener<C, List<T>>type annotationQueryObserver— aBlocObserversubclass that filters events to query and mutation controllers and re-exposes them as typed, cache-key–aware hooks (onQueryCreate,onQueryChange,onQueryError,onQueryClose);onQueryChangereceives bothcurrentStateandnextState, matching standardBlocObserver.onChangesemantics- Mutation widgets —
MutationController<T>emitsQueryState<T>, soQueryBuilder,QueryListener,QueryConsumer, andQuerySelectorall work with mutations out of the box; no separateMutationBuilderorMutationListenerneeded
Example #
- Added a Widgets tab to the example app with live interactive demos of every widget in the package, including all mutation-controller combinations
1.1.0 #
Fixes #
- Fixed race condition in
InfiniteQueryController._executeFirstPagecache-hit branch — missing_filterVersioncheck afterawait Future.delayed(Duration.zero)could cause stale filter data to be emitted ifsetParamswas called concurrently - Fixed race condition in
InfiniteQueryController._executeFirstPagepause branch — same missing version check allowed apausedstate emit to overwrite state set by a concurrentsetParamscall - Fixed
InfiniteQueryController.loadMorenot calling_startRefetchInterval()on success — polling would never start ifloadMorewas the first successful fetch operation
1.0.1 #
Fixes #
- Removed unnecessary
package:meta/meta.dartimport fromQueryControllerandInfiniteQueryController— elements are already available viapackage:flutter/foundation.dart - Removed
@internalannotation fromhandleRemount()in both controllers - Fixed unresolved dartdoc references in
QueryLoggerandRefetchOnMount— replaced[Logger.root.onRecord]and[staleTime]with backtick code spans
1.0.0 #
Initial stable release.
Features #
- QueryController — fetch and cache server data with automatic stale-while-revalidate, retry with exponential backoff, refetch on mount, and refetch on reconnect
- MutationController — user-triggered mutations with lifecycle hooks (
onSuccess,onMutationError,onSettled) and optimistic cache update support - InfiniteQueryController — paginated / infinite-scroll queries with
loadMore(),hasMore, cursor or page-number pagination, and item-level cache helpers (updateItem,removeItem,appendItem,prependItem) - QueryClient — singleton two-level cache (
baseKey+ serialized params) with stale-time tracking, garbage collection, and active observer registry - QueryClientProvider —
InheritedWidgetfor injectingQueryClientand globalQueryDefaultsinto the widget tree - QueryProvider / InfiniteQueryProvider —
StatefulWidgetwrappers with remount detection viaTickerModeforIndexedStackandVisibilitysupport - QueryBuilder / InfiniteQueryBuilder — reactive builders that rebuild on state changes
- QueryListener / MultiQueryListener — side-effect widgets that respond to success and error without rebuilding the tree
- QueryState — Freezed-based immutable state with
status,fetchStatus,isStale, and convenience getters - NetworkConnectivityObserver — true L7 connectivity verification (HTTP HEAD), debounced events (500ms), lazy initialization
- QueryDefaults — global configuration for stale time, gc time, retry count, retry delay, refetch interval, network mode, error transform, and logging
- QueryLogger — opt-in structured logging with customizable handlers