Deep linking
Handle incoming deep links, links tapped inside your app and SDK errors in your Flutter app with the Grovs SDK.
Subscribe to onDeeplinkReceived for link payloads and onError for SDK failures. Pass links tapped inside your own app to handleUrl.
Receiving deep link events
onDeeplinkReceived emits a DeeplinkDetails with the link, its custom payload and its tracking metadata. Subscribe in initState of a widget that stays mounted, such as the 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(),
},
);
}
}It fires for links that open the app, links delivered while it runs, deferred links matched after a fresh install, links passed to handleUrl and taps on Grovs push campaigns. tracking is a TrackingParams with utmCampaign, utmSource and utmMedium, each null when unset.
Delivery details
- Early links are buffered. A link resolved before Dart subscribes (the launch link, say) is held and delivered when the first listener attaches. Up to 20 events per platform; older ones are dropped.
- Broadcast stream. Several widgets can listen; cancelling one doesn't affect others. Buffered events replay only to the first subscription.
- Nothing while disabled. Links arriving under
setSDK(false)are dropped and the buffer cleared. A link that launched the app while disabled is resolved and delivered aftersetSDK(true). See Privacy & Consent. linkcan benull. A tapped push without a link has nolink, and iOS may report a payload without a link string. Treat everyDeeplinkDetailsfield as optional on both platforms.
Links tapped inside your app
iOS and Android don't hand a universal link or App Link back to the app that opened it, so a Grovs link shown in your own UI (a chat message, say) opens in a browser. Pass it to handleUrl instead:
final handled = await Grovs().handleUrl('https://your_app_host/abc');The result arrives on onDeeplinkReceived, like any other deep link.
- The URL must be absolute, with a scheme and a host. Otherwise
handleUrlthrows aGrovsExceptionwith codeINVALID_ARGUMENT. - Returns
truewhen the link was handed to the native SDK. - Returns
falseand drops the link while the SDK is disabled, and on Android when no activity is attached.
Handling SDK errors
onError emits a GrovsError when the native SDK reports an asynchronous failure. Errors are informational and the SDK recovers on its own, so use it for logging, not 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. |
Only the iOS bridge feeds onError; on Android the stream stays silent. iOS buffers the 20 most recent errors raised before you subscribe and replays them to the first listener. Errors with an unknown code are dropped. An in-app message that couldn't be shown arrives on onInAppMessage as InAppMessageFailed, not here.
Failures of your own calls, such as generateLink, throw GrovsException from the Future instead. See Error codes.
Clipboard-assisted deferred deep linking
When a copy-to-clipboard link opens in a browser, the preview page copies it to the clipboard before sending the visitor to the store. On first launch after install, the native SDK reads the clipboard once to attribute the install deterministically. The link arrives on onDeeplinkReceived.
No Dart is needed; the native SDKs handle it, including when the clipboard is read and the iOS paste notice. Rules: iOS Deep Linking and Android Deep Linking. Starting disabled with GrovsEnabled / grovs_enabled prevents the read; there's no separate opt-out.
If your links use a custom domain, list it. iOS takes an array of strings, Android 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 project default in the dashboard. Flow, settings and limits: Copy to Clipboard.
What the plugin does not expose
The iOS past-payload queries (lastReceivedPayload, allReceivedPayloadsSinceStartup) and the Android openedLinkDetails flow have no Dart counterpart. To look up a link by path, use linkDetails.