Deep Linking
Handle incoming deep links and SDK errors in your Flutter app with the Grovs SDK
Receiving deep link events
Subscribe to onDeeplinkReceived. It emits a DeeplinkDetails with the link that opened the app, its custom payload, and its tracking metadata. Set the listener up in initState of a widget that stays mounted, such as your root widget:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:grovs_flutter_plugin/grovs.dart';
import 'package:grovs_flutter_plugin/models/grovs_link.dart';
final navigatorKey = GlobalKey<NavigatorState>();
class App extends StatefulWidget {
const App({super.key});
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
final _grovs = Grovs();
StreamSubscription<DeeplinkDetails>? _subscription;
@override
void initState() {
super.initState();
_subscription = _grovs.onDeeplinkReceived.listen((details) {
final link = details.link;
final payload = details.data;
final tracking = details.tracking;
debugPrint('Opened from $link with $payload, campaign ${tracking?.utmCampaign}');
// Route the user based on the payload
if (payload?['screen'] == 'product') {
navigatorKey.currentState?.pushNamed(
'/product',
arguments: payload?['productId'],
);
}
});
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorKey,
routes: {
'/': (_) => const HomeScreen(),
'/product': (_) => const ProductScreen(),
},
);
}
}The stream fires for links the app was opened with, for links delivered while it is running, and for deferred links matched after a fresh install. tracking is a TrackingParams with utmCampaign, utmSource, and utmMedium; each field is null when the link carries no value for it.
Delivery details
- Early links are buffered. A link resolved before Dart subscribes, such as the one the app was launched with, is held by the plugin and delivered as soon as the first listener attaches. Up to 20 events are kept per platform; older ones are dropped.
- The stream is a broadcast stream. Several widgets can listen at once, and cancelling one subscription does not affect the others. Buffered events are replayed only to the first subscription.
- Nothing is delivered while the SDK is disabled. Links that arrive while
setSDK(false)is in effect are dropped, and the buffer is cleared. A link that launched the app while disabled is resolved and delivered aftersetSDK(true). See Privacy & Consent. linkcan benull. On iOS the native SDK may report a payload without a link string; on both platforms treat every field ofDeeplinkDetailsas optional.
Handling SDK errors
onError emits a GrovsError when the native SDK reports an asynchronous failure. Errors are informational: the SDK recovers on its own, so use the stream for logging rather than for control flow.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:grovs_flutter_plugin/grovs.dart';
import 'package:grovs_flutter_plugin/models/grovs_link.dart';
StreamSubscription<GrovsError>? _errorSubscription;
_errorSubscription = Grovs().onError.listen((error) {
debugPrint('Grovs ${error.code.nativeName}: ${error.message}');
});GrovsErrorCode | Native name | Meaning |
|---|---|---|
authenticationFailed | authentication_failed | Authentication with the backend failed. Check the API key and bundle ID. |
networkRequestFailed | network_request_failed | A request failed after retries. The SDK backs off and retries later. |
eventDispatchFailed | event_dispatch_failed | An event batch failed to send. The events stay queued. |
linkGenerationFailed | link_generation_failed | Link generation failed. |
onError is fed by the iOS bridge only. The Android bridge opens the channel but never emits on it, so on Android the stream stays silent. The 20 most recent errors raised before you subscribe are buffered on iOS and replayed to the first listener. Errors with a code the plugin does not know are dropped.
Failures of calls you make yourself, such as generateLink, do not go through onError; they throw GrovsException from the Future. See Error codes.
Clipboard-assisted deferred deep linking
When a link with copy-to-clipboard enabled is opened in a browser, the preview page copies it to the visitor's clipboard before sending them to the store. On the first launch after install, the native SDK reads the clipboard once to attribute the install deterministically. The deferred link arrives on onDeeplinkReceived like any other.
This works with no Dart integration and is handled entirely by the native SDKs, including the conditions under which the clipboard is read and the system paste notice iOS may show. The rules are described in iOS Deep Linking and Android Deep Linking. Starting the SDK disabled with GrovsEnabled / grovs_enabled prevents the read; there is no separate opt-out.
If your project serves links from a custom domain, list it so the SDK recognizes your links. On iOS the value is an array of strings; on Android it is one comma-separated string:
<!-- Info.plist -->
<key>GrovsClipboardDomains</key>
<array>
<string>links.yourdomain.com</string>
</array><!-- AndroidManifest.xml, inside <application> -->
<meta-data
android:name="grovs_clipboard_domains"
android:value="links.yourdomain.com" />Enable copy-to-clipboard per link with copyToClipboardIos and copyToClipboardAndroid on GenerateLinkParams, or set the default for the whole project in the dashboard. The end-to-end flow, settings, and limits are described in Copy to Clipboard.
What the plugin does not expose
The Dart API covers the deep link stream and the error stream. The native linkDetails lookup, the iOS past-payload query, and the Android openedLinkDetails flow have no Dart counterpart in 3.0.0. If you need them, call the native SDK from platform code.