Skip to content
Back to skills

Flutter Sdk Contracts

ASecurity

Use when adding, replacing or touching any third-party SDK or platform plugin in a Flutter app (Firebase, Sentry, PostHog, Dio, secure storage, shared_preferences, notifications, share, clipboard, permissions, purchases), or when feature code imports an SDK directly. Defines application-owned contracts (ports), adapters, no-op/composite/recording implementations and their Riverpod wiring.

  • 6 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 3, 2026
ai-agentsgobashgitapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 9 files and shows the line behind each finding

Scanned October 3, 2026

npx -y skills add Ethiel97/flutter-clean-riverpod-skills --skill flutter-sdk-contracts --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Flutter Sdk Contracts?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Flutter Sdk Contracts
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/ethiel97-flutter-sdk-contracts/badge)](https://www.skillsdirectory.com/skills/ethiel97-flutter-sdk-contracts)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: flutter-sdk-contracts
description: Use when adding, replacing or touching any third-party SDK or platform plugin in a Flutter app (Firebase, Sentry, PostHog, Dio, secure storage, shared_preferences, notifications, share, clipboard, permissions, purchases), or when feature code imports an SDK directly. Defines application-owned contracts (ports), adapters, no-op/composite/recording implementations and their Riverpod wiring.
---

# SDKs behind application-owned contracts

> Every dependency that crosses the application boundary is wrapped behind
> a contract the application owns. Feature code depends on contracts, never
> on SDKs.

## Why

- **Replaceable.** Vendors change (PostHog → Amplitude, Sentry → Crashlytics).
  When only one adapter file imports the SDK, a migration touches one file.
- **Testable.** Tests override a provider with a fake; no platform channel,
  no `MissingPluginException`, no SDK initialisation in widget tests.
- **Narrow.** The contract exposes what the app *needs* (`track(event)`),
  not the SDK's whole surface, so features cannot misuse it.

## The recipe

For a capability `Foo` (analytics, error tracking, clipboard, storage…):

1. **Contract** — `abstract interface class FooService` in `core/foo/`.
   Only app-owned types in its signatures. Document *why* on the methods
   whose behaviour is surprising.
2. **App-owned vocabulary** — if the SDK takes free-form input, close it:
   a `sealed class AnalyticsEvent` instead of `track(String, Map)`.
3. **Adapter** — `FirebaseFooService implements FooService`. The only file
   that imports the SDK. It translates SDK errors into `AppFailure`
   (see `flutter-result-errors`) or swallows them if the contract says
   "never throws".
4. **No-op** — `NoOpFooService` for tests and disabled builds.
5. **Provider** — expose the contract, never the adapter:
   ```dart
   final fooServiceProvider = Provider<FooService>((ref) {
     final config = ref.watch(fooConfigProvider);
     return config.isEnabled ? SdkFooService(config) : const NoOpFooService();
   });
   ```
6. **Test double** — a recording fake in `test/helpers/` (see
   `assets/recording_analytics_service.dart`). Prefer recording fakes to
   mocks: assertions read as "this event was tracked".

Naming: `<Capability>Service`. Adapter: `<Vendor><Capability>Service`.

## Contract design rules

- **No SDK types** in parameters, return types or thrown exceptions.
- **Fallible → `AsyncResult<Unit>`** (or a typed value). **Fire-and-forget
  reporting → `Future<void>` that never throws**: reporting a failure must
  not fail the operation that caused it (`ErrorTrackingService`).
- **Low-cardinality payloads.** Never put tokens or emails in tags,
  properties or breadcrumbs.
- Keep one capability per contract. Do not make an `AnalyticsAndCrash` god
  interface.

## Config and wiring

- A required config provider throws until overridden at the composition
  root, so a missing override fails loudly at startup:
  ```dart
  final networkConfigProvider = Provider<NetworkConfig>(
    (ref) => throw UnimplementedError('override in bootstrap'),
  );
  ```
- An optional capability defaults to **disabled** (`AnalyticsConfig.disabled()`)
  so widget and provider tests never initialise a platform SDK. Bootstrap
  overrides it for real runs.
- Feature code never calls `Firebase…instance`, `Sentry.captureException`
  or `Dio()`. Only adapters and provider files do.

## Several destinations, one contract

When the same event must reach several vendors, compose behind the same
contract (`assets/composite_analytics_service.dart`). Rules: call **every**
destination even after one fails, and return the **first** failure so
partial delivery stays visible. The provider picks 0 / 1 / many with a
`switch` on the list length; features never learn the count.

## The HTTP boundary (worked example)

`assets/api_client.dart`, `dio_api_client.dart` and the `ApiException`
hierarchy (from `flutter-result-errors`) are the reference implementation:

- `ApiClient` speaks decoded JSON maps and throws only `ApiException`s.
- `DioApiClient` is the only file allowed to know Dio's API; it maps
  `DioException` types to `ApiConnectionException` / `ApiResponseException` /
  `ApiUnknownException`.
- Remote data sources depend on `ApiClient` and run through `guard(...)`.

Interceptor order matters when you build the `Dio` in a provider:

- Put the **logger before the auth interceptor**, so request headers are
  logged before the `Authorization` header is attached and tokens never hit
  the console. Enable it in debug builds only.
- Put the **breadcrumb/error-reporting interceptor last**, so a 401 that the
  refresh interceptor is about to retry successfully reads as one line.
- Use a **second Dio with no interceptors** for the token-refresh call.
  Refreshing through the main client re-enters the refresh interceptor, and
  since it is queued the nested call waits behind the very request it should
  unblock.

## Verifying the boundary

Run from the project root; each should print nothing:

```bash
# SDKs must not be imported by feature code
grep -rnE "package:(dio|firebase_[a-z_]+|sentry[a-z_]*|posthog_flutter|purchases_flutter|shared_preferences|flutter_secure_storage)/" lib/features

# Static singletons must not be reached from features
grep -rnE "(Firebase[A-Za-z]*\.instance|Sentry\.capture|SharedPreferences\.getInstance|Dio\()" lib/features
```

Adapters under `lib/core/` and the provider files that construct them are
the only legitimate importers. Tune the package list to your pubspec.

## Review checklist

- [ ] New SDK import appears in exactly one adapter file
- [ ] Contract signatures contain no SDK types
- [ ] A `NoOp` and a recording fake exist
- [ ] Provider returns the contract type (`Provider<FooService>`), not the adapter
- [ ] Optional capability defaults to disabled; required config throws until overridden
- [ ] Nothing sensitive goes into tags, properties or breadcrumbs

Files in this skill

  • SKILL.md5.9 KB
  • assets/analytics_event.dart1.1 KB
  • assets/analytics_providers.dart1.3 KB
  • assets/analytics_service.dart1.4 KB
  • assets/api_client.dart1.1 KB
  • assets/composite_analytics_service.dart1.6 KB
  • assets/dio_api_client.dart3.6 KB
  • assets/error_tracking_service.dart1.5 KB
  • assets/recording_analytics_service.dart1.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.