Migration
Adopting fespalier in a go_router app
You do not have to move everything at once: fespalier can mount its generated routes inside the GoRouter you already have, so you can bring one section of the app over at a time.
- Install
fspand add the package (Installation and setup). - Keep the default
main: autoand put the router inlib/app/app.dart: aGoRouter router()that returns your ownGoRouterwith...AppRoutes.mount(at: ...)among its routes (step 4). The generatedmain()calls it once, sostartup.dart'sready()andattach()(since 0.12.0) still run around it (main()). Setmain: manualonly if you want to keep your ownmain()as it is:fspthen writes nomain()and reads neitherapp.dartnorstartup.dart, so you callreadyandattachyourself.examples/adoptis the worked case, including a 0.11-stylemain()moved intoready()andattach(). - Put the first routes under
lib/app/and runfsp gen(or keepfsp watchrunning). - Mount the generated routes in your router.
atis the URL prefix:
GoRouter(
navigatorKey: rootKey,
routes: [...yourRoutes, ...AppRoutes.mount(at: '/x', navigatorKey: rootKey)],
)Under main: auto, that GoRouter(...) is what router() in lib/app/app.dart returns. examples/adopt shows it, with ready() and attach() in startup.dart in place of a hand-written main().
Pass mount your GoRouter's own navigatorKey: routes that render on the root navigator name it as their parentNavigatorKey, which go_router requires to be an ancestor navigator's. (AppRoutes.router() takes a navigatorKey: too, and either way the key is AppRoutes.rootNavigatorKey.)
Upgrading
What changed between releases, newest first, each with a link to the reference section that describes the behavior today. There is no 0.8.0 release: it was tagged but never published, and the wave it carried ships as 0.8.1. Since 0.23.0, fsp upgrade --app moves an app's refs and prints, for each release in between, the first paragraph of its section here and the lead of each bullet.
0.25.0
No app regenerates a different app.g.dart in 0.25.0, and the one code change is a switch over fespalier_cratestack's SyncReason that lists every value, which gains push: fespalier_stream and Dio's fetch adapter are new and opt-in, three packages ask for web 0.5.0 or newer, fespalier_frb's change feed moves into fespalier_stream, which fespalier_frb re-exports, and fespalier_frb and fespalier_cratestack now depend on fespalier_stream, so an app that overrides either with a path: overrides fespalier_stream too.
fespalier_streamis new and opt-in (since 0.25.0). Server-sent events read through your ownHttpTransport, as aStreamfor adata.dart(transport.serverEvents(url)) or as aLiveFeedthat Riverpod holds and that reconnects on your signals and yourstreamTicker, with no timer of its own, and WebSockets (package:fespalier_stream/web_socket.dart, a library of its own overpackage:web_socket:SocketConnection, signed in with a ticket thatticketGrantorfetches through your transport). An app that does not list it changes nothing. Streaming, WebSockets.- Dio streams on the web (since 0.25.0).
package:fespalier_dio/fetch.darthasFetchHttpClientAdapter, a Dio adapter over the browser'sfetch, andwebStreamingAdapter(), that adapter in a web build and Dio's default elsewhere: setdio.httpClientAdapter = webStreamingAdapter()for an event stream throughDioTransportto arrive as it is written, since Dio's own web adapter delivers a body whole. It honours none of Dio's timeouts. A Dio that keeps its adapter changes nothing. HTTP clients. fespalier_frb's change feed isfespalier_stream's (since 0.25.0).ChangeFeed,ChangeTopic,ChangeBudget,Change,changeLogCapacity,InvalidationTable,InvalidationRule,UnknownChangeandFakeChangeSourcemoved, andpackage:fespalier_frb/fespalier_frb.dart(andtesting.dart) re-export the same declarations, so an app that imports them fromfespalier_frbchanges nothing, and one that imports both libraries meets no ambiguous name.ChangeCodecstaysfespalier_frb's: it is now a facade overfespalier_stream'sEventCodec, with the same members and behaviour, and still reports a skip asfespalier.frb.decode(EventCodecalone reportsfespalier.stream.decode).fespalier_frbnow depends onfespalier_streamat the sameref, which pub fetches with it; an app that overridesfespalier_frbwith apath:(a checkout, a fork) must also overridefespalier_stream, andfespalier_transport, with the same kind ofpath:, or pub resolves them at the tag.ChangeFeed.live(a feed over a live connection, where a reopen that cannot resume moves every watched key once) andEventCodecare new. Something changed:ChangeFeed.SyncReason.push: a server push starts an offline sync (since 0.25.0).fespalier_cratestack'sSyncReasongainspush, so aswitchover it that lists every value (aSyncRunnerof your own, a banner that names the reason) no longer compiles until it handlespush; one with a default case changes nothing.autoSyncfollows the newsyncPushSignal(SyncTriggers.onPush, on by default), which never moves until you override it, so an app that does not changes no behaviour. A push skips the minimum interval, and one that arrives during a sync runs one more after it instead of joining it.crateStackEventsreads a CrateStack subscription (@@subscribe) as a live connection, which is whyfespalier_cratestacknow depends onfespalier_streamat the sameref: an app that overridesfespalier_cratestackwith apath:overridesfespalier_streamtoo. A push from the server, Subscriptions.- An action may return a
Stream(since 0.25.0). Anaction.dartfunction that returnsStream<T>was an error; it is now a streaming action, with aStreamActionState<T>and a hook that givescancel(), so no app that generated before has one, and itsapp.g.dartis unchanged. A telemetry sink of your own that reads an action's outcome can now getcancelled(a stopped stream, not an error) andTelemetryEnd.actionItems, the count of its items;RecordingTelemetrywritesitems=on a streaming action's end only. Streaming actions. web0.5.0 or newer (since 0.25.0).fespalier_dio(new),fespalier_connectivityandfespalier_keypair(were>=0.4.0) ask forweb: ">=0.5.0 <2.0.0". An app that pinswebbelow 0.5.0, or depends on a package that does, no longer resolves: raise it.package:http1.5.0 already asked for 0.5.0, so an app that usesfespalier_httpis there.
0.24.0
0.24.0 changes no code you wrote and no app regenerates a different app.g.dart. An app with a config/app.yaml gets a new generated file, lib/app.config.g.dart, beside output.
fspreadsconfig/app.yaml(since 0.24.0). An app that already has aconfig/app.yamlfor another tool now getslib/app.config.g.dart, andfsp genandfsp checkfail on what they cannot read or type in it, or in aconfig/app.<name>.yamlorconfig/app.ymlbeside it: move the file elsewhere. Without aconfig/app.yamlthe feature is off and nothing inconfig/is read: aconfig/app.ymlor aconfig/app.<name>.yamlthere gets a warning that names it (is ignored), never an error, and every other file ofconfig/is not mentioned. An error inconfig/fails the command afterlib/app.g.dartis written, so it never stops the routes: onlylib/app.config.g.dartis left as it was. With aconfig/app.yaml,fsp devandfsp buildsetFSP_PROFILEandFSP_CONFIG_*in their commands' environment, over a variable of the same name of yours. App configuration.
0.23.0
0.23.0 changes no code you wrote in lib/app/, and no app regenerates app.g.dart because of fsp doctor, fsp schema, fsp add, fsp upgrade --app or adapters: auto.
- Check the upgrade with
fsp doctor(since 0.23.0). After you move the refs, it says whether fespalier and every companion are at the ref of thefspyou run, whetherpubspec.lockresolved them, and whether go_router and Flutter fit (fsp doctor). - Move the refs with
fsp upgrade --app(since 0.23.0). One command moves theref:of fespalier and of every companion to the release of thefspyou run (or to--version vX.Y.Z), runsflutter pub getandfsp gen, and prints this page's notes for every release in between (Moving the app's pins). It never moves the app back unless--versionasks for it, and from a pin before 0.23.0 the first move needs a globalfspof 0.23.0 or later.
0.22.0
0.22.0 changes no code you wrote in lib/app/. app.g.dart is unchanged for an app with no path: dependency, and for one whose route files spell no type declared in a path: dependency where fsp compares types; where they do, it can change (the first bullet).
- A
path:dependency's types are followed like your own.fspnow reads the files of the packagespubspec.yamllists with apath:(underdependencies,dev_dependenciesordependency_overrides, or inpubspec_overrides.yaml, and the overrides of the root of a pub workspace for an app that saysresolution: workspace) and of their ownpath:dependencies, as it has read your own since 0.21.0, so the three typedef bullets of 0.21.0 apply to a type declared there, and an enum declared there is found. An optional parameter of a page or layout typed with an enum of a path dependency, or with atypedefthere of a query type or of an enum (ToneAlias? tone), is now a query parameter, where it was left to its default, so its typed route gains that optional argument. If another file of the route already reads that name as a query parameter of another type, that is now an error where there was none: a page withfinal Tone? tone;(an enum of a path dependency) beside adata()with{String? tone}reports`?tone` is String? in list/data.dart:1 but Tone? here; spell both the same. A page or layout parameter spelled with a path dependency'stypedefor import prefix of what adata.dartyields gets the data by type, where an optional one was left to its default; if this folder'sdata.dartand a section's both yield it, that is now the error a parameter spelled like both gets (name itdata). Atypedefthere of a type filled by type (typedef Kid = Widget;) is filled. Hosted,git:and SDK packages are still compared as spelled. Known limitations says which packages are read.
0.21.0
0.21.0 is a breaking release for an app that uses fespalier_auth, fespalier_dio, fespalier_http, fespalier_sign_keypair, fespalier_download, fespalier_tolgee, fespalier_maps, fespalier_image or fespalier_cratestack: every package that sends HTTP now sends through one fespalier-owned interface, HttpTransport in the new fespalier_transport (HTTP transport), and fespalier_auth no longer knows what a token is. The first section lists every break with its one-line fix, and users who were signed in sign in once more. Beyond that, 0.21.0 changes no code you wrote in lib/app/, but a widget test that drives a failing guard or redirect.dart now fails until it calls tester.takeException() for the failure's report (the seventh bullet), a guard on a pushed page runs again when the page comes back on top and, over a guarded base page, can ask a dirty page's leave() twice (the ninth), and every app regenerates app.g.dart: its redirect:s run through runRedirect and its errorBuilder is AppRoutes.errorPage, so a guard or redirect.dart that fails shows the route's error.dart (the seventh bullet). Beyond that, app.g.dart is unchanged for an app that spells each type the same wherever it is compared (a page and its data.dart, a guard and its page); where a parameter's type is written as a typedef or with an import prefix, it changes more (the three bullets after the first). Four more changes touch what an app sees without touching its code: the telemetry stack moves to OpenObserve v1.1.2 (the first bullet), fespalier_frb rebuilds less, so a test that counted reloads after a burst may count fewer (the fifth), two packages raise a lower bound, of dio and of crypto (the sixth), and go_router 18.1.1, which the range fespalier allows takes, keeps the tabs a hot reload leaves (the eighth).
The HTTP transport and the auth rebuild
Each bullet is one break and its fix. Read HTTP transport and Authentication for the whole picture; You do not add fespalier_transport by hand: the packages that use it bring it at the same tag.
- Stored sessions are dropped: users sign in once more.
AuthSessionis version 2 ({"v": 2, "backend", "credential", "user"}): a session stored by 0.20.0 or earlier ("v": 1, with tokens in it) is not read.restoreAuthreports it once (FlutterError.reportError, contextwhile restoring the stored session,unknown stored session version 1), deletes it and answersSignedOut(). No code change; a widget test that restores a stored v1 session fails on the report until it callstester.takeException(). AuthConfigtakes atransport:. It is required:AuthConfig(backend: ..., transport: HttpClientTransport(http.Client()), apiOrigins: [...])(fespalier_http), orDioTransport(dio)(fespalier_dio) for Dio. It is whatauthTransportwraps and what the backend's own calls go through.AuthConfig.leewayisrenewAhead, and the backend's. PassOidcBackend(renewAhead: Duration(seconds: 30))(the default, which is whatleewaywas); a backend of your own overridesAuthBackend.renewAhead.refreshisrenew.AuthBackend.refresh(session)isrenew(session, {hint}); the notifier'stokens()iscredential(); the telemetry steprefreshisrenew(see the telemetry bullet). A backend of your own also implementsscheme,authenticator(aRequestAuthenticator),readCredential(json),renewAhead, and may overridecheckanddiscard(Writing a backend); anAuthSessionis built withcredential:(aSessionCredential), not tokens.SessionClient,SessionInterceptor,authHttpClient,authBaseClientandpackage:fespalier_auth/dio.dartare gone.ref.watch(authTransport)is aSessionTransportoverAuthConfig.transport. Forpackage:httpcode useTransportClient(ref.watch(authTransport))(fespalier_http); for Dio,dio.httpClientAdapter = TransportAdapter(ref.watch(authTransport))(fespalier_dio). OverrideauthConfigwith aFakeTransportwhere you overrodeauthBaseClientwith aMockClient.package:fespalier_auth/oidc.dartis thefespalier_oidcpackage. Addfespalier_oidc(sameurlandref) and importpackage:fespalier_oidc/fespalier_oidc.dartforOidcBackend,OidcEndpoints,BrowserSignIn,PasswordSignIn,unverifiedJwtClaims,keycloakRolesandProofOfPossession.OidcBackendtakes atransport:(instead ofclient:), the same one asAuthConfig.transport.AuthTokensisTokenCredential(session.tokensissession.credential,AuthSession.bindingisTokenCredential.jkt).FakeProofis inpackage:fespalier_oidc/testing.dart.AuthorizerandHttpCredentialstake aTransportRequest.authorize(method, uri)isauthorize(request, {previous})andretry(attempt, statusCode:, headers:)isretry(attempt, response)with aTransportResponse;HttpCredentials,HttpAuthorization,HttpWritesandFakeHttpCredentialsmoved fromfespalier_httptofespalier_transport. The replay mark istransportReplayKey('fespalier.auth.replay') inTransportRequest.attributes, where it wasisAuthReplay(request)andoptions.extra[authReplayKey];authRefusedKeyandauthWantedKeyare the request's memo.SignOutReason.revokedexists. AswitchoverSignOutReasonwith no default needs the new case. A session ended by the server (a revoked device) isSignedOut(reason: SignOutReason.revoked).DioHttpClientandpackage:fespalier_dio/client.dartare gone. Send throughDioTransport(dio), orTransportClient(DioTransport(dio))where code needs anhttp.Client.fespalier_diono longer depends onfespalier_http.- The problem decoders moved to
package:fespalier/problem.dart.fieldErrorsOf,FieldErrorsDecodersandFieldNameswere exported byfespalier_dioandfespalier_http/problem.dart; import them frompackage:fespalier/problem.dart.withFieldErrors()stays where it was. WriteGuardClientisWriteGuardTransport.RetryClient(WriteGuardClient(inner), when: WriteGuardClient.readsOnly())becomesRetryClient(TransportClient(WriteGuardTransport(HttpClientTransport(inner))), when: TransportClient.readsOnly());readErrorsOnlymoved with it.DpopProofis infespalier_keypair_dpop, andfespalier_sign_keypairis something else. DPoP (DpopProof,DpopFallback,DpopUnavailable,dpopHtu,accessTokenHash,verifyDpopProof) moved topackage:fespalier_keypair_dpop; the keys moved topackage:fespalier_keypair:DpopSignerisKeypairSigner,SignKeypairSignerisHardwareKeypairSigner,SoftwareDpopSignerisSoftwareKeypairSigner,FakeDpopSignerisFakeKeypairSigner, andjwkThumbprintis there too. Replace thefespalier_sign_keypairdependency of a DPoP app withfespalier_keypairandfespalier_keypair_dpop;fespalier_sign_keypairis now token-free device-signed sessions (Device-signed sessions). The new packages need Dart 3.12 and Flutter 3.44, likefespalier_sign_keypairdid. On the webDpopProof.device()now uses the browser's non-extractable key and the session survives a reload; it refused by default until 0.20.0. That web key is unchecked in CI.HttpDownloadBackend(client:)istransport:.HttpDownloadBackend(transport: HttpClientTransport(client), bases: ...); the same forHttpTransfer. A grantor that cannot reach its server throws aTransportFailure(it was anhttp.ClientException) and still ends the downloadFailed(network);package:httpis no longer a dependency offespalier_download.TolgeeCdn(client:)istransport:.TolgeeCdn(base, transport: transport),TolgeeEditor.fromEnvironment(transport:)andwithKey(transport:); the in-context build overrides the newtolgeeEditorTransportprovider (no default). A CDN or editor refusal is aTransportStatusError(it was anhttp.ClientException);FakeTranslations.offlinethrows aTransportUnsent.packHttpClientispackTransport. OverridepackTransport.overrideWithValue(transport)(aProvider<HttpTransport>) where you overrodepackHttpClientwith anhttp.Client.ImageCdnfetches through a transport, and is notconstwith one. PassImageCdn(transport: transport, ...): the images are aTransportImageover the app's chain, signed, traced and counted like the other requests, so aconst ImageCdn(...)with a transport is no longerconst. AnImageCdnwith neithertransport:nor aproviderFactory:shows the error view and reports anImageUrlErrorthat says so (it fetched withNetworkImageuntil 0.20.0).ImageCdn.webHtmlElementStrategyis removed; on the web a transport fetch needs CORS headers on the image host, which the<img>fallback did not;providerFactory: NetworkImage.newis the escape hatch.package:fespalier_cratestack/dio.dartis gone.ref.cancellableis inpackage:fespalier_cratestack/fespalier_cratestack.dartand is read byTransportAdapter, soCrateStackCancelInterceptorgoes;CrateStackPortalInterceptorisPortalTransport(inner);DioFailures.readisTransportFailures.read, composed withDioErrors.toTransport:CrateStackErrors([readShopError, (e) => TransportFailures.read(DioErrors.toTransport(e))]), and the generated Dio'shttpClientAdapterisTransportAdapter(chain)(CrateStack).- Telemetry: the
authstep isrenewand the DPoP flag is a scheme (a rename of two names of contract version 1;fespalier.telemetry.versionstays1).fespalier.auth.operationrefreshisrenew, and the booleanfespalier.auth.dpopisfespalier.auth.scheme, a short constant the backend names (bearer,dpop,device);TelemetryStart.authDpopisauthScheme. A dashboard, alert or query of your own that filters on either changes with it;fsp telemetry's dashboards follow (Telemetry conventions). - New, nothing to migrate.
OtelTransport(inner, {isReady, propagateTo})andSentryTransport(inner, {hub, tracing, captureFailedRequests, tracePropagationTargets})put the requests of a transport chain in your traces and breadcrumbs; dropotel_http,otel_dio,SentryHttpClientorsentry_diofrom requests they cover so nothing is reported twice. A refused TLS certificate is aTransportUntrusted, not offline.
Other changes in 0.21.0
- The telemetry stack moves to OpenObserve v1.1.2, which migrates its data (Dashboards on your computer).
fsp telemetrypins v1.1.2 where it pinned v1.0.4, and OpenObserve migrates the stack's database on its first start; v1.0.4 cannot open it afterwards. The stack in~/.fespalier/telemetryis shared by every app on the computer, so usefsp0.21.0 or later forfsp telemetryin all of them: an older one writes the v1.0.4 file back, and OpenObserve restarts in a loop (Migration file of version '…' is missingin its log, and the importer says it did not answer). Run the newerfsp telemetryagain: OpenObserve starts on the same data, and--resetis not needed. It also makes a Grafana percentage tile say 100 % when every call in the range failed, where it said Not enough data yet. No app code changes. - A typedef of a query type is a query parameter. An optional parameter of a page or layout whose type is a
typedef, declared in the app's own files, of a query type (String?,int?,double?,bool?, aListof them, or an enum) is now a query parameter, where it was left to its default, so its typed route gains that optional argument. The one exception is a type that the route'sdata.dartyields spelled the same (Future<Tags>andfinal Tags tags;withtypedef Tags = List<String>;): that is the data, as it was. Known limitations says which types are followed. - A typedef or an import prefix of a data type gets the data. A page or layout parameter spelled with a
typedefor an import prefix (m.Product) of what adata.dartyields now gets the data by type, where an optional one was left to its default. The exact spelling is tried first, so a parameter that spells the data's type as thedata.dartdoes is bound as before. - A typedef of a type that is filled by type is filled too. A parameter whose type is a
typedefof one of the types the generator fills by type is bound like that type: an optionalKid footerwithtypedef Kid = Widget;in a layout gets the page, as an optionalWidget footeralways did, next to the layout'schild, so the page is built twice. The same goes fortypedef Failure = Object;in an error view (the error) andtypedef Retry = void Function();(the retry). fespalier_frbrebuilds less (A rebuild budget). A topic's key is rebuilt only by an event that concerns it, and a burst goes through in one event-loop turn while some watched key is not concerned or something watcheschanges.latest; before, Riverpod took a burst one event per turn whenever nothing else watchedchanges.latest, so a key could reload once per event of it. A test that counted reloads after a burst may count fewer now (a key watched alone reloads as before); no code changes. A topic's value counts the events that concerned the key, so one rebuild can move it by more than one (a listener ontopic(42)that saw[1]for the burst[42, 7, 42]sees[2]while something watcheschanges.latest): compare rebuilds, not the number. An exception that a matcher throws reaches a direct listener of the topic (container.listen(topic(key), ..., onError: ...)) as aProviderException, where it arrived as the matcher's own exception.ChangeBudget, a budget of one move per key per tick of a provider you fire, is new and opt-in.fespalier_authasks for dio 5.7.0 andfespalier_storagefor crypto 3.0.6 (they asked for dio 5.4.0 and crypto 3.0.0). An app that lets pub choose changes nothing; one that holds a lower version independency_overrides:orpubspec.lockrunsflutter pub upgrade dio crypto. Why: the repository now spells each third-party dependency one way in every pubspec, and these were the two packages that allowed less than their siblings (fespalier_dioandfespalier_cratestackalready asked for dio 5.7.0, and every other package that uses crypto for 3.0.6).- A guard that fails shows the route's
error.dart, on every go_router (When a guard fails). A guard orredirect.dartthat throws, or whoseFuturefails, now reaches go_router as aGuardFailure(itserrorandstackTrace; its message isa guard or redirect.dart failed with <type>, where go_router saidException during route redirect: …), on every version the range allows. The route'serror.dartshows when it takes onlyerror,stackTraceandretry(it builds its ownScaffold, since go_router's error route is a bare page); otherwise the not-found view does. That holds afterpush,pushReplacementandreplacetoo, whereretryreplaces the failed entry with the same location, keeping its page key (Retrying a pushed navigation). Before, a sync throw was the not-found view with no word about it, everywhere, and an async failure was the same on go_router 18.1.1; on 17.2.3 to 18.1.0 an async failure was an uncaughtGoException, and the router stayed on the page it was on. Now, aftergo, a link or a tab, the page the app was on is left (itsonLeaveruns, and telemetry logspage leave); after apushit stays under the error view, covered and not left, andpushReplacementandreplaceover other pages leave the page they replaced. The failure is reported once, in every build mode, withFlutterError.reportError, soFlutterError.onErrorhears it, which is whereSentryFlutter.initlistens. Withtelemetry: truethe navigation ends with the outcomeerrorinstead ofnot_found: the "link leads nowhere" tile no longer counts it, the new "Did a failing check stop a screen from opening?" tile on the Errors dashboard does (a guard that throws before it returns is counted nowhere else), and "How many operations failed?" counts a failure after anawaitonce, on the guard's span. Nothing is uncaught any more, so a reporter that listens only onrunZonedGuardedorPlatformDispatcher.onErrorhears nothing of it. Sentry events: an app withSentryFlutter.initalone gets one event for a failure, the report's (before, the uncaughtGoExceptionof an async failure on 17.2.3 to 18.1.0, and nothing otherwise). An app withFespalierSentry(telemetry: true) as well still gets one event for a failure after anawait: the sink's (tagged with the route and the file) comes first, and the report carries the same error object, so Sentry's deduplication (on by default,enableDeduplication) drops it; with deduplication off it is two (before, the sink's alone on 18.1.1, and the sink's and the uncaught one on 17.2.3 to 18.1.0). A guard that throws before it returns gives the report's event alone. The deduplicated count is run againstSentry.initinfespalier_sentry'stest/dedup_test.dart; the other counts are read from the code paths, not run against the Sentry SDK. A widget test that drives a failing guard fails until it callstester.takeException()for the report; it asserts on the error view, or, aftergo, oncurrentConfiguration.errorbeing aGuardFailure(after apushthat error isnull: go_router keeps the failure in the pushed entry). AGoRouterof your own passeserrorBuilder: AppRoutes.errorPage. Regenerate; no other code changes. - A hot reload keeps the tabs you left, on go_router 18.1.1 (Which edits swap). From go_router 18.1.1 a hot reload that swaps in the regenerated routes keeps the stack of every tab that was loaded, not only of the tab on screen, and the pages of a tab that is not on screen run no
onLeave. Before, only the tab on screen kept its stack, and the others started over at their first page. A loaded tab the edit removed keeps its pages until its layout goes, and theironLeaveruns then. Debug builds only. No code changes. - A guard on a pushed page holds after a refresh (A guard on a pushed page). go_router shows a pushed page again without running its redirect after a refresh, a restore, a pop back to it, a tab coming back or a hot-reload swap. A redirect that the page runs now runs again each time, and a page whose guard has not passed is not built: it is an empty box until its guard has run again after the frame. A page that passed before and is still around stays visible until its guard answers, which is one frame for a synchronous guard and the whole wait for an asynchronous one; a page that left, or that the app's own
replacemoved to another query or parameter (a search box over a pushed page), and that the browser's back or forward brings back at its earlier address, is held like one that never passed and built again from scratch (since 0.21.1). A guard that did not run again before, such as aProviderContainerguard or aredirect.dart, now runs on each of those, so a test that counts its runs sees more of them. A live guard of a pushed page follows it while it is on top: when its answer changes the page moves, or the error view shows, withleave()asked as for a base page's redirect and the dialogs and sheets over the page on its own navigator closed. Over a base page that is under a redirect the router is refreshed first, so a sign-out that a guard notices (a fespalierguard.dart,requireSignedIn) still replaces the whole stack, as before, also while a re-run waits (a sign-in of your own, arefreshListenablewith a top-levelredirect, can move the pushed page alone in that window: limit 9); the price is that a change which only the pushed page's guard notices (a role revoked while the session holds) can ask a dirty page'sleave()twice, once for the refresh and once for the re-run that moves it (once if you refuse the first question, and once for a guard that passes again, which moves nothing). Over a base page under no redirect the re-run is alone and asks once. The guards of a base page under a pushed page are not watched while it is covered, as before; a base page you uncover with the app bar's back button,Navigator.popor the system back keeps the answer its guard gave until the next navigation, andGoRouter.popandcontext.popof a page with noleave.dartrun it again. A guarded pushed page you uncover runs its guard again however you uncover it. An asynchronous guard leaves the old page on screen while it waits, and what the person does meanwhile is asked as usual; when the guard answers and the page is still there, its move is asked once, at the landing. TheFutureof apushwhose page a guard replaced completes with what the page in its place pops with (withnullwhen that has another type). Every app regeneratesapp.g.dart, which now callsrefusedViewon the pages under a redirect. Each refresh of such a page is one more navigation in telemetry and DevTools.
0.20.0
0.20.0 breaks nothing: an app with a data.dart, an action.dart, a deferred page or a type written with an import prefix in a route file regenerates app.g.dart (below), with no code change; any other app's is unchanged.
The generated names follow the folders (Which edits swap). The providers, the deferred libraries, the optimistic layers and the actions are named after their folder, not after where the route sorts:
_dataProductsId,_libCheckout,_optimisticTeamsTeamId,_actionNotesId_edit. So are the DevTools sites,dProductsIdandaNotesId_edit; the guard andredirect.dartsites (g5@6,r32) do not change, and neither does a route's class. A prefixed type's import alias (_e…_m) is made from the library the prefix imports, not from the file that spells the type. The names are private and the DevTools extension reads any site, so no code changes; runfsp genand commit the result, sincefsp checkfails on a stale file until you do (Failing CI on a stale file). A hot reload now swaps a route added or removed anywhere, sofsp devreloads it instead of restarting. Still refused: a changeddata()or action, a moved folder that has one (and the second of two folders whose names share a stem, added or removed, when one has adata.dartor anaction.dart; two deferred pages alone swap), an import prefix pointed at another library (it was accepted before 0.20.0), and a router setting. A script that readsfsp routes --graph jsonfinds the new site ids.A deferred route added or removed hot reloads too (Which edits swap).
AppRoutes.deferredis a getter now, where it was astatic finallist, and a deferred page's import is named after its folder (deferred as _pageCheckout, where it was_i7). So an app with a deferred route regeneratesapp.g.dart, with no code change: code that readsAppRoutes.deferredor callsloadDeferred()works as before. Now a swap: a deferred route added, removed or moved beside another; a page added withdeferred: true;deferredturned on or off.fsp sizereads the new names, so runflutter build webagain first; an older build reports that it was built from anotherlib/app.g.dart(Web chunk sizes). Before 0.20.0, adding a route before a deferred one was refused (it changedAppRoutes.deferred), but a file that is not a route (aloading.dart, anerror.dartor aguard.dartin the root, or in a folder that sorts before a deferred route) renumbered the position-based import (_i2became_i3) while every hash the reload compares stayed the same. That swap went through, and the deferred page then threw "Deferred library ... was not loaded" until a restart: a bug of 0.19.0 that 0.20.0 fixes, since the import keeps its folder's name.
0.19.0
0.19.0 breaks nothing for an app whose telemetry sinks extend FespalierTelemetry (every sink fespalier ships does): every app regenerates app.g.dart (below), and an app that does not name its screen or set otel: true changes nothing else. A telemetry sink that implements FespalierTelemetry instead of extending it adds one method (below). With fespalier_sentry, the scope is named at the commit, which changes a few events from a page's first frame (below).
- Optional: native crash records can name the screen (The screen a native crash happened on). Bump
otel_zoneto64c4661ac6cd09718141864d528892916677cc38(v0.8.1, on the rc.4 line; v0.7.0 was the first withonScreen) in your pubspec, passonScreen: observability.setCrashScreentoFespalierOtel, and runfsp telemetry --stop, thenfsp telemetry, so the collector reads the new attribute and the Errors table has its panel.app.g.dartis not changed by this item (every app regenerates it for 0.19.0, below). An app that does none of this keeps its crash records as they are. - A telemetry sink of your own (Several sinks).
FespalierTelemetryhas a new member,commit(navigation, commit), with an empty default: the router tells it what each navigation commits, before the page is built. A sink that extendsFespalierTelemetrychanges nothing; one that implements it stops compiling until it adds the method (or extends instead), and one with a member already namedcommitof another signature renames it.RecordingTelemetrywrites commit lines only withrecordCommits: true. - Optional: OpenTelemetry without wiring (OpenTelemetry from the generated main()). Add
otel: trueunderfespalier:, next totelemetry: true, and delete the hand wiring: inlib/main.dart,Future<void> main() => AppMain.run();, and inlib/app/startup.darttheOtelZone, itsstart(), theFespalierOtelsink, the two observers and anyzone()that runsrunGuarded.fsp genwarns while theOtelZoneor theFespalierTelemetry.installis still in startup.dart. A sink thatstartup()still installs withFespalierTelemetry.installmust useFespalierTelemetry.add, once (guarded byFespalierTelemetry.contains, because Retry runsstartup()again), or it replaces the OpenTelemetry sink.fespalier_otelresolves withotel_zonev0.8.0 or later, but only v0.8.1, the pin above, runs the generated zone on the web: on v0.8.0 the app works on mobile and its web build is blank, so use the pin. An app withmain: manualkeeps its ownmain(), andotel: trueis refused there. For this item onlylib/app.main.g.dartchanges;app.g.dartchanges for 0.19.0 in every app (below). - Sentry names a page at its commit (What Sentry gets from fespalier). The scope's transaction name,
fespalier.routeand, next tofespalier_otel,otel.trace_id/otel.span_idare set when a navigation commits, before the new page'sinitStateand firstbuild. Most events read the same: Sentry's own capture of an exception reads the scope after the frame, and a guard,data()or action error takes itsfespalier.routefrom its call site. What changes is what reads the scope during that frame: an event captured on a copy of it (captureException(..., withScope: ...)in your code, and fespalier's own events, whose transaction name now is the page being opened), and code that readsscope.transactionor the tags, such as a page'sinitState. From the commit until the first frame, an event from the page being left names the destination too (captureExceptionright afterrouter.go(...)in the same handler). A search on the transaction name finds these events under the page being opened. Nothing to change in code; a test that readsentry.tagsortransactionNamebefore a pump now sees the new page. - Every app regenerates
lib/app.g.dart: runfsp genand commit the result (a committed file that is not regenerated is stale: see Failing CI on a stale file). There is no code change.AppRoutes.router()builds aGoRouter.routingConfigover a routing config, the same router in release; the layouts' navigators and shells take their keys fromAppRoutes, so a hot reload keeps the pages in them; andAppManifest.byTypeandAppManifest.byPathare getters over the same maps. A hot reload in a debug build now swaps in the regenerated route table, with limits: a route added, removed or moved swaps when no route after it in the order offsp routeshas adata()function, an action or a deferred page (the generated names of those are numbered by position, and a changed name is refused), and a provider, a deferred library or a router setting that changed is refused too. A refused swap prints one message and keeps the old routes until a hot restart; stable names made from the folders will lift the limit on position in a later release.fsp devnow hot reloads after a regeneration that swaps, and hot restarts after one that does not (the CLI reference).
0.18.0
0.18.0 breaks nothing: the generated app.g.dart is unchanged unless an app lists the new fespalier_launch adapter, an app that does not use telemetry changes nothing, and fespalier_frb's ChangeCodec is new and opt-in.
- Four attributes on the data and action spans:
fespalier.data.attempt(which try a load is),fespalier.data.source(cacheorfetchfor adataCache),fespalier.action.rolled_back(an optimistic patch was taken back) andfespalier.action.invalid(the run ended withFieldErrors). They come withtelemetry: true, like the others; the names are in Telemetry conventions. ARecordingTelemetrylog gains theattempt=,source=,rolled_back=andinvalidtokens, so a test that compares whole lines needs them. - The Screens and Actions tiles for them need the stack restarted once: run
fsp telemetry --stop, thenfsp telemetry, which rewrites the files and updates the dashboards, so the collector reads its four new dimensions (Dashboards on your computer). An app before 0.18.0 sends none of the four, so those tiles read 0. fespalier_launchis new and opt-in (Shortcuts and widgets). It opens a typed route from a home-screen shortcut or a widget tap, cold or while the app runs. An app that does not list it regenerates an identicalapp.g.dart, and nothing else changes. An app that lists it gets one more adapter inAppAdapters, callsFespalierLaunch.configure(sources:, route:)inmain()beforeAppMain.run(), and configures it in every widget test that bootsAppMain.root()(FespalierLaunch.debugReset()insetUpandtearDown, as for fespalier_push). The plugins (quick_actions, home_widget) stay the app's: the recipes are in thefespalier-routingskill.fespalier_frbdecodes raw core messages:ChangeCodec(Decoding, since 0.25.0EventCodecunder it). Nothing changes for a feed over typed events, andChangeFeedis unchanged. If youropendecodes raw messages itself (rawChanges().map(decode)), a message that failed to decode was anAsyncErrorinlatest; with.transform(ChangeCodec(decode))it is skipped and counted,latestkeeps its last change, and a decoder can throwUnknownChangefor a variant a newer core added.
0.17.0
0.17.0 is about code generators, where fespalier and freezed or build_runner trip over each other, and it changes nothing for an app that does not use them (the generated app.g.dart is unchanged, and nothing breaks):
ExtraCodec.offinds anextrawithis, so a freezed class restores (Restoringextraon the web). Nothing is removed or renamed; this is for an app whoseextrais a freezed class written asconst factory Note(...) = _Note. The object is a private_Note, soExtraCodec({Note: ...}), which looks a type up by its exact runtime type, never matched it: the extra was saved asnulland a web reload gave the pagenull, without a word. Changelib/app/extra_codec.dart, and keep the old constructor for anything else:dart// Before (0.16.0 and earlier): never matches a freezed `Note` final extraCodec = ExtraCodec({ Note: (toJson: (Note n) => n.toJson(), fromJson: Note.fromJson), }); // After (0.17.0): the first ExtraType the object `is` takes it final extraCodec = ExtraCodec.of([ ExtraType<Note>(toJson: (n) => n.toJson(), fromJson: Note.fromJson), ]);A debug build now also prints one line, once per type, for an
extrathat no entry takes (a release build prints nothing), so the next one you forget is not silent.fspwarns whenbuild_runnerwould deletelib/app.g.dart(The generator). The builders that write.g.dartfiles (json_serializable, riverpod_generator) claim anX.g.dartfor everyX.dart, so an app withbuild_runnerand alib/app.dartloses fespalier's default output on the nextdart run build_runner build, with nothing logged.fsp gen,check,watchanddevnow warn onlib/app.dart, and still succeed; the generated code is unchanged. Setoutput: lib/routes.g.dart(thenfsp gen, and change theapp.g.dartimports), renamelib/app.dart, or keeplib/app.dartout ofsource_gen:combining_builderinbuild.yaml, whichfspreads and then stays quiet.The examples no longer pass
-dtobuild_runner. It has ignored the flag since 2.7.0. The commentedtasks:thatfsp initandfsp createwrite, and the docs, leave it out; atasks:you wrote with-dstill works.
0.16.0
0.16.0 raises the floor to Flutter 3.35 and Dart 3.9, changes one member of FespalierAdapter, fixes the places where a write could be sent twice, a provider started a hidden timer, a notification tap was lost and a step name did not compile, makes three outcomes of the network packages right (one port, DownloadBackend.resume, gains a named parameter: see the resume item), so an app sees different outcomes there, and adds opt-in tooling:
Flutter 3.35 and Dart 3.9 are the floor (Requirements). Breaking, for an app on Flutter 3.32 to 3.34:
fespalierand every companion that was in the 3.32 floor job declaresdk: ^3.9.0andflutter: ">=3.35.0"(fespalier_sign_keypair, Flutter 3.44, andfespalier_download_background, Flutter 3.47, keep their own). On an older Flutterflutter pub getfails naming the SDK, andfsp createsaysFlutter {v} is too old: fespalier needs Flutter 3.35 or newer. Upgrade Flutter (3.35.7 is the last 3.35 patch, Dart 3.9.2), set the sameflutter-versionin CI, and raise your ownenvironment:; an app that has to stay on 3.32 to 3.34 stays on 0.15.x. The floor does not changelib/app.g.dart.yamlenvironment: sdk: ^3.9.0 flutter: ">=3.35.0"go_router's lower bound is 17.2.3 (it was 17.0.0). An app that lets pub choose changes nothing: 17.2.3 is the newest go_router that Flutter 3.35 to 3.37 resolve. An app that holds go_router 17.0.0 to 17.2.2 in
dependency_overrides:orpubspec.lockrunsflutter pub upgrade go_router. Why: Flutter 3.32 resolved go_router 17.0.0 and no newer one (17.0.1 asks for Flutter 3.35); that release ran aBlock.thenof anonEnterinside the parse, before the commit, and asserted on a platform payload with no leading slash. On 17.2.3 neither needs a workaround.fsp create --features allnow includesotelon every supported Flutter, and an app it makes declaressdk: ^3.9.0andflutter: ">=3.35.0".
The generated app.g.dart changes in an app with adapters: (regenerate: AppAdapters.handlesEnter, and the router passes onEnter only when an adapter has one) and is unchanged in an app without. Adapters:
An adapter's
onEnteris a getter (Adapters).FespalierAdapter.onEnterwas a method in 0.11.0 to 0.15.0 and is now a nullable getter,FespalierEnter? get onEnter => null;, whereFespalierEnteris the function type of the old method.AppRoutes.routerpasses go_router anonEnteronly when some adapter's getter is not null (onEnter: AppAdapters.handlesEnter ? onEnter : null; before, anyadapters:entry passed one), and only those adapters are asked. An adapter that overrides the method no longer compiles, which is the point: the analyzer saysClass 'MyAdapter' can't define method 'onEnter' and have field 'FespalierAdapter.onEnter' with the same name.instead of the adapter being skipped without a word. No first-party adapter had anonEnter. Regenerate (fsp gen):lib/app.g.dartchanges in an app withadapters:, and not in one without.dart// Before (0.15.0) @override FutureOr<OnEnterResult>? onEnter(InboundNavigation navigation) { if (navigation.next.uri.scheme != 'vaam') return null; return Block.then(() => navigation.router.go('/orders/42')); } // After (0.16.0) @override FespalierEnter? get onEnter => (navigation) { if (navigation.next.uri.scheme != 'vaam') return null; return Block.then(() => navigation.router.go('/orders/42')); };go()thenpush()keeps both again, in an app whose adapters have noonEnter. AnyonEntermakes go_router parse every navigation asynchronously, so before 0.16.0router.go('/a'); router.push('/b');ended as/home > /bandcurrentConfigurationlagged a frame in every app withadapters:; now it is/a > /b, committed in the call. An app with an adapter that has anonEnterstill has the async parse.A router of your own passes
onEnter: AppAdapters.handlesEnter ? AppRoutes.onEnter : nullinstead ofonEnter: AppRoutes.onEnter.
The other fixes make a write that could be sent twice be sent once, so an app sees different outcomes in these places:
- An upload after a
5xx,408or499isFailed(other), an unknown outcome (Uploads). On 0.15.0 every HTTP error but 401 and 403 endedFailed(rejected), andUploads.retrysent an upload without anIdempotency-Keyagain after a500or504, which could run the write twice. Now only a client error that proves nothing was applied isrejected; the others areother,outcomeUnknown(id)is true for an upload without a key, andretryandstartrefuse it until youremoveit on purpose. A download is unchanged. - An upload whose registry entry cannot be saved is
Failed(storage)and is not sent.UploadStore.putmay throw (UploadStoreExceptionfromFileUploadStore), andFileUploadStoreno longer reads an unreadable file as empty:loadandputthrow and leave the file alone, and an upload without a key is not started until the registry can be read (the nextopen()) orclearAccount()empties it. HandleFailed(storage)withretry. - A
5xxwithout a CrateStack envelope keeps an intent's key (CrateStack with fespalier). A bare500isCrateStackOffline, notCrateStackUnavailable; only a5xxin the envelope moves to the next key. A read behindservefalls back to the saved copy on it. On 0.15.0 the next key could run a committed write twice. IntentQueue.drainreads each intent again before it sends it, so an intent that was accepted, discarded or wiped while the drain was busy is not sent from a stale list.- A request with a body is sent again after a 401 through
DioHttpClient(HTTP clients). Anhttp.Request(get,post,put,patch,delete) goes to Dio as bytes, soSessionInterceptorrefreshes and sends it again whole (WriteGuardlets that one second send of a write through, as it always did after a 401). Before 0.16.0 it failed withClientException: fespalier_dio: unknownand the refresh never helped: remove a retry you wrote around that. A streamed or multipart request is not sent again: the caller gets the 401 as the response, after the refresh. AClientExceptionfromDioHttpClientthat has no Dio message now carries its cause (fespalier_dio: unknown: <cause>): code that compared the whole message changes. - Offline while a token refreshes is
Failed(network), no longerFailed(unauthorized)(Credentials). A download whose credentials could not be reached endsnetworkwhen its grantor, or theHttpCredentialsofHttpDownloadBackend, throws anhttp.ClientException(fespalier_auth'sAuthUnavailableis one).unauthorizedstays for a refusal and for any other throw. If your app treatsunauthorizedas "sign the person in again", it no longer does that for being offline: show "offline, try again" fornetworkand callDownloads.retry. Aresumewhose grantor cannot be reached leaves the downloadPausedand returns false. - A paused download resumes from its freshly granted address, and each resume gets its own renewal (Credentials). Before 0.16.0
resumeasked the grantor and threw the newurlaway, so an expired signed address earned a 403 first, and only one renewal existed for the whole life of the download: a second long pause endedFailed(unauthorized). Nowresumegoes to the grant'surl(the bytes kept stay), and a start, a retry and a resume each have one renewal. The grant aresumeasks for is used now, not discarded. ADownloadBackendof your own (a fake in a test, a custom transfer) must accept the newurl:named parameter ofresume(id, {authorization, url}):flutter analyzenames the override that is missing it.FakeDownloadBackend.resumeUrlsrecords it. - An app with a
links:section getsAppRoutes.urlOfwhen it regenerates, the absolute URL of a route on the first domain: a doc comment and one method aftermatchUrl. An app withoutlinks:regenerates byte for byte, and a first domain that is not usable is a warning offsp gen, as before. fespalier: test: a11y:is opt-in (Route smoke tests). Withtrue(or a list oftap_targets,labels,contrast) every testfsp testwrites also checks Flutter's accessibility guidelines once its page is on screen; without it the file is the one it always was.- The DevTools extension has an Adapters tab (DevTools extension), and the protocol is still 1:
hellolistsadaptersin its features. A tool of your own that comparesfeaturesexactly should askcontains. - A plugin that throws is not asked again (Biometric unlock, Push, Offline-first, Change feeds).
biometricAvailable,pushPermission,pendingIntentsandChangeFeed.latestdid not setretry:, so Riverpod 3's default re-asked a throwing plugin or store up to ten times over about 50 seconds, with aTimer. They now keep the error and ask once;ref.invalidate(provider)asks again. - A notification tap while the app starts is opened (Push). It was lost (
launch()runs beforerunApp,attachafter the first frame, and nobody listened totapsin between).launch()now keeps the latest tap andattachopens it once; the cold-start notification seen again ontapsis still opened once. APushSource.tapson a platform channel must return one stream every time, becausetapsis listened to twice while the app starts. fsp checkrefuses a step folder that is no Dart enum constant (Multi-page forms):final,new,default,in,values,index, a name that starts with a digit (2fa), or two folders that become one constant. They used to pass and write a step enum that did not compile; rename the folder and its key insteps.
0.15.0
The
package:httphalf offespalier_dioisfespalier_http(HTTP clients). Breaking, for an app that importspackage:fespalier_dio/http.dartorpackage:fespalier_dio/problem.dart: both libraries are removed (not deprecated), as the forms were in 0.11.0. The move is an import rename and apubspec.yamlline; no member changed its name or behaviour. An app that only uses Dio changes nothing:package:fespalier_dio/fespalier_dio.dartstill exportsproblem.dart.dart// Before (0.14.0) import 'package:fespalier_dio/http.dart'; import 'package:fespalier_dio/problem.dart'; // After (0.15.0) import 'package:fespalier_http/fespalier_http.dart'; // also exports problem.dart import 'package:fespalier_http/problem.dart';yamldependencies: fespalier_http: # the same url and ref as fespalier git: url: https://github.com/fespalier/fespalier path: packages/fespalier_http ref: <the 0.15.0 tag, as for fespalier>fespalier_diodepends onfespalier_httpfor the write rule and the problem decoders, so a Dio app resolves it without listing it; list it when you import it. Rundart pub get, then search forfespalier_dio/http.dartandfespalier_dio/problem.dart.HttpWrites.isWriteis the one write rule.WriteGuard(Dio) andWriteGuardClient(package:http) both call it, so they agree on what a write is (any method butGET,HEAD,OPTIONSandTRACE, unless it carries anIdempotency-Key). Behaviour is unchanged.DioHttpClient(package:fespalier_dio/client.dart) is your Dio as anhttp.Client, for a package that takes one (packHttpClient,TolgeeCdn) and forref.abortable(...).HttpCredentials(fespalier_http) is the session for a transfer that is not anhttp.Client.fespalier_auth'sAuthorizerimplements it, andAuthAttemptimplementsHttpAuthorization; nothing that usesAuthorizerchanges.Fakes for tests:
FakeHttpClientandFakeHttpCredentialsinpackage:fespalier_http/testing.dart.fespalier_authdepends onfespalier_http(forHttpCredentials), so an app that listsfespalier_authand overrides its dependencies by hand addsfespalier_httpto the same overrides.
0.14.0
fespalier_pushhands the app aPushToken, not aString(Tokens and permission). Breaking, for an app that reads a push token. TheStringtied the package to FCM and APNs. APushTokenhas akind(an open string;PushTokenKind.fcm,apns,hms,unifiedpush,onesignal,mipush,oppo,vivo,honor,jpushname the usual ones), a non-nullvalueandproperties, an unmodifiableMap<String, String>for what a backend needs beyond the token (OneSignal's subscription id, a UnifiedPush instance). It has value equality, and itstoStringprints the kind only, never the value or the properties.PushSource.tokensis aStream<PushToken>,pushTokenaStreamProvider<PushToken>,onTokentakes aPushTokenandFakePushSource(token:)andemitTokentake one. A revocation (the vendor's unregister or token-invalidated callback) is a separate event, not a null token:PushSource.revocations(aStream<PushTokenRevoked>, empty by default, so a source that never revokes changes nothing), thepushTokenRevokedprovider,onTokenRevokedinFespalierPush.configureandFakePushSource.revokeToken(kind). Telemetry is unchanged: no token event is reported, and the value and the properties never are.dart// Before (0.13.0) FespalierPush.configure(source: MyPushSource(), route: pushRoute, onToken: (String token) => api.register(token)); // Stream<String> get tokens => messaging.onTokenRefresh; // After (0.14.0) FespalierPush.configure( source: MyPushSource(), route: pushRoute, onToken: (PushToken token) => api.register(token.kind, token.value, token.properties), onTokenRevoked: (PushTokenRevoked revoked) => api.unregister(revoked.kind, revoked.properties), ); // Stream<PushToken> get tokens => messaging.onTokenRefresh.map( // (value) => PushToken(kind: PushTokenKind.fcm, value: value));A test:
FakePushSource(token: PushToken(kind: PushTokenKind.fcm, value: 't')),emitToken(PushToken(...)). EveryPushSourcechanges itstokenstype; an app with no token callback changes nothing else.
0.12.0
ready()andattach()in startup.dart (main()). Opt-in, and an app that exports neither regenerates to the samelib/app.main.g.dartand the sameStartupGate.ready(ProviderContainer)runs on the app's own container afterstartup()and before the router;attach(GoRouter, ProviderContainer)runs after the first frame that shows the router, after the adapters'. They are what amain: manualapp that builds its ownProviderContainerwrites in itsmain(); moving to the generatedmain()is moving that code into them. Runfsp genafter adding either.readyandattachare reserved names in startup.dart now: a helper of that name with another shape is anfsperror (readytakes oneProviderContainer, optional or nullable allowed, and returnsvoid,Future<void>orFutureOr<void>;attachtakesGoRouterandProviderContainer, returnsvoidand is notasync): rename it or make it private.
0.11.0
Forms moved to the
fespalier_formspackage (Forms).form()anduseFormneed thefespalier_formspackage. Addfespalier_formsunderdependencies:, at the same giturlandrefasfespalier, and runfsp gen:app.g.dartnow importspackage:fespalier_forms/fespalier_forms.dart(only in an app with aform()), andfspreports an error at eachform()until the dependency is there. Code that namesActionForm,ActionField,ActionTextField,ActionFormFields,FieldCodec,ActionFormMessages,ActionFormValidationoruseActionFormimportspackage:fespalier_forms/fespalier_forms.dart:package:fespalierno longer exports them. The names and the behaviour are the same, and so is a page that only calls the generateduseForm.FieldErrors,validate()andoptimistic()stay infespalier(validate()andFieldErrors): an app with no form changes nothing. In a repository checkout,examples/featuresandexamples/authshow the dependency.Drafts (Drafts). A form can keep what the user typed per route and restore it on return:
NicknameRoute.useForm(ref, data: p, draft: const FormDraft()). It is opt-in, so no form changes behaviour, but every app with aform()regeneratesapp.g.dart:useFormtakes aFormDraft? draftand passes the action's file and name, its keys and the form's field types touseActionForm, and abool,DateTimeor enum field gets aDraftCodec. A key of an action with a form can no longer be calleddraft. Drafts go toformDraftStorage, which is yourdataCacheStorageunless you override it; callclearFormDraftswhen somebody signs out.Asking before unsaved changes go (Leaving with unsaved changes). A
leave.dartthat isleaveIfClean(context, ref, page)asks in a bottom sheet ("Keep editing", "Discard", and "Keep as draft" for a form with adraft:), and auseFormunder a page with aleave.dartis the page'sLeaveSourceby itself. Nothing changes for an app that has noleave.dart(andapp.g.dartis the same). Flutter'sMaterialLocalizationsare needed by the sheet: on material_ui'sMaterialApp, overrideleavePrompt.Multi-page forms (Multi-page forms). A section's
action.dartwith aconst stepsmap is a form over several pages that share one form and one draft. It is new and opt-in: no app changes unless it adds one. Only aconstmap literal calledsteps(or<action>Steps) in anaction.dartwith aform()is read as one; astepsof another shape (a number, a list) is left alone. A page-less section may now have aleave.dartwhen it is a flow,leaveExitgets awithin:there, and aLeaveScope.onBackhandler now blocks the pop on the first page of a navigator too (a step of a flow is the only page of its shell's navigator).A guard above a tab layout runs once, on the tab shell (
StatefulShellRoute.redirect), instead of being copied into each tab's route. What it decides is the same (it still runs for every location under the tabs, before the tab's own guards), but its telemetry and DevTools site is now one, named after the tab folder (g<guard>@<tab folder>: thefespalier.routeof its spans is the folder's pattern,/for a group, not each tab's), so a dashboard that groups guard outcomes byfespalier.routesees the tabs merged. Runfsp gen; an app with no guard above its tabs changes nothing.observe.dart'sonEntercan take aRouteScope(The page's scope).RouteHooks.onEnterisvoid Function(Ref ref, RouteScope scope)?now: every app with anobserve.dartregenerates (fsp gen), because the generated closures readonEnter: (ref, scope) => .... A hook you wrote keeps compiling unchanged; one that builds aRouteHooksby hand takes two parameters.onEnter(Ref ref, {required RouteScope scope})may callscope.hold(provider)(kept loaded until the page is gone, a parked tab included) andscope.onLeave(callback).AppRoutes.attachtakes the app'sProviderContainerin an app with anobserve.darttoo:lib/app.main.g.dartnow gives theStartupGateattach: AppRoutes.attach(regenerate it withfsp gen), and the hooks run in the app's root container, where a provider overridden only in a nestedProviderScopereads its default. A scope ends, and itsonLeavecallbacks run, when its container is disposed. Page ids changed forreplace: the lifecycle (observe.dart, telemetry, Sentry breadcrumbs) told a page made byreplaceapart by a random key, and now by the key of the page it replaces.replace('/c/1?q=2')on the tree page/c/1is no longer a leave and an enter;replace('/c/2')still is, and so arepushandpushReplacement.
0.9.x
- Page names (Transitions). Every
pageBuilder:the generated file writes is wrapped innamedPage('/products/:id', () => ...), so aNavigatorObserversees the pattern instead ofnull(aremountpage used to be:id). Regenerate withfsp gen: every app'sapp.g.dartchanges. Telemetry (Spans around data() and actions). Withtelemetry: truethe data provider callstraceDataCall(ref, 'd4', id, () => data(ref, id: id), telemetry: ...)(one closure per provider build, noFutureand no microtask); regenerate withfsp gen. An app withouttelemetry: truekeepstraceData(...), and its file does not change.- A
dataspan starts beforedata()runs, so its duration includes the synchronous part. - A
data()that throws before it returns has adataspan (before 0.9.0 it had none). FespalierOtelmakes data and action spans current, so the HTTP spans ofotel_httpandotel_dioare their children. A sink with a member namedwithinof another signature must rename it.
- A
0.8.1
main()(main(): app.dart, startup.dart and splash.dart). Before 0.8.1fsp initprinted amain()that built theProviderScopeand theMaterialApp.routeritself. That still works, and it is whatmain: manualkeeps. Nothing changes until you opt in; the table in app-startup.md shows where each line of a hand-writtenmain()goes. Telemetry (Turning it on): withouttelemetry: truenothing is generated for it.
0.7.0
- Deferred routes (Deferred routes): a route that is not deferred generates exactly the code it did before 0.7.0.
0.6.0
remount(Remounting a page).neveris what fespalier generated before 0.6.0, and the generated code keys a page by the URL where it used to use go_router'sstate.pageKey.disposeRouter(pumpRouter and currentLocation): a test that disposes the router itself with anaddTearDownregistered before the call, as tests written for 0.4.x do, passesdisposeRouter: false.replace(The URL as state) now shows its location in the address bar and replaces the history entry; on 0.5.0 it was go_router's in every case (the address bar followed it only when no page was below it), so usegofor URL state there.
0.5.0
- Guards and redirects (Guards, redirect.dart).
- A guard may take
Ref reffirst and runs again when what itref.watches changes. Before 0.5.0 a guard read once, when you navigated;ProviderContainer cfirst is that older form, still read once. - A redirect may take
Ref reffirst, orProviderContainer c. - Menus read the older form once, when the menu asks (Menus and breadcrumbs).
- See also the
guard.dartrow of File kinds.
- A guard may take
- Layout restoration (State restoration): a layout's page used the route object's hash code as its key, so a router built again by a hot reload or a test replaced the layout and lost its state. Since 0.5.0 it uses a
ValueKeymade of its restoration id. Prefetch (Typed helpers on the route): a prefetch used to lapse after 30 seconds without akeepFor, and now lasts until closed;prefetchKeepAliveis gone.ref,keepFor,preload,of,maybeOfandcopyWithcan no longer be segment or query names. pumpRouter(pumpRouter and currentLocation) disposes the router when the test ends, and the generatedAppRoutesmakes a freshnavigatorKeyon eachrouter()ormount()call without one.