Deep linking, attribution, and smart links for Flutter.
Part of the Grovs open-source mobile linking platform.
Quick Start · API Reference · Full Docs
The Grovs Flutter SDK provides deep linking, app links, universal links, link generation, in-app messaging, revenue tracking, and attribution for your Flutter apps. It wraps the native iOS and Android SDKs.
- Deep linking & universal links — route users to the right in-app screen, even after install
- Smart link generation — create trackable links with metadata, custom redirects, and UTM parameters
- In-app messaging — display messages and announcements from the Grovs dashboard
- Push notifications — receive push notifications for dashboard-sent messages
- Revenue tracking — log App Store, Google Play, and custom purchases with automatic attribution
- Analytics: track custom events and screen views, with automatic screen tracking via
Grovs.navigatorObserver - User identity — attach user IDs and attributes for analytics and segmentation
- Self-hosting support — point the SDK at your own backend
- Auto-configuration — platform config via
AndroidManifest.xmlandInfo.plist - Consent control: start disabled and enable the SDK once the user agrees
- Deferred deep linking: resolve links tapped before installing via fingerprinting and clipboard
- Flutter 3.3.0+
- Dart 3.9.2+
- iOS 13.0+
- Android API 24+ (Android 7.0)
Add the dependency to your pubspec.yaml:
dependencies:
grovs_flutter_plugin: ^3.0.0Then run:
flutter pub get1. Add configuration to AndroidManifest.xml
Add the Grovs API key and environment setting inside the <application> tag in android/app/src/main/AndroidManifest.xml:
<application>
<meta-data
android:name="grovs_api_key"
android:value="YOUR_API_KEY" />
<meta-data
android:name="grovs_use_test_environment"
android:value="true" /> <!-- Set to false for production -->
<!-- Optional: Custom base URL for self-hosted backends -->
<meta-data
android:name="grovs_base_url"
android:value="https://your-domain.com" />
<!-- Optional: start the SDK disabled until the user gives consent (default true) -->
<meta-data
android:name="grovs_enabled"
android:value="false" />
<!-- Optional: extra hosts accepted for clipboard deferred deep linking, comma separated -->
<meta-data
android:name="grovs_clipboard_domains"
android:value="links.example.com,promo.example.com" />
</application>2. Add intent filters
Add these to your main activity for deep link handling:
<activity android:name=".MainActivity">
<!-- Custom URL scheme -->
<intent-filter>
<data android:scheme="your_app_scheme" android:host="open" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
<!-- App links (production) -->
<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:scheme="https" android:host="your_app_host" />
</intent-filter>
<!-- App links (test) -->
<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:scheme="https" android:host="your_app_test_host" />
</intent-filter>
</activity>1. Add configuration to Info.plist
Add to ios/Runner/Info.plist:
<key>GrovsApiKey</key>
<string>YOUR_API_KEY</string>
<key>GrovsUseTestEnvironment</key>
<true/> <!-- Set to <false/> for production -->
<!-- Optional: Custom base URL for self-hosted backends -->
<key>GrovsBaseURL</key>
<string>https://your-domain.com</string>
<!-- Optional: start the SDK disabled until the user gives consent (default true) -->
<key>GrovsEnabled</key>
<false/>
<!-- Optional: extra hosts accepted for clipboard deferred deep linking -->
<key>GrovsClipboardDomains</key>
<array>
<string>links.example.com</string>
<string>promo.example.com</string>
</array>2. Configure URL schemes
Add custom URL scheme support to Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>your_app_scheme</string>
</array>
</dict>
</array>3. Configure Associated Domains
- Open your project in Xcode
- Select your app target → Signing & Capabilities tab
- Click + Capability → add Associated Domains
- Add
applinks:your_app_hostandapplinks:your_app_test_host
import 'package:grovs_flutter_plugin/grovs.dart';
final grovs = Grovs();
// Optional: enable debug logging
await grovs.setDebugLevel('info');
// Optional: set user identity for analytics
await grovs.setUserIdentifier('user_id_from_your_app');
await grovs.setUserAttributes({
'name': 'John Doe',
'plan': 'premium',
});Subscribe to the onDeeplinkReceived stream to handle incoming deep links:
import 'dart:async';
import 'package:grovs_flutter_plugin/grovs.dart';
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;
print('Opened from: $link');
// Route the user based on payload
if (payload?['screen'] == 'product') {
navigateToProduct(payload?['productId']);
}
});
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}import 'dart:async';
import 'package:grovs_flutter_plugin/models/grovs_link.dart';
StreamSubscription<GrovsError>? _errorSubscription;
_errorSubscription = grovs.onError.listen((error) {
print('Grovs ${error.code.nativeName}: ${error.message}');
});Emitted for authentication, network, event delivery, and link generation failures. iOS only for now; Android emits nothing. The latest 20 errors raised before subscribing are buffered. Cancel the subscription when disposing your widget.
The SDK is enabled by default and authenticates on launch. For apps that need user consent first:
- Set
GrovsEnabledtofalse(iOS) andgrovs_enabledtofalse(Android). The SDK is configured but makes no network calls, tracks nothing, and does not read the clipboard. - Once the user agrees, call
setSDK(true).
await grovs.setSDK(true); // user gave consent
await grovs.setSDK(false); // user withdrew consentThe SDK does not persist consent. Store the user's choice in your app and apply it on every launch. Start with the config key set to false and enable the SDK after restoring consent. Calling setSDK(false) from Dart cannot prevent native startup work when the config key is true.
While disabled, user identifier and attribute changes are kept and synced once enabled. A deep link that opened the app while disabled is processed and delivered on onDeeplinkReceived after setSDK(true).
When a user taps a link with copyToClipboard enabled and does not have the app installed, the preview page copies the link to the clipboard. On the first launch after install the native SDK checks the clipboard once to resolve that link:
- It runs only once per install, and only after the backend confirms the project had recent clipboard-enabled clicks.
- The clipboard is read only when its content looks like a URL. iOS may show the system paste notice.
- Accepted hosts are the Grovs link domains plus any
GrovsClipboardDomains/grovs_clipboard_domainsentries. Anything else is ignored and never leaves the device. - There is no separate opt-out. Starting the SDK disabled (see Consent) prevents the read.
Set the flags per link with copyToClipboardIos and copyToClipboardAndroid on GenerateLinkParams, or leave them null to use the project default.
Create smart links with metadata, payload data, and tracking parameters:
import 'package:grovs_flutter_plugin/grovs.dart';
import 'package:grovs_flutter_plugin/models/grovs_link.dart';
try {
final link = await grovs.generateLink(
GenerateLinkParams(
title: 'Check out this product',
subtitle: 'Limited time offer',
imageURL: 'https://example.com/image.jpg',
data: {
'screen': 'product',
'productId': '12345',
},
tags: ['promotion', 'share'],
tracking: TrackingParams(
utmCampaign: 'spring_sale',
utmSource: 'in_app',
utmMedium: 'share_button',
),
),
);
print('Generated: $link');
} on GrovsException catch (e) {
print('Error: ${e.message}');
}Override where a link sends users on each platform:
final link = await grovs.generateLink(
GenerateLinkParams(
title: 'Special offer',
data: {'promoId': 'summer25'},
customRedirects: CustomRedirects(
ios: CustomLinkRedirect(url: 'https://example.com/ios-promo'),
android: CustomLinkRedirect(url: 'https://example.com/android-promo'),
desktop: CustomLinkRedirect(url: 'https://example.com/desktop-promo', openAppIfInstalled: false),
),
),
);Launch the platform share sheet after generating a link:
import 'package:share_plus/share_plus.dart';
final link = await grovs.generateLink(
GenerateLinkParams(title: 'Share this', data: {'itemId': 'abc'}),
);
Share.share(link);If console messages have automatic display enabled in your dashboard, they will appear in your app without any additional integration.
Pass the device token to receive push notifications for dashboard-sent messages:
import 'package:firebase_messaging/firebase_messaging.dart';
// Get and set the token
final token = await FirebaseMessaging.instance.getToken();
if (token != null) {
await grovs.setPushToken(token);
}
// Listen for token refreshes
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
grovs.setPushToken(newToken);
});Upload your Firebase or APNs credentials in the Grovs dashboard under your platform's push notification settings.
Push notifications require a physical device. They do not work in the iOS Simulator.
Revenue tracking is currently in beta.
- Enable revenue tracking in the Grovs dashboard under Settings → Revenue Tracking
- Configure platform notifications:
- Android — Set up Google Play Real-Time Developer Notifications
- iOS — Configure App Store Server Notifications in App Store Connect
// iOS: pass the StoreKit transaction ID as a string
// Android: pass the purchase originalJson string
await grovs.logInAppPurchase('transaction_id_or_json');The SDK automatically extracts price, currency, and product info. Duplicates are filtered.
import 'package:grovs_flutter_plugin/models/grovs_link.dart';
await grovs.logCustomPurchase(
type: TransactionType.buy,
priceInCents: 999, // $9.99
currency: 'USD',
productId: 'premium_monthly',
);Use .cancel and .refund transaction types for cancellations and refunds. For store purchases, these are detected automatically via platform server notifications.
Track custom events, screen views, and tags. Lifecycle events
(install, app_open, time_spent, reactivation) are recorded
automatically by the native SDK — no setup required.
await Grovs().track(
'signup_completed',
properties: {'plan': 'pro'},
tags: ['onboarding'],
);Event names must not be empty or one of the reserved names: view, open,
install, reinstall, app_open, time_spent, reactivation,
user_referred, custom, screen_view. Properties are capped at 8 KB and
tags at 20 (enforced natively).
await Grovs().setGlobalTags(['premium', 'beta']); // pass null to clearAdd the observer to your MaterialApp:
MaterialApp(
navigatorObservers: [Grovs.navigatorObserver],
// ...
);Screen names come from each route's RouteSettings.name. Unnamed routes fall
back to the route's runtime type, which is obfuscated in release builds — name
your routes or pass a screenNameExtractor to a custom
GrovsNavigatorObserver for stable names.
await Grovs().trackScreenView('Checkout', properties: {'step': 2});Map raw screen names to friendly dashboard names:
await Grovs().setScreenAliases({'/p': 'Product', '/c': 'Cart'});| Property | Type | Description |
|---|---|---|
onError |
Stream<GrovsError> |
Native SDK errors (iOS only for now) |
onDeeplinkReceived |
Stream<DeeplinkDetails> |
Stream of deep link events |
| Method | Description |
|---|---|
setDebugLevel(level) |
Set logging level ('info', 'error') |
setPushToken(token) |
Set FCM/APNs push token |
setUserIdentifier(identifier) |
Set user ID for dashboard and reports |
setUserAttributes(attributes) |
Set user attributes for analytics |
setSDK(enabled) |
Enable or disable the SDK at runtime (consent) |
generateLink(params) |
Generate a smart link |
logInAppPurchase(transactionId) |
Log a store purchase |
logCustomPurchase(type, priceInCents, currency, productId, startDate) |
Log a custom purchase |
track(name, {properties, tags}) |
Log a custom event |
trackScreenView(screenName, {properties}) |
Log a screen view manually |
setGlobalTags(tags) |
Set tags applied to all future events |
setScreenAliases(aliases) |
Map raw screen names to friendly dashboard names |
Grovs.navigatorObserver |
NavigatorObserver for automatic screen tracking |
Full API reference: docs.grovs.io/docs/sdk/flutter/api-reference
A demo project is included in the example/ directory.
Full documentation at docs.grovs.io.
For technical support and inquiries, contact support@grovs.io.
This project is licensed under the MIT License — see LICENSE for details.