Design perfect onboarding experiences that work even when users install your app after clicking a marketing link. Learn how to create seamless user journeys from click to first value with deferred deep linking.
Deferred deep linking allows users to click a marketing link, install your app, and then land exactly where they intended - even if they didn't have the app installed when they first clicked. This creates seamless onboarding experiences that maintain context and drive higher engagement rates.
User clicks a link from email, social media, or advertisement
User is redirected to app store to install the app
User installs and opens the app for the first time
App detects the original intent and navigates to the right screen
User receives personalized onboarding based on their original intent
Create a service to handle deferred deep linking and onboarding:
class OnboardingService { static final FlutterRedirectly _redirectly = FlutterRedirectly(); static bool _hasProcessedInitialLink = false; static Future<void> initialize() async { await _redirectly.initialize(RedirectlyConfig( apiKey: 'YOUR_API_KEY', baseUrl: 'https://redirectly.app', enableDebugLogging: true, )); // Listen for app install events _redirectly.onAppInstalled.listen(_handleAppInstall); // Check for initial link on app start await _checkInitialLink(); } static Future<void> _checkInitialLink() async { if (_hasProcessedInitialLink) return; final initialLink = await _redirectly.getInitialLink(); if (initialLink != null) { await _processOnboardingLink(initialLink); _hasProcessedInitialLink = true; } } static void _handleAppInstall(AppInstallEvent event) { if (event.matched && event.link != null) { _processOnboardingLink(event.link!); } } static Future<void> _processOnboardingLink(LinkClickEvent event) async { if (event.error != null) { print('Onboarding link error: ${event.error}'); return; } final uri = Uri.parse(event.originalUrl); final onboardingData = _extractOnboardingData(uri); if (onboardingData != null) { // Store onboarding context await _storeOnboardingContext(onboardingData); // Navigate to appropriate onboarding flow _navigateToOnboardingFlow(onboardingData); } } static Map<String, dynamic>? _extractOnboardingData(Uri uri) { // Extract onboarding parameters from the link final source = uri.queryParameters['source']; final campaign = uri.queryParameters['campaign']; final feature = uri.queryParameters['feature']; final referrer = uri.queryParameters['ref']; if (source != null) { return { 'source': source, 'campaign': campaign, 'feature': feature, 'referrer': referrer, 'timestamp': DateTime.now().toIso8601String(), }; } return null; } static Future<void> _storeOnboardingContext(Map<String, dynamic> data) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString('onboarding_context', jsonEncode(data)); } static void _navigateToOnboardingFlow(Map<String, dynamic> data) { final source = data['source'] as String; switch (source) { case 'marketing': _navigateToMarketingOnboarding(data); break; case 'referral': _navigateToReferralOnboarding(data); break; case 'feature': _navigateToFeatureOnboarding(data); break; default: _navigateToDefaultOnboarding(); } } static void _navigateToMarketingOnboarding(Map<String, dynamic> data) { Get.offAllNamed('/onboarding/marketing', arguments: { 'campaign': data['campaign'], 'source': data['source'], }); } static void _navigateToReferralOnboarding(Map<String, dynamic> data) { Get.offAllNamed('/onboarding/referral', arguments: { 'referrer': data['referrer'], 'source': data['source'], }); } static void _navigateToFeatureOnboarding(Map<String, dynamic> data) { Get.offAllNamed('/onboarding/feature', arguments: { 'feature': data['feature'], 'source': data['source'], }); } static void _navigateToDefaultOnboarding() { Get.offAllNamed('/onboarding/default'); } }
Initialize the onboarding service when your app starts:
void main() async { WidgetsFlutterBinding.ensureInitialized(); // Initialize onboarding service await OnboardingService.initialize(); runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return GetMaterialApp( title: 'My App', theme: ThemeData( primarySwatch: Colors.blue, ), home: AuthService.isLoggedIn() ? HomeScreen() : OnboardingWrapper(), routes: { '/onboarding/default': (context) => DefaultOnboardingScreen(), '/onboarding/marketing': (context) => MarketingOnboardingScreen(), '/onboarding/referral': (context) => ReferralOnboardingScreen(), '/onboarding/feature': (context) => FeatureOnboardingScreen(), '/home': (context) => HomeScreen(), }, ); } } class OnboardingWrapper extends StatelessWidget { @override Widget build(BuildContext context) { return FutureBuilder<Map<String, dynamic>?>( future: _getOnboardingContext(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return SplashScreen(); } final context = snapshot.data; if (context != null) { // Navigate to appropriate onboarding based on context WidgetsBinding.instance.addPostFrameCallback((_) { OnboardingService._navigateToOnboardingFlow(context); }); } return DefaultOnboardingScreen(); }, ); } Future<Map<String, dynamic>?> _getOnboardingContext() async { final prefs = await SharedPreferences.getInstance(); final contextString = prefs.getString('onboarding_context'); if (contextString != null) { return jsonDecode(contextString) as Map<String, dynamic>; } return null; } }
Create personalized onboarding experiences for users coming from marketing campaigns:
class MarketingOnboardingScreen extends StatefulWidget { final String? campaign; final String? source; const MarketingOnboardingScreen({ Key? key, this.campaign, this.source, }) : super(key: key); @override _MarketingOnboardingScreenState createState() => _MarketingOnboardingScreenState(); } class _MarketingOnboardingScreenState extends State<MarketingOnboardingScreen> { int _currentStep = 0; final PageController _pageController = PageController(); final List<OnboardingStep> _steps = [ OnboardingStep( title: 'Welcome to Our App!', description: 'Thanks for joining us from our marketing campaign.', image: 'assets/onboarding/welcome.png', ), OnboardingStep( title: 'Discover Features', description: 'Explore the amazing features we have to offer.', image: 'assets/onboarding/features.png', ), OnboardingStep( title: 'Get Started', description: 'You're all set! Start exploring the app.', image: 'assets/onboarding/complete.png', ), ]; @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Column( children: [ // Progress indicator LinearProgressIndicator( value: (_currentStep + 1) / _steps.length, backgroundColor: Colors.grey[300], valueColor: AlwaysStoppedAnimation<Color>(Colors.blue), ), // Onboarding content Expanded( child: PageView.builder( controller: _pageController, onPageChanged: (index) { setState(() => _currentStep = index); }, itemCount: _steps.length, itemBuilder: (context, index) { final step = _steps[index]; return OnboardingPage( step: step, campaign: widget.campaign, source: widget.source, ); }, ), ), // Navigation buttons Padding( padding: EdgeInsets.all(24.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ if (_currentStep > 0) TextButton( onPressed: _previousStep, child: Text('Back'), ) else SizedBox.shrink(), ElevatedButton( onPressed: _nextStep, child: Text(_currentStep == _steps.length - 1 ? 'Get Started' : 'Next'), ), ], ), ), ], ), ), ); } void _previousStep() { if (_currentStep > 0) { _pageController.previousPage( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, ); } } void _nextStep() { if (_currentStep < _steps.length - 1) { _pageController.nextPage( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, ); } else { _completeOnboarding(); } } void _completeOnboarding() { // Track onboarding completion AnalyticsService.trackEvent('onboarding_completed', { 'source': widget.source, 'campaign': widget.campaign, 'flow': 'marketing', }); // Navigate to main app Get.offAllNamed('/home'); } }
You now have a complete onboarding system with deferred deep linking that provides seamless user experiences. This setup will help you convert more users and improve retention rates significantly.