Install & initialize
Five steps: add the dependency, register the app config in the OptoLink portal, ship your platform's link entitlements (iOS, Android, or both), initialize before runApp(), confirm the device ID. Since 2.0.0 the plugin is a thin channel client over the native OptoLink SDKs (Swift on iOS, Kotlin on Android): the natives own link resolution, deferred matching, and telemetry, so the platform setup below is the real work of the integration.
This page covers the app-side work; step ② is the only part that touches the portal. Integrating natively instead? The plugin and the natives share the same portal AppConfig and the same platform entitlements — see the Android SDK's install & initialize or the iOS SDK's install & initialize.
Requirements
- Flutter ≥ 3.38.0 (Dart ≥ 3.10). 3.38 is the oldest Flutter whose iOS engine has the scene-lifecycle API the plugin registers on.
- iOS 15.0+ deployment target; Android minSdk 23.
- A CLIENT-tier API key (
opl_sdk_…), minted on the OptoLink portal's API keys page. - An OptoLink backend at v2.1.0 or newer.
① Add the dependency
The plugin ships as a git dependency:
dependencies:
optolink_flutter:
git:
url: https://github.com/Optomatica/optolink-flutter.git
ref: main
flutter pub get
No extra declarations for the natives. SPM apps resolve the Swift SDK from the Optomatica package mirror; CocoaPods apps get the identical binary vendored inside the plugin; the Kotlin SDK arrives from Maven Central either way. Both dependency managers work as your project is set up today — no flutter config flags, no Podfile edits.
② Register the app config in the portal
The OptoLink backend serves the verification files both platforms check at install time (/.well-known/apple-app-site-association for iOS, /.well-known/assetlinks.json for Android) from the AppConfig registered for your org. Fill in the platform configs during portal onboarding (set up your organization). Your links are served from your org's link domain — the examples below use links.yourdomain.com (see custom domains for org-owned domains):
| Platform | Fields | Notes |
|---|---|---|
| iOS | bundle ID, Team ID, App Store URL | The bundle ID must match your Xcode target byte-for-byte. |
| Android | application ID, Play Store URL, SHA-256 fingerprints | Register every cert that can sign an install: debug keystore while testing, CI release keystore, Play upload and app-signing certs. |
Read a keystore fingerprint with:
keytool -list -v -keystore release.keystore -alias your-alias
A missing fingerprint is the most common cause of Android App Link verification failure: debug installs verify against the debug cert, and a build signed with an unregistered cert falls back to the link disambiguer.
③ iOS: associated domains
- In Xcode, select the Runner target → Signing & Capabilities → + Capability → Associated Domains.
- Add an entry for your link domain. Xcode creates or updates the entitlements file:
<string>applinks:links.yourdomain.com</string>
Flutter's template consumes two entitlements files; keep the applinks: entry identical in both:
| Build configuration | File |
|---|---|
| Debug | ios/Runner/Runner.entitlements |
| Profile / Release | ios/Runner/RunnerProfile.entitlements |
If Release points at a file without the entry, release builds silently lose Universal Links.
While iterating, bypass Apple's AASA cache with the developer suffix:
<string>applinks:links.yourdomain.com?mode=developer</string>
Remove the suffix before any TestFlight or App Store upload; it fails App Review's Universal Link validation. Entitlement changes ride the signed binary, and hot reload does not propagate them: run flutter clean && flutter run after any change.
④ Android: App Links intent-filter
Give your launcher activity an autoVerify filter and pin singleTop so warm deep links reuse the activity:
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="links.yourdomain.com"
android:scheme="https"
android:pathPrefix="/acme" />
</intent-filter>
</activity>
Canonical OptoLink links look like https://links.yourdomain.com/acme/shortCode; host plus pathPrefix must cover them. autoVerify makes Android fetch the assetlinks file at install. The backend serves it from your AppConfig and refreshes it hourly, so a freshly registered fingerprint can take up to an hour to reach verifiers. Check status and re-verify:
adb shell pm get-app-links com.example.shop
adb shell pm verify-app-links --re-verify com.example.shop
⑤ Initialize before runApp()
The OS delivers a cold-start deep link exactly once, at launch. Initialize in main(), before runApp():
import 'package:flutter/material.dart';
import 'package:optolink_flutter/optolink_flutter.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final optoLink = await OptoLink.initialize(
OptoLinkConfig(
apiKey: const String.fromEnvironment('OPTOLINK_API_KEY'), // CLIENT-tier opl_sdk_…
orgKey: 'acme', // the /acme/ segment of your short URLs
baseUrl: 'https://links.yourdomain.com', // your OptoLink origin — serves your short links and /match
),
);
runApp(MyApp(optoLink: optoLink));
}
flutter run --dart-define=OPTOLINK_API_KEY=opl_sdk_your_key
Verify: a successful initialize returns the instance with a non-null UUID deviceId, and debug builds print the SDK's INFO trail (OptoLink [info] … lines) without any logger wired. Routing the links it hands you: handle deep links.
Migrating from 1.x
2.0.0 swaps the pure-Dart implementation for the native-SDK plugin. Your initialize / onLink / getInitialLink / identity calls compile unchanged; what changes:
| Change | What you see |
|---|---|
| Native engines under the hood | Matching, ingestion, and telemetry run in the Swift/Kotlin SDKs. The plugin drops app_links, http, shared_preferences, device_info_plus, and flutter_timezone from your lockfile. |
| Fresh-start identity | Upgrading installs get a new deviceId (1.x device profiles are not migrated). The first launch after upgrade makes one spurious deferred-match attempt, and iOS may show the one-time paste prompt. |
| Delivery rule | Cold-start direct links arrive only via getInitialLink(); they never stream on onLink. |
| New surface | OptoLinkData.score (scored fingerprint matches only), OptoLink.lastError + OptoLinkError, the info log level. |
| Custom URI schemes deliver no payload | Only https Universal Links / App Links are ingested; a registered uriScheme still routes the redirect page. |
| New floors | Flutter ≥ 3.38.0, iOS 15+, Android minSdk 23. iOS ships both SPM and CocoaPods packaging. |
| Removed | DeviceIdStore, OptoLinkData.fromMatchResponse, OptoLinkData.fromUri. |
Call contracts are unchanged: the identity trio still returns Future<bool>, resolveDeferredLink() still returns null on no match, and initialize remains the one throwing API. Details per page: deep links, deferred deep linking, API reference.