A deep link can be syntactically correct, reach the right application, and still resume the wrong state.
That sounds contradictory until we separate two contracts that mobile teams often review as one: the URI-routing contract and the application-lifecycle contract.
The URI answers, “Which application can handle this return?” Android task and activity configuration helps answer, “Which activity receives it?” For a stateful .NET MAUI or Blazor Hybrid workflow, the second answer can matter just as much as the first.
The URL can be right while the lifecycle is wrong
Imagine a person leaves a mobile workflow to complete a step in the browser. The browser then opens a custom-scheme callback.
The happy-path mental model is simple:
running app -> browser -> callback -> same running app
But an activity launch mode that does not match that state model can produce this:
activity A -> browser -> callback -> fresh activity B
Activity A still owns the live UI-scoped workflow. Activity B starts with fresh activity-scoped state and may show an apparently unrelated screen. Process-level services may still be shared. The callback was parsed; continuity was not preserved.
A recent committed fix made this mismatch visible across several Android application variants. Launcher declarations were made consistent, and a focused test was added to reject a missing or incompatible setting. That is useful evidence for the configuration boundary. It is not proof that one change fixed an entire browser-to-server-to-device journey; later work found separate gaps elsewhere in that journey.
Launch mode is part of the state contract
On Android, SingleTop and SingleTask are not stylistic alternatives.
SingleTop can reuse an activity only when the relevant instance is already at the top of the target task. A callback arriving from a browser crosses task context, so that condition may not match a stateful app’s expectations.
SingleTask can bring an existing task forward and deliver the new intent to the existing activity. That can be appropriate when the workflow deliberately belongs to one activity instance. It is not a universal default: it also changes task and back-stack behaviour.
The design question is therefore not “Which launch mode is fashionable?” It is:
What lifetime owns the state that this callback is meant to resume?
Make cold and warm paths converge
A cold start and a warm callback enter through different lifecycle methods. They should quickly converge on one small, idempotent routing function.
This illustrative shape is intentionally generic:
[Activity(
MainLauncher = true,
LaunchMode = LaunchMode.SingleTask)]
public sealed class MainActivity : MauiAppCompatActivity
{
protected override void OnCreate(Bundle? state)
{
base.OnCreate(state);
RouteIncomingIntent(Intent);
}
protected override void OnNewIntent(Intent? intent)
{
base.OnNewIntent(intent);
RouteIncomingIntent(intent);
}
}
The important idea is convergence, not the exact method body. RouteIncomingIntent should treat a callback as untrusted, repeatable input. It should validate the destination, reject stale or mismatched state, and make duplicate delivery harmless.
Process death remains a separate case. SingleTask can reuse an existing activity; it cannot preserve memory after the operating system has killed the process. Durable workflow identity and server-side reconciliation still matter.
Test configuration as architecture
Multi-variant applications make small configuration drift surprisingly expensive. A template may be correct while one shipped application is not. Manual review is weak evidence when the invariant can be discovered mechanically.
A lightweight architecture test can:
- Discover every Android launcher activity in the governed app subtree.
- Fail if the launch mode is absent.
- Fail if it differs from the product’s documented lifecycle model.
- Name the offending application variant.
A source-level guard is useful, but it has limits. It does not prove the generated manifest, runtime intent flags, or real-device behaviour. A stronger pipeline adds a packaged-manifest assertion and a small instrumentation journey.
The trade-off is back-stack behaviour
Reusing one activity protects continuity only when the rest of the application is designed for it.
The trade-off includes:
- activities above the reused task entry may be removed;
- Back and Recents can behave differently;
- repeated callbacks reach a live object graph;
- stale intents can compete with newer workflow state;
- incoming flags or intermediary callback activities may alter the route.
That means the new-intent handler must be idempotent and state-aware. It also means a configuration-only unit test cannot close the evidence gap by itself.
Use a lifecycle matrix, not one happy path
For every external callback, exercise at least these states:
- app never started;
- app warm in the foreground;
- app backgrounded behind the browser;
- process killed while the browser is open;
- callback delivered twice;
- Back pressed after return;
- task reopened from Recents.
Observe which activity and task receive the intent, whether durable workflow identity is restored, and whether duplicate delivery changes the outcome.
The practical lesson is simple: a deep link is an app-lifecycle event, not merely a URL. Align launch semantics with state lifetime, converge cold and warm handling, pin the configuration across every variant, and verify the journey on a real device.
Which callback in your app currently has URI tests but no lifecycle matrix?
Top comments (0)