HarmonyOS Multi-Entry Launch Architecture: Unify AbilityStage, Want, and UIAbility with LaunchPayload

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

Juejin

Juejin

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.

image.png 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.

image.png 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.
  • onNewWant is forgotten, so relaunch handling fails.
  • onForeground repeats 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.