The upgrade to Flutter 3.27 has introduced a critical regression for applications relying on established deep linking plugins like uni_links or firebase_dynamic_links. Developers are reporting immediate crashes on startup (specifically IllegalStateException on Android) or silent failures where valid deep links are ignored, defaulting the user to the initial route.
This isn't a bug in your routing logic. It is a fundamental conflict between the Flutter Engine's new default behavior and the method hooking strategies employed by third-party plugins.
Root Cause Analysis: The Race for the Intent
To understand why your app crashes, you must look at the native lifecycle events.
Historically, the Flutter Engine left deep link handling largely to the developer (or plugins). Plugins like uni_links operate by inspecting the Intent (Android) or NSUserActivity (iOS) during the onCreate or application:continueUserActivity lifecycle methods. They "swizzle" or override these methods to intercept the URL before the main Flutter view is fully established.
With recent updates culminating in Flutter 3.27, the Flutter team introduced automatic native deep link parsing. The framework now eagerly consumes the incoming Intent to populate PlatformDispatcher.defaultRouteName immediately upon engine attachment.
The Conflict:
- Android: The Flutter Engine calls
getIntent()and processes the data URI. In some configurations, it marks the intent as consumed or modifies the internal state of theFlutterActivity. Whenuni_linksorfirebase_dynamic_linkssubsequently attempts to parse the same intent in their plugin layer, they encounter a null object reference or a state exception because the Engine has already "claimed" the event. - iOS: Both the Flutter Engine and the plugins attempt to conform to the delegate methods for universal links. If the Engine handles the
userActivityand returnstrue, the event propagation stops, and the plugin listeners never fire.
To restore functionality to your existing plugins and prevent crashes, you must explicitly disable the Flutter Engine's eager deep link handling.
The Fix: Disabling Native Auto-Handling
You do not need to rewrite your Dart navigation logic immediately. You need to opt-out of the framework's native interception to return control to your plugins.
1. Android Configuration (AndroidManifest.xml)
You must add a specific meta-data tag to your AndroidManifest.xml file. This tells the Flutter embedder v2 to ignore the intent's data URI, allowing plugins to read it raw.
File: android/app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:label="My App">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Standard Intent Filter -->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- Deep Link Intent Filter -->
<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="example.com" />
</intent-filter>
<!-- THE FIX: Disable Flutter's automatic deep link handling -->
<meta-data
android:name="flutter_deeplinking_enabled"
android:value="false" />
</activity>
</application>
</manifest>
2. iOS Configuration (Info.plist)
Similarly, on iOS, you must disable the internal flag that instructs the FlutterAppDelegate to automatically bridge the NSUserActivity to the Flutter route.
File: ios/Runner/Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- ... other configurations ... -->
<!-- THE FIX: Disable Flutter's automatic deep link handling -->
<key>FlutterDeepLinkingEnabled</key>
<false/>
<!-- ... other configurations ... -->
</dict>
</plist>
3. Modern Dart Implementation
While the native config stops the crash, reliance on firebase_dynamic_links (deprecated) or older versions of uni_links is technical debt.
Below is a modern implementation using app_links (the maintained successor to uni_links) that works reliably once the native flags above are set. This service handles both "Cold Start" (app launch from link) and "Warm Start" (app running in background) scenarios.
File: lib/services/deep_link_service.dart
import 'dart:async';
import 'package:app_links/app_links.dart';
import 'package:flutter/foundation.dart';
class DeepLinkService {
// Singleton pattern
static final DeepLinkService _instance = DeepLinkService._internal();
factory DeepLinkService() => _instance;
DeepLinkService._internal();
final AppLinks _appLinks = AppLinks();
StreamSubscription<Uri>? _linkSubscription;
/// Initialize deep link listeners.
/// Call this early in your main() or App initialization.
Future<void> initialize({
required Function(Uri uri) onLinkReceived,
}) async {
// 1. Handle Cold Start (App launched via link)
try {
final Uri? initialUri = await _appLinks.getInitialLink();
if (initialUri != null) {
debugPrint('DeepLinkService: Cold start link detected: $initialUri');
onLinkReceived(initialUri);
}
} catch (e) {
// Ignore format errors on initial link, common on some Android OEMs
debugPrint('DeepLinkService: Error getting initial link: $e');
}
// 2. Handle Warm Start (App resumed via link)
_linkSubscription = _appLinks.uriLinkStream.listen(
(Uri? uri) {
if (uri != null) {
debugPrint('DeepLinkService: Background link detected: $uri');
onLinkReceived(uri);
}
},
onError: (Object err) {
debugPrint('DeepLinkService: Stream error: $err');
},
);
}
void dispose() {
_linkSubscription?.cancel();
}
}
Usage in main.dart
import 'package:flutter/material.dart';
import 'services/deep_link_service.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
@override
void initState() {
super.initState();
_initDeepLinks();
}
void _initDeepLinks() {
DeepLinkService().initialize(
onLinkReceived: (Uri uri) {
// Normalize logic: Handle your routing here
// Example: /product/123 -> push product page
if (uri.pathSegments.contains('product')) {
final id = uri.pathSegments.last;
_navigatorKey.currentState?.pushNamed('/product', arguments: id);
}
},
);
}
@override
void dispose() {
DeepLinkService().dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: _navigatorKey, // Critical for navigating without context
home: const Scaffold(body: Center(child: Text("Home"))),
routes: {
'/product': (context) => const Scaffold(body: Center(child: Text("Product Details"))),
},
);
}
}
Why This Works
By setting flutter_deeplinking_enabled to false (Android) and FlutterDeepLinkingEnabled to NO (iOS), you essentially revert the Flutter Engine's behavior to pre-3.19 standards regarding intent handling.
- Android: The
FlutterActivityno longer consumes theIntentdata duringonCreate. This leaves the Intent "fresh" for the plugin'sMethodChannelcall to retrieve via the Android Activity API. - iOS: The
FlutterAppDelegatestops intercepting theapplication:continueUserActivity:restorationHandler:method, allowing the plugin's swizzled implementation to catch the callback first.
Conclusion
Flutter's move toward native deep link handling is the correct long-term architectural decision, as it removes the dependency on third-party plugins for core functionality. However, for existing applications heavily integrated with uni_links or legacy Firebase implementations, the automatic enablement in 3.27 acts as a breaking change.
The metadata override provides a stable bridge, allowing you to maintain your current deep link architecture without crashes while you plan a migration to the native PlatformDispatcher or go_router solution.