Docs
grovs.io Start for free

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.

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:

Dart
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 after setSDK(true). See Privacy & Consent.
  • link can be null. A tapped push without a link has no link, and iOS may report a payload without a link string. Treat every DeeplinkDetails field as optional on both platforms.

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:

Dart
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 handleUrl throws a GrovsException with code INVALID_ARGUMENT.
  • Returns true when the link was handed to the native SDK.
  • Returns false and 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.

Dart
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}');
});
GrovsErrorCodeNative nameMeaning
authenticationFailedauthentication_failedAuthentication with the backend failed. Check the API key and bundle ID.
networkRequestFailednetwork_request_failedA request failed after retries. The SDK backs off and retries later.
eventDispatchFailedevent_dispatch_failedAn event batch failed to send. The events stay queued.
linkGenerationFailedlink_generation_failedLink 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:

XML
<!-- Info.plist -->
<key>GrovsClipboardDomains</key>
<array>
    <string>links.yourdomain.com</string>
</array>
XML
<!-- 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.

Next steps

Last updated 2026-10-27
Was this page helpful?