skip to content

How do you test a Flutter app's deep links with adb and xcrun simctl, including cold and warm starts, and what does each test prove?

level: middleimportance: should knowfreq 32%

answer

  1. install first with flutter run
  2. adb am start with VIEW and BROWSABLE
  3. xcrun simctl openurl booted
  4. kill the app for a cold start
  5. a real tap tests verification

basics

~20 s

After flutter run installs the app, fire the link with adb shell am start or xcrun simctl openurl booted, once with the app killed and once backgrounded; this proves the app side, while only a real tap proves verification.

solid answer

~50 s

First the app must be installed, so I run `flutter run` on the emulator, simulator or device. On Android, `adb shell 'am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "https://tickets.example.com/events/42"' com.example.tickets` delivers the link to the app; on a Simulator, `xcrun simctl openurl booted https://tickets.example.com/events/42` opens it as the system would. I run each twice: with the app **killed**, which tests the cold-start path where the link becomes the initial route (on iOS the app starts at `/` and receives the link a moment later), and with the app **in the background**, which tests `pushRouteInformation`. These prove the manifest or entitlement, Flutter's flag and the router. They do not prove verification: the Flutter docs note that the adb command launches the app even without the hosted file, so a final check is tapping the link in another app, with DevTools' Deep Links tab validating the hosted files.

code

bash · 14 lines
bash
# Android: install, then cold start (app killed) and warm start (app in background).
flutter run -d emulator-5554
adb shell am force-stop com.example.tickets
adb shell 'am start -a android.intent.action.VIEW \
    -c android.intent.category.BROWSABLE \
    -d "https://tickets.example.com/events/42"' com.example.tickets
adb shell input keyevent KEYCODE_HOME
adb shell 'am start -a android.intent.action.VIEW \
    -c android.intent.category.BROWSABLE \
    -d "https://tickets.example.com/events/43?seat=A7"' com.example.tickets

# iOS Simulator: cold and warm runs of the same link.
xcrun simctl terminate booted com.example.tickets
xcrun simctl openurl booted https://tickets.example.com/events/42

go deeper

for a junior

Recall the adb am start and xcrun simctl openurl commands, and that the app must be installed with flutter run first.

for a middle

Explain how cold and warm starts reach the Router differently on Android and iOS, and why a direct adb launch does not test verification.

for a senior

Build a repeatable link test routine covering cold, warm, signed-out and unknown-id cases, combining CLI launches, real taps and DevTools validation.

for a principal

Decide how link reliability is verified in release processes, balancing quick scripted checks against device-level end-to-end testing.

## What to test and why A deep link travels through several layers: the platform decides which app handles the URL, the Flutter embedding forwards it to Dart, and the router turns the location into screens. The command-line tools let you drive the middle and last layers quickly and repeatably. They are not a test of the first layer when a package is named explicitly, and that difference is the most important thing to know about them. ## Android with adb 1. Install the app on the emulator or device with `flutter run` (the cookbook requires this before testing). 2. Send the link: ``` adb shell 'am start -a android.intent.action.VIEW \ -c android.intent.category.BROWSABLE \ -d "https://tickets.example.com/events/42"' \ com.example.tickets ``` 3. Expect the event page for event 42. The same command works for a custom scheme, `-d "ticketapp://open/events/42"`. Because the command names the package, it delivers the intent to the app directly. The Flutter cookbook states that it launches the app even if the web files are not present, so it tests the app setup only. ## iOS with xcrun simctl 1. Install the app on a booted Simulator with `flutter run`. 2. Run `xcrun simctl openurl booted https://tickets.example.com/events/42`. 3. On a physical device, put the URL in a note and tap it. For Universal Links the Simulator path goes through the system's association check, so a new or changed `apple-app-site-association` may not be honoured until Apple's CDN has fetched it, which can take up to 24 hours. ## Cold start versus warm start The two starts reach Flutter differently, and bugs usually live in one of them: | Start | Android | iOS | |---|---|---| | cold (app killed) | the link is the initial route passed to the `Router` | the app starts with `/`, then the link is parsed shortly after | | warm (app in background) | `onNewIntent` sends `pushRouteInformation` | the delegate forwards the URL and the `Router` configures new pages | Consequences to check: - On an iOS cold start, a splash screen or redirect runs for `/` before the link arrives; make sure it does not navigate away and discard the event page. - On a warm start with a `Router`, the incoming link **replaces** the current page stack; with Navigator named routes it is pushed on top instead. - A signed-out user should reach the event after signing in, which exercises the router's redirect with the deep link. Kill the app between cold runs: swipe it away, or `adb shell am force-stop com.example.tickets` on Android. ## What each test proves - **adb or simctl launch**: manifest filter or entitlement, Flutter's deep-linking flag, router mapping, cold and warm behaviour. - **Tap from another app** (a chat, a note, an email): the full path, including domain verification and the system's choice between app and browser. - **DevTools > Deep Links**: the manifest, entitlements and hosted files, validated against each other on Android and iOS. - **Router diagnostics**: `debugLogDiagnostics: true` on go_router logs each location it receives, confirming what reached Dart. ## Common findings - **Works warm, fails cold**: something at start-up, a splash route, an auth check or an `initialLocation` override, navigates after the link has been applied; on iOS the `/` that precedes the link is a frequent trigger. - **Works cold, fails warm**: a screen treats the link as "open the app" and keeps its own state, or the app uses Navigator named routes and the pushed route has no handler. - **Opens the app on the home screen**: the link reached the app but the path matched nothing, or the custom-scheme host swallowed the first segment. - **Opens the browser on a real tap only**: verification, not the app; check the hosted file and signing fingerprints. ## Making it repeatable Keep a short script with the adb and simctl commands for the handful of link shapes the app supports (event page, event with a query, an unknown event id) and run it after every routing change. Automated end-to-end link tests belong to UI-automation tooling, but this script catches most regressions in seconds.

  • In a Flutter iOS app using a Router, what happens on a cold start from a universal link?
    The app first receives the initial route `/` and, a short time later, the link is parsed by the RouteInformationParser and the Navigator is configured with the matching pages. A splash or redirect that navigates on `/` must not discard the link that follows.
  • With a Flutter app already running, how does a deep link change the screen when the app uses a Router versus Navigator named routes?
    With a Router, the path is parsed and the Navigator is configured with a new set of pages, replacing the current stack. With Navigator named routes, the engine's `pushRoute` pushes the named route on top of the existing stack.
  • Why does the Flutter cookbook ask you to run flutter run before testing a link with adb?
    The adb command only delivers an intent; the app must already be installed on the emulator or device to receive it. `flutter run` installs and launches the build, so the later link launch has an app to open.

saying these in an interview costs you the question

  • Treats a successful adb launch as proof that verification works.
  • Tests only with the app in the background, missing cold-start bugs.
  • Expects a Router app to push a warm-start link on top of the current stack.
  • Tests before the app is installed on the device or emulator.
  • Expects a changed apple-app-site-association to apply to Simulator tests immediately.