The personalization opportunity
First impressions matter. When users install your app, you have a critical moment to show them what's most relevant to them. Without deferred deep links, that moment is lost.
Why personalized onboarding matters
- •First-time user activation: Users are most engaged in the first 5 minutes. Show them relevant content immediately.
- •Context from the web: User came from a product page, category, or specific content. Preserve that context in the app.
- •Skip irrelevant steps: Don't force everyone through generic onboarding. Route users based on their entry point.
- •Higher conversion: Users who see relevant content convert at higher rates than those shown a generic home screen.
Generic onboarding
- 1.User clicks link to see specific product
- 2.App installs (if needed)
- 3.Generic welcome screen appears
- 4.User must search for the product they wanted
- 5.Friction. User may abandon.
Personalized with deep links
- 1.User clicks link to see specific product
- 2.App installs (if needed)
- 3.App opens directly to that product
- 4.No friction. User sees exactly what they wanted.
- 5.Higher likelihood of purchase or engagement.
How deferred deep links enable personalization
Step 1: User on web or email
A user views a specific product on your website or in a marketing email. You create a deep link to that product: https://myapp.redirectly.app/product/blue-shoes-size-10
Step 2: User taps link on mobile
User taps the link on their phone. If the app is installed, it opens directly to the product screen.
If the app is not installed, Redirectly captures the full URL including product ID and stores it.
Step 3: Install and redirect
User is sent to App Store or Play Store. The product context is preserved in Redirectly's servers.
Step 4: App retrieves deferred link on launch
When the app launches for the first time, the Redirectly SDK queries: "What page should this user see?" Redirectly responds: "/product/blue-shoes-size-10".
Step 5: App navigates to product
Your app receives the deep link and navigates directly to the product page. User sees exactly what they wanted. First impression is perfect.
Onboarding scenarios powered by deferred deep links
E-commerce: Product page
User sees a shoe on social media. Clicks link. Doesn't have your shopping app. App installs and opens directly to that shoe product, size and color included.
Result: One-tap purchase. No searching required.
Streaming: Watch specific video
Friend shares a YouTube video link via your streaming app's sharing feature. Recipient doesn't have the app yet. App installs and opens directly to that video.
Result: Instant video playback. No discovery friction.
Social: User profile
Someone shares their profile link. New user clicks it. App installs and opens directly to that person's profile, already ready to follow or message.
Result: Immediate connection. Lower friction to social engagement.
Marketplace: Category or search results
Marketing campaign sends users to a specific category (e.g., "Winter Jackets"). Users without the app get it installed and see that category first.
Result: Campaign relevance maintained. Higher campaign ROI.
Implementation guide
Step 1: Generate contextual deep links
Whenever a user could install your app, create a deep link with context:
# Web backend - Generate deep link with context
def generate_deep_link(product_id, user_id=None, category=None):
params = {'product': product_id}
if category:
params['category'] = category
if user_id:
params['user'] = user_id
deep_link = build_redirectly_url(params)
return deep_link
# Usage: On product pages, in emails, in social shares
product_link = generate_deep_link(
product_id='blue_shoes_10',
category='footwear'
)
# Result: https://myapp.redirectly.app/shop?product=blue_shoes_10&category=footwearStep 2: Initialize and listen for deferred link
In your Flutter app, initialize Redirectly and listen for the deferred link on first launch:
import 'package:redirectly/redirectly.dart';
import 'package:go_router/go_router.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Redirectly
await Redirectly.initialize(
apiKey: 'your_api_key_here',
);
// Get deferred link on first launch
final initialLink = await Redirectly.getInitialLink();
// Determine initial route based on deep link
String initialRoute = '/home';
if (initialLink != null) {
initialRoute = _routeFromDeepLink(initialLink);
}
runApp(MyApp(initialRoute: initialRoute));
}
String _routeFromDeepLink(RedirectlyLink link) {
switch (link.path) {
case 'product':
final productId = link.queryParameters['product'];
return '/product/$productId';
case 'category':
final categoryId = link.queryParameters['category'];
return '/category/$categoryId';
case 'profile':
final userId = link.queryParameters['user'];
return '/profile/$userId';
default:
return '/home';
}
}Step 3: Route to correct screen on startup
Use go_router to navigate to the personalized screen on app launch:
final GoRouter _buildRouter(String initialRoute) {
return GoRouter(
initialLocation: initialRoute,
routes: [
GoRoute(
path: '/home',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/product/:productId',
builder: (context, state) {
final productId = state.pathParameters['productId']!;
return ProductDetailScreen(productId: productId);
},
),
GoRoute(
path: '/category/:categoryId',
builder: (context, state) {
final categoryId = state.pathParameters['categoryId']!;
return CategoryScreen(categoryId: categoryId);
},
),
GoRoute(
path: '/profile/:userId',
builder: (context, state) {
final userId = state.pathParameters['userId']!;
return UserProfileScreen(userId: userId);
},
),
],
);
}Complete onboarding example
import 'package:flutter/material.dart';
import 'package:redirectly/redirectly.dart';
import 'package:go_router/go_router.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Redirectly
await Redirectly.initialize(
apiKey: 'your_api_key_here',
);
// Get deferred deep link
final initialLink = await Redirectly.getInitialLink();
// Determine initial route
String initialRoute = '/home';
if (initialLink != null) {
initialRoute = _getRouteFromDeepLink(initialLink);
}
runApp(MyApp(initialRoute: initialRoute));
}
String _getRouteFromDeepLink(RedirectlyLink link) {
if (link.path == 'product') {
final productId = link.queryParameters['product'];
return '/product/$productId';
} else if (link.path == 'category') {
final categoryId = link.queryParameters['category'];
return '/category/$categoryId';
}
return '/home';
}
class MyApp extends StatelessWidget {
final String initialRoute;
const MyApp({required this.initialRoute});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: GoRouter(
initialLocation: initialRoute,
routes: [
GoRoute(
path: '/home',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/product/:productId',
builder: (context, state) {
final productId = state.pathParameters['productId']!;
return ProductScreen(productId: productId);
},
),
GoRoute(
path: '/category/:categoryId',
builder: (context, state) {
final categoryId = state.pathParameters['categoryId']!;
return CategoryScreen(categoryId: categoryId);
},
),
],
),
);
}
}
class ProductScreen extends StatelessWidget {
final String productId;
const ProductScreen({required this.productId});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Product')),
body: FutureBuilder<Product>(
future: fetchProduct(productId),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.loading) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
if (!snapshot.hasData) {
return const Center(child: Text('Product not found'));
}
final product = snapshot.data!;
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(product.imageUrl),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 8),
Text(
'$${product.price}',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Colors.green,
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Added to cart!')),
);
},
child: const Text('Add to Cart'),
),
],
),
),
],
),
);
},
),
);
}
Future<Product> fetchProduct(String id) async {
// Fetch product details from your API
// This is called immediately on app launch
return Product(
id: id,
name: 'Sample Product',
price: 29.99,
imageUrl: 'https://example.com/product.jpg',
);
}
}
class Product {
final String id;
final String name;
final double price;
final String imageUrl;
Product({
required this.id,
required this.name,
required this.price,
required this.imageUrl,
});
}This example shows:
- Deferred link retrieval on app launch
- Dynamic routing based on deep link content
- go_router integration for seamless navigation
- Data loading while showing the right screen
Next steps
Learn more about onboarding and deferred deep linking:
Frequently asked questions
What if the user has the app already installed?
If the app is installed, the deep link opens immediately without needing deferred link lookups. It's instant. Deferred links are a safety net for when the app isn't installed yet.
Can I personalize the onboarding screen itself?
Yes. You can pass context parameters in the deep link to show different onboarding flows. For example, new e-commerce users could see product tips, while social users see connection suggestions.
How do I handle invalid product IDs in the deep link?
Check if the product exists before navigating. If invalid, navigate to home instead. The user can always find the product through your app's normal search/browse.
Does this work with analytics?
Yes. You can log which deep links users arrived from, track how often each page is a first impression, and measure conversion from personalized onboarding.
Can I A/B test onboarding flows?
Absolutely. Pass an experiment variant in the deep link, and show different UX based on it. Measure which onboarding flow converts better.