Flutter's UIScene Migration Is Silently Breaking iOS Deep Links
A Flutter version bump can silently break every deep link path in an iOS app — Universal Links, custom schemes, OAuth redirects — without a single error log. Teams that don't trace the event across the full native-to-Dart chain will waste days re-checking AASA files and router configs while the URL was lost in the lifecycle handoff or a stuck startup `await`.
Upgrading to Flutter 3.38 or later triggers a quiet failure in iOS deep linking. The app launches correctly, AASA files and Associated Domains check out, but `app_links` never receives the URL and `getInitialLink()` returns null. The root cause is a lifecycle split: UIScene is now the default, yet many Flutter plugins and native modules still only register with `UIApplicationDelegate`, so URL events enter a callback path that never reaches Dart. A second conflict arises from Flutter's own built-in deep link handler, which fights with third-party plugins like `app_links` or `uni_links` and can bounce the URL back to Safari when neither handler properly consumes it.
Even after fixing the native layer, the Dart side can lose the link if the listener is set up too late. Complex startup sequences that `await` a chain of SDK initializations before subscribing to `uriLinkStream` miss the event window entirely. A stuck initialization call, such as `FirebaseMessaging.instance.getInitialMessage()` never returning, freezes the entire startup state machine, making the link appear lost when the transport chain is actually intact. The fix involves registering `addSceneDelegate` in native code, disabling `FlutterDeepLinkingEnabled` when using a third-party plugin, creating the `AppLinks` instance early, and wrapping blocking SDK calls in timeouts with fallbacks.
The UIScene transition exposes a brittle assumption in Flutter's plugin ecosystem: that a single delegate registration point will always receive lifecycle events. Apple deprecated that path years ago, but the ecosystem moved only when Flutter forced the issue.
Flutter's decision to ship its own deep link handler enabled by default creates a hidden conflict surface. Two handlers consuming the same URL stream with different fallback behavior produces symptoms that look like server misconfiguration, not a client-side routing bug.
The real failure mode in many Flutter apps is not a broken link chain but a frozen startup state machine. A single `await` that never resolves blocks the entire initialization sequence, and the deep link — already delivered — sits in a buffer that never drains.