This article focuses on multi-entry launch governance in HarmonyOS: it shows how to unify desktop, notification, service widget, Deep Link, and internal Want inputs into a single LaunchPayload, solving issues such as scattered parameters, failed relaunch handling, and duplicate initialization. Keywords: AbilityStage, UIAbility, Want.
The technical specification snapshot clarifies the launch governance scope
| Parameter | Description |
|---|---|
| Tech Stack | HarmonyOS Stage model, ArkTS/ETS, ArkUI |
| Core Components | AbilityStage, UIAbility, Want, LocalStorage |
| Launch Entrances | Desktop icon, notification, service widget, Deep Link, internal launch |
| Core Dependencies | @kit.AbilityKit, @kit.ArkUI, @kit.PerformanceAnalysisKit |
| Governance Goal | Unified launch payload, stable instance reuse, isolated page and entry logic |
| GitHub Stars | Not provided in the source content |
AbilityStage, Want, and UIAbility should be designed as one coordinated launch chain
In early HarmonyOS projects, teams often put launch parameter handling directly in the home page’s aboutToAppear. That may work when the app has only a few entry points, but once notifications, service widgets, and Deep Links are introduced, the home page quickly turns into a parameter dispatch center that becomes nearly impossible to maintain.
The real issue is not a missing callback implementation. The problem is that responsibility boundaries have been broken apart. A better design is this: AbilityStage handles process-level preparation and instance routing, Want carries only raw entry data, UIAbility injects a unified payload, and pages consume only business-ready results.
AI Visual Insight: The image shows the relationship diagram of the HarmonyOS Stage model launch chain. The core nodes revolve around AbilityStage, Want, UIAbility, and the page layer. It emphasizes that entry parameters should go through unified parsing before reaching routing and page consumption, rather than being handled in a scattered way by the home page.
The recommended responsibility split reduces entry-point sprawl
- AbilityStage: lightweight initialization and instance key routing in Specified mode.
- Want: raw entry data only; do not expose it directly to pages.
- UIAbility: handles cold start, relaunch, window creation, and payload injection.
- Page layer: receives only normalized business parameters and routing intent.
Converging raw Want data into a unified LaunchPayload enables stable evolution
Letting pages read want.parameters directly is convenient in the short term, but over time it turns pages into half routers. A unified LaunchPayload lets you lock down the information the business actually cares about, such as scene, target page, business ID, source, and extra parameters.
// common/launch/LaunchPayload.ets
export enum LaunchScene {
NORMAL = 'normal',
CARD = 'card',
NOTIFICATION = 'notification',
DEEP_LINK = 'deep_link',
INTERNAL = 'internal'
}
export interface LaunchPayload {
scene: LaunchScene
targetPage: string
bizId?: string
uri?: string
from?: string
rawAction?: string
extras: Record<string, string>
receivedAt: number // Record the receive time for launch-chain troubleshooting
}
This code defines a unified launch payload so that downstream pages and the routing layer can program only against business fields.
The value of LaunchPayload lies in controlling complexity, not copying every Want field
Keep only a whitelist of string fields in extras. For complex objects, pass an ID and let the business layer fetch details in a second step. Launch parameters should answer “where did this come from?” and “where should it go?”, not transport the entire business context.
A unified parser should be the single gateway for all launch entrances
The parser maps notifications, widgets, Deep Links, internal launches, and other sources into one unified structure while filtering out untrusted fields. This prevents if-else logic from being scattered across pages and allows problems to be governed centrally at the entry layer.
// common/launch/LaunchPayloadParser.ets
import { Want } from '@kit.AbilityKit'
import { LaunchPayload, LaunchScene } from './LaunchPayload'
export class LaunchPayloadParser {
static parse(want: Want | undefined): LaunchPayload {
const params = want?.parameters ?? {}
const uri = want?.uri ?? ''
const action = want?.action ?? ''
const scene = this.parseScene(params, uri, action)
const bizId = this.readString(params, 'bizId')
const from = this.readString(params, 'from')
return {
scene,
targetPage: this.resolveTargetPage(scene, bizId, uri),
bizId,
uri,
from,
rawAction: action,
extras: this.pickSafeExtras(params), // Extract whitelist parameters only
receivedAt: Date.now()
}
}
}
The core purpose of this code is to turn a raw Want into a controllable, traceable, and auditable business payload.
Whitelist mapping is safer than exposing route paths directly
This matters especially in Deep Link scenarios. External URIs must never control internal page paths directly. The correct approach is to map URIs to whitelisted pages such as pages/Detail and pages/Search, preventing arbitrary path navigation.
AbilityStage is better suited for process-level initialization and instance routing
AbilityStage is the module-level manager created when the HAP is loaded for the first time. It is a good place for lightweight preparation, such as log initialization, dependency container readiness, and key computation for the Specified launch mode. Do not overload it with heavy tasks such as database startup, networking, or remote configuration.
// entry/src/main/ets/entryability/MyAbilityStage.ets
import { AbilityStage, Want } from '@kit.AbilityKit'
import { LaunchPayloadParser } from '../common/launch/LaunchPayloadParser'
export default class MyAbilityStage extends AbilityStage {
onAcceptWant(want: Want): string {
const payload = LaunchPayloadParser.parse(want)
if (payload.bizId && payload.bizId.length > 0) {
return `detail_${payload.bizId}` // Reuse instances stably by business ID
}
return 'main' // Default main-entry instance
}
}
This code uses stable keys to control the granularity of instance reuse in Specified mode, avoiding task-stack disorder caused by random keys.
A stable key fundamentally means “reuse by business scenario”
Do not generate keys with timestamps or random numbers. That only disguises the “relaunch does not refresh” problem as “always create a new instance,” which eventually leads to more instances, more state confusion, and harder-to-debug task stacks.
UIAbility should unify the handling path for cold start and relaunch
Many production issues happen because parameters are parsed only in onCreate, while onNewWant is ignored. The correct pattern is to let cold start and relaunch of an existing instance share the same parsing logic. Update only the launch payload, and do not repeat global initialization.
// entry/src/main/ets/entryability/EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'
import { LaunchPayloadParser } from '../common/launch/LaunchPayloadParser'
export default class EntryAbility extends UIAbility {
private storage: LocalStorage = new LocalStorage()
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
const payload = LaunchPayloadParser.parse(want)
this.storage.setOrCreate('launchPayload', payload) // Inject the unified payload on cold start
}
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
const payload = LaunchPayloadParser.parse(want)
this.storage.setOrCreate('launchPayload', payload) // Update only the payload on relaunch
}
}
This code ensures that cold start and relaunch follow the same rules, preventing stale page state when the app is awakened from the background.
Pages can escape entry coupling only by consuming LaunchPayload
An ArkUI page should not care about the raw Want. It should decide only on prompt text, routing intent, or lightweight rendering based on LaunchPayload. That way, when you add a new entry point, the page does not need to be rewritten with new condition branches.
// entry/src/main/ets/pages/Home.ets
import { LaunchPayload, LaunchScene } from '../common/launch/LaunchPayload'
@Entry
@Component
struct Home {
@LocalStorageProp('launchPayload') launchPayload?: LaunchPayload
@State tip: string = 'Enter the home page normally'
aboutToAppear(): void {
if (this.launchPayload?.scene === LaunchScene.CARD) {
this.tip = `Entered from a service widget, business ID: ${this.launchPayload.bizId ?? 'none'}` // Update the UI based on the unified payload
}
}
}
This code shows how a page can consume only normalized parameters and avoid parsing entry details directly.
Lifecycle governance depends on correct responsibility placement, not memorizing callback order
onCreate reads the Want and generates the launch payload. onWindowStageCreate loads pages and binds the window. onNewWant updates launch intent when an existing instance is triggered again. onForeground and onBackground should handle only lightweight resume and pause work.
AI Visual Insight: The image presents the UIAbility lifecycle sequence, including key callbacks such as onCreate, onWindowStageCreate, onForeground, onNewWant, onBackground, and onDestroy. The main takeaway is that each stage carries a different responsibility: launch parsing, window loading, foreground/background recovery, and resource release should be handled in layers.
Common pitfalls usually come from mixing lifecycle responsibilities
- The home page handles entry-point judgment, causing page responsibilities to bloat.
onNewWantis forgotten, so relaunch handling fails.onForegroundrepeats listener initialization, causing duplicate callbacks.- External parameters directly control page paths, introducing security and maintenance risks.
Adding a task sequence number to the launch chain significantly reduces async overwrite risk
One of the hardest issues to debug in concurrent multi-entry scenarios is when an older request returns late and overwrites newer state. Assign an incrementing sequence number to each launch and allow only the latest launch intent to update page or route state.
// common/launch/LaunchSession.ets
import { LaunchPayload } from './LaunchPayload'
export class LaunchSession {
private currentSeq: number = 0
next(payload: LaunchPayload): number {
this.currentSeq += 1
return this.currentSeq // Assign a unique increasing sequence number to each launch
}
isLatest(seq: number): boolean {
return seq === this.currentSeq // Only the latest task may commit results
}
}
This code protects the commit order of asynchronous results and prevents an old entry from overwriting the page state of a newer one.
This protection is especially effective for concurrent notification and widget launches
When a user taps a notification and a service widget in quick succession, both launches may trigger data requests. Without sequence protection, the slower, older task may roll back the page state of the newer task, showing up as an “occasional wrong-page navigation” issue.
This pattern is better suited to multi-entry and long-lived applications
If your app has only a home page and a settings page, this governance model may feel heavy. But content, office, e-commerce, local services, file editing, and multi-UIAbility apps usually have entry points such as notifications, sharing, Deep Links, and service widgets. The earlier you converge these flows, the lower your long-term maintenance cost will be.
FAQ
1. Why not parse Want directly in the home page?
Because the home page belongs to the UI layer and should not serve as the entry gateway. If Want parsing is scattered across pages, multi-entry logic becomes fragmented, and relaunch handling, task recovery, and troubleshooting all become much harder.
2. How should onCreate and onNewWant divide responsibilities?
onCreate handles the launch payload when an instance is created for the first time. onNewWant handles new entry intent when an existing instance is triggered again. Both should share the same parser, but onNewWant should not repeat global initialization.
3. Why must Deep Links use whitelist mapping?
Because external URIs are untrusted. If you build internal route paths directly from external parameters, you risk unauthorized navigation, page coupling, and maintenance chaos. Whitelist mapping constrains external intent to controlled target pages.
Core summary: This article explains the responsibility boundaries among AbilityStage, Want, and UIAbility in the HarmonyOS Stage model. It demonstrates how to use LaunchPayload to unify launch parameters from desktop, notifications, service widgets, and Deep Links, so the home page does not devolve into an entry-point dumping ground.