Skip to main content

Flutter SDK contract

Scope: the platform-channel boundary optolink_flutter owns. It is a thin plugin channelling the native SDKs — every HTTP call, deferred match, signal collection, and identity store runs natively, so the wire contract is Android + iOS, not this page.

Source-checked against optolink-flutter @ 6ad24b4, 2026-10-07 (pubspec.yaml, lib/src/optolink.dart, link_data.dart, optolink_error.dart, android/build.gradle.kts, ios/optolink_flutter/Package.swift, CHANGELOG.md). Native pins: com.optomatica:optolink-android 0.1.1, Optomatica/optolink-ios-sdk from 0.1.0.

Identity​

pub.devoptolink_flutter 2.0.1 (2.0.1 is a README-only fix over 2.0.0 — no code changes)
Runtimezero non-Flutter Dart dependencies — the 1.x Dart match stack is deleted, no fallback path
Repooptolink-flutter/
Native pinsAndroid optolink-android 0.1.1 (Gradle); iOS optolink-ios-sdk from 0.1.0 (SPM; CocoaPods gets the identical bits vendored in ios/optolink_flutter/Frameworks/OptoLink.xcframework)
Compat linebackend requirements unchanged from 1.x — "Compatible with OptoLink backend v2.1.0+"

Spec delta: the hand-off spec records a git-dependency install; the plugin now ships on pub.dev (2.0.1's only change is the README for that release).

Floors​

FloorVersionEnforced by
Flutter≥ 3.38.0pubspec flutter: constraint (the scene-lifecycle plugin API the iOS bridge calls)
Dart≥ 3.10pubspec sdk: constraint
iOS deployment target15.0Package.swift .iOS(.v15) + podspec
Android minSdk23plugin build.gradle.kts (the native SDK's floor)

Three independent semver clocks (plugin / natives / backend), no release train. A breaking native or backend surface change forces a plugin major.

Channel contract (pinned)​

Two channels, fixed names, identical on both platforms:

  • MethodChannel com.optomatica.optolink_flutter/channel — six methods: initialize ({apiKey, orgKey, baseUrl, timeoutMs, maxRetries, clipboardEnabled} → {deviceId}), getInitialLink (payload map or null, idempotent), resolveDeferredLink (payload map or null), setIdentity ({externalId}), clearIdentity, trackEvent ({eventName, properties} — values coerced to strings, nulls dropped, each coercion logged). Native → Dart onLog callbacks forward the natives' INFO decision trail fire-and-forget; the Dart client registers the handler before invoking initialize.
  • EventChannel com.optomatica.optolink_flutter/links — one stream of link-payload maps, replay-last. Payload keys = native property names verbatim: linkId, path, params, matchMethod, confidence, isDeferred, score.

Delivery rule: the first isDeferred == false emission following a plugin-performed cold-start ingestion goes to the initial-link cache (getInitialLink() only, never the stream); every other emission (deferred, warm) flows to the EventChannel, which replays its last emission to a late subscriber. Preserves 1.x observable behavior: cold-start direct links on getInitialLink(), everything else on onLink.

Error codes (both platforms, surfaced as typed OptoLinkError in lastError): api_error (envelope-mapped — status, message, requestId, retryAfterSeconds, details[]), connection_error, invalid_argument (bad config, or a call made before initialize). initialize is the only thrower; the identity trio returns Future<bool> (false only on native error); the Dart edge never retries — pacing is the natives' maxRetries.

Wire mapping at the Dart edge​

matchType carries the native matchMethod wire string verbatim (UPPER_SNAKE — DIRECT, CLIPBOARD, INSTALL_REFERRER, FINGERPRINT_EXACT, FINGERPRINT_SCORED, IP_FUZZY; 'unknown' if omitted). confidence maps the lowercase wire values to MatchConfidence; unknown wire values fall back to medium. score is the backend matchScore (int, FINGERPRINT_SCORED only). Platform asymmetry is native fidelity, not a wrapper bug: an iOS scored match reads 79/MEDIUM where Android reads 86/HIGH (tz omission vs client hints).

Backend attribution write path: Device identity & match attribution.

Migration from 1.x (the integrator-visible deltas)​

Fresh-start identity: the natives keep their own device/first-launch stores — upgrading installs get a new deviceId, first launch after upgrade makes one spurious deferred-match attempt, iOS may show the one-time paste prompt. getInitialLink() returns natively-resolved data; custom URI schemes deliver no payload (only https links are ingested); audit headers switched to the natives' ios/<version> / optolink-android/<version> formats. Unchanged: the bool trio, resolveDeferredLink() → null on failure, initialize idempotence, dispose() (Dart-side reset only).

Source-checked against optolink-flutter @ 6ad24b4, 2026-10-07. Channel/behavior claims are code-read (plus the plugin's own two-platform T1/T2 gates recorded in the spec); the natives' wire contracts are re-verified on their pages.