Skip to content

Upgrading to 1.0

1.0 is the first release under the versioning policy. Coming from 0.2.x, it bundles the error model, retries, auth, type policies, the persistence migration and the offline mutation queue — and settles the public surface, which renamed or removed a handful of APIs outright (no deprecation shims this once).

  1. Upgrade every package together — they share a major version now:

    dependencies:
    sling_gql: ^1.0.0
    sling_gql_sqflite: ^1.0.0 # if you persist the cache
    sling_gql_link: ^1.0.0 # if you use gql_link
    sling_gql_hooks: ^1.0.0 # if you use flutter_hooks
    dev_dependencies:
    sling_gql_gen: ^1.0.0
    sling_gql_test: ^1.0.0
  2. Regenerate with the 1.0 generator. Code generated by 0.1.x still compiles, but the new file carries slingSchema.fields (what lets the SQLite store migrate instead of wiping on the next schema change), and its client.mutate(...) / client.subscribe(...) forward the new options (errorPolicy, timeout, retry, offline, onQueued, debugLabel). If your build script passes --part-of-import, rename it --runtime-import.

  3. Fix the compile errors with the tables below, then read the behaviour changes — two of them (default retries, dispose) change what a working app does without a compile error.

0.2.x 1.0
SubscriptionState.retry() SubscriptionState.reconnect() (retry means a RetryPolicy everywhere else).
SlingScope(mutationRoot: Mutation.root) SlingScope(schema: slingSchema), or nothing: a client built with schema: slingSchema supplies the mutation and subscription roots.
QueryScope(client, onChanged: …) client.createScope(onChanged: …) (a scope built directly was never registered with the client).
GraphQLError('…') (sling_gql_test) MockGraphQLError('…') — gql_exec has a GraphQLError too.
sling_gql_gen --part-of-import --runtime-import.
import 'package:sling_gql_gen/sling_gql_gen.dart' for sanitizeIdentifier, scalarDartType, ScalarRegistry, … Not exported any more; the library is generate, generatedCodeHash, IntrospectionSchema (+ its Gql* / TypeRef types), ScalarMapping, introspectionQuery.
SlingLinkException (sling_gql_link) The typed SlingExceptions: slingExceptionFromLink maps a LinkException to SlingHttpException, SlingNetworkException, …
PaginationController.cursors / .pageCount Gone: the cache holds the pages (see pagination); the controller only has pendingCursor.
SlingException(message, graphqlErrors:, statusCode:) Sealed, no public constructor; see errors.

Now analyzer warnings (not errors), because they are runtime internals:

  • Accessor.scalar / object / list / enumValue / on / whenType / write and the other helpers are @protected: call them only from generated (or hand-written) Accessor subclasses. Read through the generated getters everywhere else.
  • Selection’s tree-building members (child, objectChild, mergeFrom, cacheKey, …), PrintedOperation.from / toCacheKeys and ListRule.evaluate are @internal. Selection.field / alias / args / children and PrintedOperation.document / variables stay public (onOperation, requests).

SlingException is a sealed hierarchy; every error you see (QueryState.error, MutationState.error, SubscriptionState.error, PaginatedState.error, SlingRequest.error, what resolve / mutate throw) is one of: SlingNetworkException, SlingTimeoutException, SlingHttpException, SlingGraphQLException, SlingAuthException, SlingCancelledException, SlingTransportException.

0.2.x
final codes = e.graphqlErrors.map((m) => (m['extensions'] as Map?)?['code']);
// 1.0
final codes = e.errors.map((err) => err.code); // List<SlingGraphQLError>
final message = switch (state.error) {
null => null,
SlingNetworkException() => 'You are offline',
SlingHttpException(:final statusCode) => 'Server error $statusCode',
SlingGraphQLException(:final errors) => errors.first.message,
final other => other.message,
};

What a transport (or a gql_link chain) throws is wrapped, never passed through raw: SlingTransportException.cause keeps the original. MutationBuilder’s mutate no longer swallows exceptions that are not SlingExceptions (thrown by your own body or optimistic callback).

errorPolicy (client-wide, or per QueryBuilder / resolve / mutate) decides what a response with both data and errors does. ErrorPolicy.none — the default — is the 0.2.x behaviour: errored paths are pruned and the scope fails. all keeps the data and the error; ignore keeps the data and hides the error. See Loading states & errors.

  • Queries are retried by default. SlingClient(retry: RetryPolicy()) is the default: three attempts in all on network errors, timeouts and HTTP 5xx, with exponential backoff and jitter. Scopes stay loading meanwhile; only the final failure becomes their (sticky) error. Pass retry: RetryPolicy.none for 0.2.x’s single attempt. Mutations are still sent once unless the call passes retry:.
  • SlingClient.dispose() no longer closes an httpClient: you passed in — it is yours; close it when you are done with it. The client still closes the one it creates when you pass none.
  • Timeouts and auth are built in. SlingClient(timeout:) (and per call) aborts an attempt with a SlingTimeoutException; a disposed widget’s request is aborted. SlingClient(auth: SlingAuth(headers: …, refresh: …)) adds credentials per attempt and refreshes once on a 401 / UNAUTHENTICATED — the token handling many apps wrote in a transport: wrapper. A wrapper keeps working; it now sees one call per attempt.

0.2.x kept each page of a connection as its own cache entry and PaginationController remembered every cursor. 1.0 merges the pages in the cache with a type policy:

SlingClient<Query>(
endpoint: uri,
schema: slingSchema,
typePolicies: {
Query: TypePolicy(fields: {'launches': RelayStylePagination()}),
},
);

PaginatedQueryBuilder requires it for its field (it asserts otherwise), reads the first page — which is the merged list — and fetches only the next page on loadMore(). PaginatedState.refetch() starts the list over from page one; the controller only tracks the page being loaded (pendingCursor). PaginatedQueryBuilder also takes QueryBuilder’s options now (fetchPolicy, maxAge, errorPolicy, timeout, debugLabel, scheduler), and PaginatedState has isSkeleton, isStale and revalidate().

CacheScope.list((q) => q.launches(first: 20).nodes) addresses the merged list when the field has RelayStylePagination. Custom policies (FieldPolicy(keyArgs:, merge:)) are described in Caching → type policies.

sling_gql_sqflite: format 2 and migrations

Section titled “sling_gql_sqflite: format 2 and migrations”
  • The store’s tables are at sqfliteFormatVersion 2. The first SqflitePersistence.open after upgrading wipes a 0.1.x store once (the app starts cold, then persists as before).
  • After that, a regenerated schema (new slingSchema.hash) migrates the store along slingSchema.fields — fields whose signature did not change are kept — instead of wiping it. That needs code generated by sling_gql_gen 1.0.
  • A corrupt file is reported to onError, deleted and recreated (loaded.recovered) instead of failing open. A second open of a path already open in the isolate throws; across isolates the last open wins (superseded, SqfliteSupersededException).
  • codec: (SqfliteCodec) encrypts or otherwise transforms every row.
  • If you implemented Cache yourself (unsupported, but possible in 0.2.x): it must implement compact(upTo:), and CacheDelta.changed no longer holds the operation roots outside a full delta — read changedFields / removedFields, or use delta.applyTo(json).

client.mutate(..., offline: true) (and MutationBuilder’s / useSlingMutation’s mutate) queues a call the server cannot be reached for, keeps its optimistic writes, shows it as MutationState.isQueued, and replays it in order. To survive a restart, give the client the store’s queue, and clear it on sign-out:

final persistence = await SqflitePersistence.open('cache.db', schema: slingSchema);
final client = SlingClient<Query>(
endpoint: uri,
schema: slingSchema,
cache: persistence.cache,
mutationQueue: persistence.mutationQueue,
);
// Sign-out: roll back and drop what is queued, then empty the store.
await client.clearMutationQueue();
await persistence.clear();

See Mutations → offline for replay rules and onQueuedMutationFailed.

MockGraphQLServer gained failNext(MockFailure.status(503) / .graphQL('…', code:) / .network()), request headers on MockRequest, and MockGraphQLError (renamed from GraphQLError) takes code:. pumpUntilSettled and disposeAfterTest are unchanged.