smart_list 1.0.0
smart_list: ^1.0.0 copied to clipboard
Production-ready paginated lists for Flutter: search, pull-to-refresh, caching, retries, and customisable UI states — wired up with one controller and one widget.
Changelog #
All notable changes to this package are documented here. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
1.0.0 — 2026-08-29 #
First stable API. Core still depends only on Flutter.
Breaking #
SmartListFetchernow receivesSmartListCancelTokenas the second argument.- Filters are
Map<String, Object?>(SmartListFilters). - Default
RetryPolicyretries only transient errors (timeouts / typical I/O type names), not everyException. UseRetryPolicy.aggressive()for the old behaviour. SmartListCacheKeyincludes optionallistId;invalidateScopeacceptslistId.SmartListControllerextendsChangeNotifierand implementsValueListenable(no publicvaluesetter). The full constructor takescache:instead ofenableCache:(SmartListController.simplestill hasenableCache).
Added #
SmartListCancelToken— cooperative cancel on refresh/search/dispose.SmartListSliverforCustomScrollView.listIdonSmartListControllerfor shared cache stores.- Load-more fires on threshold crossing, not every scroll notification.
uniqueKeyapplies to inserts; mutations invalidate the current cache scope.- Search keeps previous items visible while a search fetch is in flight (
isSearchLoading). - Example
JsonFileCacheStore(not a core dependency). insertAtBottom,loadMoreErrorBuilder,controller.statealias (from 0.1.0).
Changed #
- Cache is documented as a fetch snapshot, not a live store.
0.1.0 — 2026-05-24 #
A correctness, concurrency, and performance sweep based on a full code review. 28 issues addressed across the controller, widget, caching, and pagination layers. Two breaking changes — both narrow, both with clear migration paths.
⚠️ Breaking #
-
SmartListControllerno longer extendsValueNotifier. It now extendsChangeNotifierandimplements ValueListenable<SmartListState<T>>.- Still works: reading
controller.value(or the new aliascontroller.state); subscribing viaValueListenableBuilder(valueListenable: controller, ...). - No longer compiles:
controller.value = Xfrom external code. External writes were a footgun — they bypassed the controller's internal bookkeeping. Use the mutation APIs (insertAtTop,applyFilters,refresh,reset, etc.) instead.
- Still works: reading
-
SmartListController(...)no longer acceptsenableCache:. The full constructor now takes a singlecache:parameter:cache: null(or omitted) → no cache.cache: someStore→ use that store.
Migration: drop any
enableCache: false; add an explicitcache: MemoryCacheStore<T>()if you previously relied on the implicit default.SmartListController.simplestill acceptsenableCache: truefor the common case — no migration needed there.
Added #
insertAtBottom(item)— companion toinsertAtTop; appends to the end of the data list. Documentedreverse: truesemantics for chat-style layouts.loadMoreErrorBuilderslot onSmartListViewfor inline pagination-error footers. Defaults to a compact "Failed to load more / Retry" row instead of the full-screen error widget.SmartListController.state— alias forcontroller.value, readable shorthand outsideValueListenableBuildercontexts.
Fixed — correctness #
- Equality / hashCode contract for
SmartListStateandSmartListCacheKey. Previously hashedfilters.entries;MapEntryhas no value-equality, so equal filter maps produced different hashes — silently breaking cache lookups. - Separator off-by-one in
SmartListView— the divider between the last two real items was being suppressed along with the intended footer-slot suppression. SmartListPage.empty().totalCountis nownull(unknown) instead of0(server reported zero).
Fixed — concurrency #
RetryPolicy.runnow honours cancellation. NewshouldContinuecallback aborts the retry chain viaSmartListCancelledException. The controller wires it to!_disposed && token.isCurrent, so retries no longer continue past dispose or stomp on superseded requests.onRetryalso guards against post-dispose mutations.- Cache-bypass intent is now per-call, not a controller-wide flag. Concurrent fetches can no longer steal each other's bypass.
applyFiltersemits a single coherent state transition instead of flashing{new filters, old phase, old items}first.- Pending debounced searches are cancelled when
refresh()/loadInitial(force: true)/applyFilters()fires, so a stale search can't re-enter search mode after the reset. - Concurrent
loadNextPage()calls are serialized via an internal lock — extra callers await the in-flight result and return without firing a duplicate fetch.
Fixed — widget / UI #
- Pull-to-refresh works on every placeholder state (loading /
error / empty). Placeholders are now wrapped in a viewport-tall
scrollable so the
RefreshIndicatoralways has a target. Debouncer(Duration.zero)always schedules asynchronously (previously fired synchronously, which could trigger "setState called during build" when invoked from a build pass).- Internal
ScrollControlleris not resurrected after dispose — the lazy getter returnsnullinstead of leaking a fresh one. clearSearchpreserves real-time edits made during search.insertAtTop,removeWhere, andupdateWheremutations now replay against the pre-search snapshot so they survive the restore.insertAtIndexis unchanged (positional, no semantic mapping) and that exclusion is documented.
Performance #
List<T>.unmodifiable(...)→UnmodifiableListView<T>(...)in the controller. Same immutability contract, zero-copy wrap. Microbenchmarked at ~400× faster for a 10k-item list (seetest/benchmark_test.dart)._mergeItemsis now O(M) instead of O(N + M) per page. The dedupe seen-set is persisted per phase and reset on fresh sequences.- Split-rebuild for
SmartListView. The populatedListViewbody is extracted into a dedicated widget passed viaValueListenableBuilder'schild:slot. It subscribes to the controller independently and onlysetStates whenitems,phase, orerroractually change — filtering out query / filters /retryAttemptnotifications. Benchmarked: a refresh cycle triggers ~50% feweritemBuildercalls (therefreshingtransition preserves the items reference and no longer rebuilds the body); retry chains with N attempts skip N body rebuilds entirely. - Scroll-end handler bails when
maxScrollExtent <= 0— short lists no longer fireloadNextPageon every metric change.
Docs #
README.md: dropped the inaccurate "two lines" tagline; documented the 5-minute default cache TTL; clarified thatSmartListController.simpleis page-pagination only.CHANGELOG.md: corrected the "56 tests" /LLD.mdreferences in 0.0.1.- Dartdoc clarifications on
refresh()-during-search,clearSearchstale-pagination,applyFilters({})no-op semantics,SmartListPage.itemsimmutability contract, stale cache-write behaviour, and reverse-modeinsertAtTop.
0.0.2 — 2026-08-28 #
Fixed #
- Pagination strategies now peek the next request and commit only after
a successful apply, so a failed "load more" retries the same page instead of
skipping it. The controller also reuses
_failedRequestfor custom strategies that still mutate innextRequest. - Bypass
refresh()invalidates the current query+filters cache scope so later pages cannot mix with a freshly fetched page 1. AddedSmartListCacheStore.invalidateScope. - Disposing the controller during an in-flight fetch no longer notifies a disposed notifier.
- Cursor paging treats
hasMore: truewith a nullnextCursoras end-of-list (avoids re-requesting page 1 forever). clearSearch()afterapplyFilters()during a search refetches the browse list instead of restoring a stale snapshot.- Empty / error / loading states are wrapped in a scrollable so pull-to-refresh
works when
enableRefreshis true.
Added #
SmartListPaginationStrategy.commit(default no-op; built-ins implement it).SmartListCacheStore.invalidateScope.
0.0.1 — 2026-05-02 #
Initial release. A unified, production-ready abstraction for paginated, searchable, cached lists in Flutter.
Added #
Controller & state
SmartListController<T>extendingValueNotifier<SmartListState<T>>— drop-in compatible withsetState, Provider, Riverpod, GetX, and BLoC with no adapter.- Immutable
SmartListState<T>with derived booleans (isInitialLoading,isLoadingMore,isRefreshing,hasError,isEmpty,isSearchActive,isSearchEmpty,hasReachedEnd). SmartListPhaseenum:initial/loading/loadingMore/refreshing/success/error.- Public API:
loadInitial({force}),loadNextPage(),refresh({bypassCache}),search(query),clearSearch(),applyFilters(filters),insertAtTop,insertAtIndex,removeWhere,updateWhere,reset(),clearCache(). - Race-condition guard via
RequestToken— superseded responses are silently discarded; old slow responses can never overwrite newer state. - Pre-search snapshot / restore:
clearSearch()returns the user to exactly where they were before searching.
Pagination (strategy pattern)
SmartListPaginationStrategy<T>interface.PagePaginationStrategy<T>—?page=N&size=M(default in.simple).CursorPaginationStrategy<T>— opaque-cursor APIs.OffsetPaginationStrategy<T>—?offset=N&limit=M.- End-of-list inference: explicit
hasMore→ strategy-specific signal (null cursor / short page / empty page).
Cache
SmartListCacheStore<T>abstract interface (in-memory today; pluggable for disk / network caches).MemoryCacheStore<T>with TTL expiry, optional LRU eviction (maxEntries), and an injectable clock for deterministic testing.- Composite
SmartListCacheKeykeyed on query + filters + page + cursor. refresh()bypasses the cache read by default while still writing the fresh response — opt out withrefresh(bypassCache: false).
Resilience utilities
Debouncer— coalesces rapid search keystrokes into a single fetch.RetryPolicy— exponential backoff with jitter, configurablemaxAttemptsandshouldRetrypredicate;RetryPolicy.none()factory.RequestToken— monotonically increasing token for race-condition guards.
Widget layer
SmartListView<T>composingListView.separated,RefreshIndicator, andNotificationListener— auto-pagination vialoadMoreThreshold.- Builder slots:
itemBuilder,separatorBuilder,loadingBuilder,loadingMoreBuilder,emptyBuilder,searchEmptyBuilder,errorBuilder,footerBuilder(genericSmartListFooterBuilder<T>— preserves item-type safety). DefaultSmartListStates— sensible Material 3 defaults for every state.mounted+ controller-identity guards on the post-frame initial-load callback; controller swaps handled viadidUpdateWidget.
Real-time updates & deduplication
insertAtTop/insertAtIndexfor prepend / arbitrary insert.updateWhere/removeWherefor bulk mutations.- Optional
uniqueKeyextractor — collapses duplicates across pages.
Filters
applyFilters(Map<String, dynamic>)— re-fetches from page 1; no-op when filters are unchanged. Filters are propagated to the fetcher viaSmartListPageRequest.filters.
Documentation & examples
- Comprehensive Dart-doc comments on every public symbol.
README.mdwith quickstart, customisation guide, pagination styles, state-management interop examples, and full API reference.example/app demonstrating pagination, debounced search, pull-to-refresh, simulated transient failures with auto-retry, and a custom empty-state builder.
Testing
- Tests covering controller flow, pagination strategies, cache semantics, debouncer, retry policy, request token, state derivations, and widget UI states.