跪拜 Guibai
← All articles
Frontend · Android · Flutter

Flutter's UIScene Migration Is Silently Breaking iOS Deep Links

By 恋猫de小郭 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

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`.

Summary

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.

Takeaways
Flutter 3.38+ makes UIScene the default iOS lifecycle, but plugins that only call `registrar.addApplicationDelegate` will never receive URL events routed through `UISceneDelegate`.
`app_links` 7.0.0 adds `UISceneDelegate` support and raises the minimum Flutter version to 3.38.1, forcing a compatibility break for older projects.
Flutter's built-in deep link handler, enabled by default since 3.27, conflicts with third-party plugins and can bounce unhandled Universal Links back to Safari.
Set `FlutterDeepLinkingEnabled` to `false` in Info.plist when using a third-party deep link plugin to prevent dual-handler conflicts.
Create the `AppLinks` instance and subscribe to `uriLinkStream` as early as possible in the app lifecycle; cold-start URLs arrive before most initialization completes.
Wrap blocking third-party SDK calls like `FirebaseMessaging.instance.getInitialMessage()` in a `.timeout()` with a fallback to prevent the startup state machine from deadlocking.
Diagnose silent deep link failures by checking which layer the URL reaches: system-level AASA/domains, UIScene delegate registration, plugin event channel wiring, and Dart listener timing.
Conclusions

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.

Concepts & terms
UIScene
Apple's modern iOS app lifecycle API that manages multiple windows and scenes independently, replacing the older AppDelegate-only model. URL and user activity callbacks move from `UIApplicationDelegate` methods to `UISceneDelegate` methods.
AASA (Apple App Site Association)
A JSON file hosted at `/.well-known/apple-app-site-association` on a domain that tells iOS which paths should open in the native app via Universal Links instead of the browser.
FlutterDeepLinkingEnabled
An Info.plist key that controls Flutter's built-in deep link handler. When set to `false`, Flutter stops consuming deep link events internally, allowing a third-party plugin to take over.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗