flutter_spine 0.2.6
flutter_spine: ^0.2.6 copied to clipboard
Shared infrastructure for Flutter apps — MVVM base classes, effect bus, scaffold suite, network layer, and error model.
0.2.6 #
Removed #
- Breaking: entire pagination module deleted (
lib/src/pagination/):PagedState,PagedNotifierMixin,PagedNotifierMixinNoArg,PagedController,PagedScaffold. Business implements its own list state class + plainAsyncNotifierwith hand-writtenrefresh/loadMore(see migration below). - Breaking: CLI
paged-listcommand deleted (command + 4 templates).flutter_spine:new feature --variant=listno longer exists —--variantis nowpage | async | form. - Lint supertype lists (
avoid_static_mutable_in_notifier,no_ui_in_viewmodel) no longer reference the removed pagination mixins.
Changed #
- README §4 "Pattern C" rewritten as a business-side pagination reference implementation: custom
TaskListState+AutoDisposeAsyncNotifier+refresh()/loadMore()+RefreshIndicator/NotificationListenerUI. - Example
tasks_vm.dartrewritten as the manual pagination reference:TaskListState(items/page/hasMore/isLoadingMore/moreError), snapshot-based optimistic update + rollback. - Example
tasks_tab.dartupdated to the new state type.
Migration #
// 之前(≤0.2.5)
class TasksVm extends AutoDisposeAsyncNotifier<PagedState<Task>>
with PagedNotifierMixinNoArg<Task> {
int get pageSize => 20;
Future<List<Task>> fetchPage(int page, int size) => repo.list(page: page, size: size);
}
// 之后(0.2.6+,业务侧完全自实现,约 40 行)
@immutable
class TaskListState {
const TaskListState({this.items = const [], this.page = 1,
this.hasMore = true, this.isLoadingMore = false, this.moreError});
final List<Task> items; final int page;
final bool hasMore; final bool isLoadingMore; final Object? moreError;
// copyWith / isEmpty ...
}
class TasksVm extends AutoDisposeAsyncNotifier<TaskListState> {
static const _pageSize = 20;
@override
Future<TaskListState> build() => _fetch(1);
Future<TaskListState> _fetch(int page) async { ... }
Future<void> refresh() async { await future; ref.invalidateSelf(); await future; }
Future<bool> loadMore() async { ... }
}
0.2.5 #
Removed #
- Breaking:
easy_refreshdependency removed — flutter_spine no longer provides pull-to-refresh / load-more UI machinery. - Breaking:
PagedListViewdeleted (lib/src/pagination/paged_list_view.dart), includingPagedScrollViewBuilder. Business builds its own list UI:PagedScaffold+RefreshIndicator/NotificationListener/ own refresh framework, callingPagedController.refresh()/loadMore(). - Breaking:
AppListPageScaffolddeleted. UsePagedScaffold(state machine: loading / error / empty / data) + business-side list. - Breaking:
AppTabChildScaffolddeleted. ComposeEffectListener(source: ..., handleDefaults: false)+AutomaticKeepAliveClientMixindirectly (the class was a 66-line thin wrapper). - Breaking:
FilterNotifierdeleted (lib/src/filter/). Write a plainNotifier<F>subclass instead (initial/set/update/resetare trivial to replicate). avoid_raw_scaffoldlint message updated: no longer suggests the removed scaffolds.
Changed #
PagedNotifierMixindoc:PagedControlleris now driven by business-side UI triggers.PagedScaffolddoc examples updated to business-side refresh pattern.- CLI
paged-listpage template rewritten: generatesPagedScaffold+ListView+RefreshIndicatorexample with TODO markers where business adds its own load-more trigger. flutter_spine.dartexports updated (4 export lines removed).
Example #
tasks_tab.dartrewritten as the reference implementation:RefreshIndicator+NotificationListener<ScrollNotification>load-more +AsyncValue.whenfirst-loading/error +MoreErrorBarfooter.- Removed
demo_paged_list_page.dart/demo_app_list_page.dartdemos (router + demos index updated).
Fixed #
test/pagination/paged_notifier_mixin_test.dart— family provider reads updated to pass an argument (pre-existing breakage on Riverpod 2.6.1: reading_fakeListProvider.futurewithout an arg threw a null cast).
Migration #
// 之前:PagedListView 全自动(内置 easy_refresh)
PagedListView<Task>(
provider: tasksVmProvider,
controllerProvider: tasksVmProvider.notifier,
itemBuilder: (ctx, task, _) => TaskTile(task),
)
// 之后:业务自己组合
RefreshIndicator(
onRefresh: () => ref.read(tasksVmProvider.notifier).refresh(),
child: NotificationListener<ScrollNotification>(
onNotification: (n) {
if (n.metrics.pixels > n.metrics.maxScrollExtent - 200) {
ref.read(tasksVmProvider.notifier).loadMore();
}
return false;
},
child: ListView.builder(...),
),
)
0.2.4 #
Changed #
- Breaking:
DioHttpConfig.authRefreshandDioHttpConfig.retryfields removed.AuthRefreshInterceptorandRetryInterceptorare no longer auto-registered byDioHttpClient.fromConfig(). Business code should explicitly add these interceptors viaDioHttpConfig.interceptors(forAuthTokenInterceptor/HttpLoggingInterceptor/EnvelopeUnwrapInterceptor) or by assembling aDioinstance withDioHttpClient.fromDio()(forAuthRefreshInterceptor/RetryInterceptorwhich require a Dio reference). - Built-in interceptor classes (
AuthTokenInterceptor,HttpLoggingInterceptor,EnvelopeUnwrapInterceptor,AuthRefreshInterceptor,RetryInterceptor,AuthRefreshConfig,RetryConfig) are preserved — business may still use them, just must register them explicitly. DioHttpConfig.interceptorsdoc updated to clarify it accepts any DioInterceptor(built-in or custom).
Added #
HttpClient.requestStream()— streaming request method for SSE / large file downloads / long-poll push. ReturnsStreamedHttpResponsewithstatusCode/headers/stream(Stream<List<int>>). Error normalization matchesrequest()— non-2xx / network errors / timeouts throwAppException.StreamedHttpResponse— immutable stream response wrapper withstatusCode,headers,stream,isSuccess, andheader()lookup.HttpResponseType.stream— new enum value; maps to Dio'sResponseType.stream.- 3 new tests in
dio_http_client_test.dartcovering stream response, 401 error, and cancelToken cancellation.
Docs #
- README §1.5 RetryInterceptor / §1.6 AuthRefreshInterceptor / §1.7 内置 Interceptor 速查 updated to show
DioHttpClient.fromDio()assembly pattern. - README §1.8 流式响应 / SSE added.
AiHelper/skills/flutter-core-http-setup/SKILL.mdupdated: removedauthRefresh/retryfields, added RetryInterceptor/AuthRefreshInterceptor assembly sections, added SSE/stream section.
Tests #
http_auth_refresh_interceptor_test.dartandhttp_retry_interceptor_test.dartrefactored to useDioHttpClient.fromDio()instead ofDioHttpConfig.authRefresh/.retry.
0.2.3 #
Fixed #
_defaultFactoryURL construction — switched to pure string operations to avoid Dart SDKUri.replace()port:0bug. Added scheme auto-correction (http→ws,https→wss) and port:0removal. Added step-by-step debug logging.isConnectAuthErrormissing fromWsModuleConfig— field was added toWsClientConfigbut not propagated throughWsModuleConfigconstructor,toConfig(), andtoConfigWith(), causing configuration loss when usingWsModuleRegistry.WsModuleConfigmissing reconnect fields —protocols,connectTimeout,baseReconnectDelay,maxReconnectDelay,maxReconnectAttempts,reconnectJitterRatiowere not inWsModuleConfig, preventingsharedWsConfigdefaults from reaching the finalWsClientConfig.- Connect auth refresh infinite loop —
_reconnectAttemptwas reset to 0 on every connect-level auth refresh, preventingmaxReconnectAttemptsfrom ever being reached. - EffectListener double-dispatch — root-level
EffectListenernow defaults tohandleDefaults: false.AppTabChildScaffoldandAppBottomSheetScaffoldsethandleDefaultEffects: false. OnlyAppPageScaffoldprocesses built-in effects, eliminating duplicate navigation/toast/dialog triggers. - DemosPage double-navigation (example) — parent route container now sets
handleDefaultEffects: falseto prevent processing effects emitted by child routes' ViewModels.
Example #
ws_modules.darttoken extracted to_currentTokenvariable —onAuthExpiredwrites back refreshed token,queryParamsProviderreads it on each connect/reconnect.demo_market_ws_page.dartrefactored to use globalmarketGatewayProviderinstead of self-built fake channel and hardcoded URL.
0.2.2 #
Added #
BaseWsGateway— abstract WebSocket gateway for business modules. Delegates connection lifecycle toWsClient; subclasses (Market / Asset / Swap) define type-safe subscription APIs and topic encoding.WsClientConfig.headersProvider— dynamic headers callback. Called on every connect / reconnect to fetch the latest token, eliminating the need to rebuild config after auth refresh.WsClientConfig.queryParamsProvider— dynamic query string callback. Same pattern asheadersProvider, for backends that pass auth tokens via URL query params (?token=xxx) instead of HTTP headers. Manual string concatenation avoids Dart SDKUri.replace()port:0bug.WsClientConfig.onAuthExpired+isAuthCloseCode— token expiry auto-refresh. When the server closes with an auth close code (e.g. 4001),DefaultWsClientcallsonAuthExpiredwith single-flight guarantee, then reconnects with the new token. Unsuccessful refresh transitions toWsFailed.WsClientConfig.isConnectAuthError— connection-level auth error detection. Handles HTTP 401/403 rejection during WebSocket upgrade handshake (complementsisAuthCloseCodewhich handles post-connect close frames). Defined externally via predicate so backend-specific error formats are not hardcoded.- Close code handling in
DefaultWsClient— normal close codes (1000, 1001) now transition toWsDisconnectedwithout triggering auto-reconnect. All other close codes continue to trigger standard reconnect. - Unified
IOWebSocketChannelin_defaultFactory— always usesIOWebSocketChannel.connect(), no longer implicitly switches betweenWebSocketChannel.connect()andIOWebSocketChannelbased on parameter presence. Behavior is now consistent regardless of whether headers/queryParams are configured. WsTopicRouter.simple()— factory constructor for standard pub/sub protocols where channel name equals topic name. Auto-generatestopicExtractor,subscribeFrameBuilder, andunsubscribeFrameBuilderfrom a singlechannelKeyparameter.WsModuleRegistry+WsModuleConfig— registration pattern for WebSocket modules. Each business module defines aWsModuleConfiginstance;WsModuleRegistry.build()maps URIs to configs, replacing hand-written if-else chains inwsConfigBuilderProvideroverrides.- CLI
ws-gatewaycommand —flutter_spine:new ws-gateway <name>generates topic / topic_router / ws_gateway / providers four-file scaffold.
Changed #
- Breaking:
WsClientConfig.headersreplaced byheadersProvider(Map<String, dynamic> Function()?). Existing code must change fromheaders: {'key': 'val'}toheadersProvider: () => {'key': 'val'}. WsClientConfigconstructor now acceptsheadersProvider,queryParamsProvider,onAuthExpired, andisAuthCloseCode(all optional)._defaultFactoryno longer branches on parameter presence — always constructsIOWebSocketChannelfor consistent behavior.
Tests #
- 9 new test cases for close code handling, auth refresh, and connect-level auth detection (40 total in
test/network/ws/).
Example #
demo_market_ws/— fullMarketWsGatewayimplementation: topic encoding/decode (MarketTopic), protocol adapter (marketTopicRouter), RiverpodStreamProvider.autoDispose.familyfor automatic subscription lifecycle, and interactive lifecycle demo page.demo_asset_ws/&demo_swap_ws/— showcase multi-module Gateway pattern. All three modules share the same auth/heartbeat/reconnect config via a_sharedWsConfigfactory inmain.dart, each overriding only its owntopicRouter.main.dartnow demonstratesFlutterSpineConfig.extraOverrideswithWsModuleRegistry.build()replacing the if-else chain.
0.1.2 #
Added #
PagedListView.scrollViewBuilder— embed the list in aCustomScrollViewwith extra slivers.PagedListView.enableLoadMore— disable load-more footer, keep only pull-to-refresh.AppListPageScaffold.scrollViewBuilder/enableLoadMore— forwarded toPagedListView.PagedScrollViewBuildertypedef.- Example demo pages:
/demos/paged-list,/demos/app-list.
Fixed #
DioExceptionType.transformTimeoutnot found with dio 5.9.x — replaced withdefault/_fallback for forward compatibility.
0.1.1 #
Added #
- Scaffold CLI (
flutter_spine:new). generator_templates.dart— riverpod_generator support.FeatureCommand—flutter_spine:new featureone-key whole feature generation.BootstrapCommand—flutter_spine:new bootstrapapp skeleton.FlutterCoreDiagnosticsBanner.MaterialDefaultEffectHandlerctor overrides.mixin-based API:ViewModelMixin,AsyncViewModelMixin, family variants.
Changed #
- CLI templates now use
{{Name}}/{{name}}/{{name_snake}}/{{name-kebab}}/{{Title}}naming conventions. - Renamed
FlutterCoretoFlutterSpine,FlutterCoreConfigtoFlutterSpineConfig.
0.1.0 #
- Initial release.
error/: sealedAppExceptionhierarchy +safeApiCallnormalization.network/:ChannelClientMethodChannel wrapper.pagination/:PagedState+PagedNotifierMixin(family + noArg).filter/:FilterNotifierbase class.presentation/:AsyncBuilder+AsyncValueextensions.logging/:AppLoggerinterface +PrettyAppLoggerimplementation.observers/:ErrorObserver(toast callback injection) +LogObserver.storage/:KeyValueStorageabstraction +HiveStorage+keyValueStorageProvider.theme/:AppThemeExtension+ThemeModeNotifier.utils/:num_ext,string_ext,date_ext,iterable_ext,context_ext.