DEV Community

Terence Faid JABO
Terence Faid JABO

Posted on

Flutter Deep Linking

Ever clicked a link and ended up exactly where you wanted to go? That's deep linking magic!

Imagine your friend finds a cool event on your app and wants to share it. Instead of texting you "Hey, search for Flutter Conference 2025 in that app," they just send a link. You tap it, and boom – you're looking at that exact event. No searching, no scrolling, no frustration.

That's what we're building today.

Why Deep Links Matter

Deep links are like shortcuts to specific parts of your app. They make your app feel connected to the rest of the digital world, not like a lonely island.

Here's what users get:

  • Instant access to shared content
  • No hunting through menus
  • Smooth experience from web to app

Here's what you get:

  • Better user experience (happy users stick around)
  • More effective marketing (links go straight to products)
  • Higher engagement (less friction = more action)

How Deep Links Work

Every deep link has parts, like a home address:

https://myapp.com/event/flutter-conference-2025
Enter fullscreen mode Exit fullscreen mode

Breaking it down:

  • https:// - How to deliver the link
  • myapp.com - Your app's address
  • /event/flutter-conference-2025 - The specific page

You can also add extras:

  • ?category=tech&date=2025 - Filters and options
  • #speakers - Jump to a specific section

Two Ways to Make Deep Links

Option 1: Custom Schemes (Easy Start)

myapp://event/flutter-conference
Enter fullscreen mode Exit fullscreen mode

Good: Quick to set up, works without a website
Bad: Any app can steal your scheme, looks unprofessional

Option 2: HTTPS Links (Professional)

https://myapp.com/event/flutter-conference
Enter fullscreen mode Exit fullscreen mode

Good: Secure, falls back to website, looks professional
Bad: Requires owning a domain, bit more setup

We recommend HTTPS links for real apps.

Setting Up Android

Step 1: Tell Android About Your Links

Open android/app/src/main/AndroidManifest.xml and add this inside your <activity> tag:

<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="myapp.com" />
</intent-filter>
Enter fullscreen mode Exit fullscreen mode

Step 2: Prove You Own Your Domain

Create a file called assetlinks.json:

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.myapp",
      "sha256_cert_fingerprints": ["YOUR_SHA256_FINGERPRINT"]
    }
  }
]
Enter fullscreen mode Exit fullscreen mode

Upload it to: https://myapp.com/.well-known/assetlinks.json

Step 3: Test It

adb shell am start -a android.intent.action.VIEW \
  -c android.intent.category.BROWSABLE \
  -d "https://myapp.com/event/flutter-conference" \
  com.example.myapp
Enter fullscreen mode Exit fullscreen mode

Setting Up iOS

Step 1: Add Associated Domains

  1. Open ios/Runner.xcworkspace in Xcode
  2. Select Runner → Signing & Capabilities
  3. Add "Associated Domains" capability
  4. Add: applinks:myapp.com

Step 2: Create Apple's Verification File

Create apple-app-site-association (no file extension):

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "TEAM_ID.com.example.myapp",
        "paths": ["/event/*", "/events"]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Upload to: https://myapp.com/.well-known/apple-app-site-association

Step 3: Test on iOS

xcrun simctl openurl booted "https://myapp.com/event/flutter-conference"
Enter fullscreen mode Exit fullscreen mode

Flutter Code: Making It Work

Set Up GoRouter

import 'package:go_router/go_router.dart';

final router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => HomePage(),
    ),
    GoRoute(
      path: '/events',
      builder: (context, state) => EventsPage(),
    ),
    GoRoute(
      path: '/event/:eventId',
      builder: (context, state) {
        final eventId = state.pathParameters['eventId']!;
        return EventDetailsPage(eventId: eventId);
      },
    ),
  ],
);
Enter fullscreen mode Exit fullscreen mode

Connect to Your App

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'My App',
      routerConfig: router,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Handle Missing Content

What if someone shares a link to an event that doesn't exist? Handle it gracefully:

class EventDetailsPage extends StatelessWidget {
  final String eventId;

  EventDetailsPage({required this.eventId});

  @override
  Widget build(BuildContext context) {
    // Try to find the event
    final event = findEventById(eventId);

    if (event == null) {
      return Scaffold(
        appBar: AppBar(title: Text('Oops!')),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(Icons.event_busy, size: 64, color: Colors.grey),
              SizedBox(height: 16),
              Text('Event Not Found', 
                   style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
              Text('This event might have been moved or cancelled.'),
              SizedBox(height: 16),
              ElevatedButton(
                onPressed: () => context.go('/events'),
                child: Text('Browse All Events'),
              ),
            ],
          ),
        ),
      );
    }

    // Show the event details
    return Scaffold(
      appBar: AppBar(title: Text(event.title)),
      body: Column(
        children: [
          // Event details here
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Adding Login Protection

Want to protect certain pages? Easy:

final router = GoRouter(
  redirect: (context, state) {
    final isLoggedIn = checkIfUserIsLoggedIn();
    final isLoginPage = state.matchedLocation == '/login';

    // Not logged in? Send to login
    if (!isLoggedIn && !isLoginPage) {
      return '/login?redirect=${state.matchedLocation}';
    }

    // Already logged in? Skip login page
    if (isLoggedIn && isLoginPage) {
      final redirectUrl = state.uri.queryParameters['redirect'];
      return redirectUrl ?? '/';
    }

    return null; // No redirect needed
  },
  routes: [
    // Your routes here
  ],
);
Enter fullscreen mode Exit fullscreen mode

Testing Your Links

Simple Tests

Android:

adb shell am start -a android.intent.action.VIEW \
  -c android.intent.category.BROWSABLE \
  -d "https://myapp.com/event/flutter-conference" \
  com.example.myapp
Enter fullscreen mode Exit fullscreen mode

iOS:

xcrun simctl openurl booted "https://myapp.com/event/flutter-conference"
Enter fullscreen mode Exit fullscreen mode

Real-World Tests

  1. Send the link via WhatsApp or Messages
  2. Create a simple webpage with your link
  3. Share on social media and test
  4. Test with and without your app installed

Common Problems and Fixes

Problem: App opens inside another app

Fix: Add android:launchMode="singleTask" to your activity in AndroidManifest.xml

Problem: Link opens in browser instead of app

Fix: Check your domain verification files are uploaded correctly

Problem: Multiple apps claim the same URL

Fix: Make sure android:autoVerify="true" is set and your verification files are correct

Pro Tips

  1. Keep URLs simple: myapp.com/event/flutter-summit is better than myapp.com/e/fs2025/details

  2. Plan your structure: Think about how users will share and remember links

  3. Handle errors gracefully: Always plan for missing content and network issues

  4. Test everything: Test on different devices, with and without your app installed

  5. Track performance: Use analytics to see how users interact with your links

Demo: See It in Action

Let's walk through the complete deep linking experience step by step:

1. Our App's Home Screen

First, here's what our EventSpot app looks like when users open it normally:

EventSpot Home Screen

2. Events List Screen

Users can browse all available events from our events screen:

All Events Screen

3. Deep Link in Action - Browser Entry

Now here's the magic! When someone shares a deep link, users can enter it directly in their browser:

Browser entering URL

4. App Permission Request

iOS asks for permission to open the link in our app instead of staying in the browser:

Permission to open in app

5. Direct Navigation Success!

And voilà! The user lands directly on the specific event details screen, skipping all the navigation:

Event Details Screen

6. Demo Video!

This seamless transition from web link to specific app content is what makes deep linking so powerful for user experience!

Source Code and Resources

Get the complete source code:

Additional Resources:

Wrapping Up

Deep linking transforms your Flutter app from an isolated island into a connected part of the digital world. Users can share content effortlessly, your marketing becomes more effective, and the overall experience gets much better.


Questions? Found a bug? Let me know in the comments below!

Top comments (3)

Collapse
 
rwibutso_robert_482fb3ae0 profile image
rwibutso robert

This is very handy! @faidterence thanks for sharing

Collapse
 
code_with_random_1340 profile image
Code with Random

Helpful @faidterence , a live demo would be better !

Collapse
 
faidterence profile image
Terence Faid JABO

Hi @code_with_random_1340 It's added, Thanks!