brainstorming
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or planning anything new, in software or out of it (a talk, a business, a renovation).
harmonyos-developmentUse when developing, reviewing, debugging, or migrating HarmonyOS NEXT native apps with ArkTS, ArkUI, DevEco Studio, DevEco Code, DevEco CLI, Agent Framework Kit, Stage model, UIAbility, .ets, module.json5, oh-package.json5, HAP/HSP/HAR, API 22-24, API 26 Beta1, HarmonyOS 6.1, HarmonyOS 6.1.1 Release, HarmonyOS 7 preview, HarmonyOS 26.0.0 Beta1, state manag…
Independent GitHub snapshot · Automated classification · Original documentation governs
Covers HarmonyOS 6.1 (API 23, stable) / 6.1.1 (API 24, Release) / HarmonyOS 7 developer preview (API 26 Beta1) / NEXT native app development — the Huawei mobile OS family that runs independently of Android (AOSP-free since HarmonyOS NEXT, released 2024). Primary language is ArkTS (a strict, statically-checked superset of TypeScript) and the primary UI framework is ArkUI (declarative, state-driven). Use API 24 Release as the default production baseline; use API 26 Beta1 only for preview, adaptation, and early compatibility work.
| Item | Value |
|---|---|
| OS | HarmonyOS 6.1 (stable, released 2026/04/20, API 23). HarmonyOS 6.1.1 (Release, released 2026/05/26, API 24). HarmonyOS 7 / 26.0.0 Beta1 (developer preview, released 2026/06/12, API 26). Pure HarmonyOS, AOSP-free |
| Language | ArkTS (primary), Cangjie (beta), C/C++ via NAPI |
| UI framework | ArkUI declarative (ArkUI-X for cross-platform) |
| Compiler | ArkCompiler — AOT to native machine code; LiteActor concurrency |
| Package manager | ohpm — oh-package.json5; registry at DevEco Service (OHPM Central) |
| IDE | DevEco Studio 6.1.1 Release (6.1.1.280; API 24 production). DevEco Studio 26.0.0 Beta1 (26.0.0.461; API 26 preview) |
| App model | Stage model (FA model is legacy — don't use in new apps) |
| Packaging | HAP (entry/feature), HSP (shared package), HAR (static archive), atomic .app |
| Recommended API | Use API 24 Release for production. Use API 26 Beta1 only for HarmonyOS 7 preview/adaptation. |
| Sample catalog | https://developer.huawei.com/consumer/cn/samples/ |
Release timeline (recent):
ArkUI enhancements:
Navigation supports binding the routing stack to the component itself and specifying a NavDestination as the navigation bar (home page) — no more separate root container neededMenu adds anchorPosition property: control popup position relative to upper-left of anchor with horizontal/vertical offsetsImage component improved SVG parsing capabilitiesNew C-side capabilities (Native/NAPI):
Data:
relationalStore enhanced sendable function — better cross-thread data passingGraphics & AI:
ArkWeb: further capability enhancements (intercept/cookies)
Version baseline: API 24 is now a Release SDK (not only Beta). Use DevEco Studio 6.1.1 Release (6.1.1.280), HarmonyOS SDK 6.1.1 Release, Hvigor 6.24.2, ohpm 6.1.2.268, and Node.js 18.20.1 for API 24 projects.
Release additions over Beta1:
AbilityStage adds callbacks before the first Ability is created and when a process starts from an application snapshot.enableLocalHandleDetection to keep EventHandler/libuv tasks inside the intended scope and avoid leaks; XML parsing adds XmlSAXHandler callbacks for SAX-style parsing.Beta1 additions still relevant in API 24 Release:
// Conceptual API — full signature in official docs
cameraSession.enableSubjectTracking(camera.TrackingMode.PERSON);
taskpool.execute() timeout settings.UIContext, async drag-drop decisions, onNeedSoftkeyboard, Canvas/OffscreenCanvas antialias, Tabs nested scrolling, multiline ellipsis modes (MULTILINE_START, MULTILINE_CENTER).strictCheckerOnly for faster strict syntax checks.Status (checked 2026/07/25): developer Beta, not the default production baseline. Huawei has not published API 26 Beta2, RC, or Release. Mention API 26 features only when the user asks about HarmonyOS 7, API 26, HDC 2026, preview adaptation, or Beta1 capabilities. For production code, prefer API 24 Release unless the project explicitly targets API 26 preview.
Developer kit baseline: HarmonyOS SDK 26.0.0 Beta1 (OpenHarmony SDK Ohos_sdk_public 26.0.0.23, API Version 26.0.0 Beta1) and DevEco Studio 26.0.0 Beta1 (26.0.0.461). Toolchain: HarmonyOS Emulator 26.0.0.200, Hvigor/hvigorw 6.26.1, ohpm 26.0.0.410, Node.js 24.14.1, hstack 6.0.0, compileSdkVersion: "26.0.0", targetSdkVersion: "4.0.0(10)~26.0.0".
Version-number rule: Starting with API 26.0.0, HarmonyOS developer kit API versions use SemVer (X.Y.Z) instead of the legacy X.Y.Z(N) format. X means a major version with substantial capabilities or adaptation-impacting changes, Y means a minor version with new capabilities, and Z means compatible fixes/small improvements.
High-value API 26 Beta1 changes:
ModularObjectExtensionAbility.LoginWithHuaweiIDButton supports custom multilingual text and loading animation.@Reusable / @ReusableV2; standard floating windows; title-area customization updates.repeatInterval and repeatCount.UNCACHE open option, recursive listFileExt, mmap-based file mapping, and sharing an app sandbox directory as system-visible.getPolicy and isKia.ArrayBuffer from external memory.startScan scans all discoverable nearby NearLink devices.netBoost.setDataFlowDesc sets flow descriptions from five-tuple data.HttpVersionSelectCallback for HTTP version selection, HMS_Rcp_SetRequestGetDataCallback() for streaming upload, HMS_Rcp_SetFormOrder() for ordered forms, and QUIC C API.API 26 behavior and UX changes to watch during adaptation:
COMMON_EVENT_PACKAGE_ADDED, REMOVED, CHANGED, CACHE_CLEARED) add controls for In-House apps when targetSdkVersion >= 26.0.0; In-House apps must configure allowListenBundleChangedEvent in app.json5 for third-party listeners.fastConvertToJSObject now preserves sibling text nodes when parsing XML.NodeAdapter.onAttachToNode, mouse rawDeltaX/rawDeltaY, LayoutPolicy.matchParent, EmbeddedComponent focus, WithTheme, queryNavDestinationInfo / onResult on the home NavDestination, NODE_SWIPER_EVENT_ON_CONTENT_DID_SCROLL, and shadow blur radius behavior have adaptation-impacting changes.ohos.permission.READ_IMAGEVIDEO, getUidRxBytes, getUidTxBytes, and general permission policy behavior change under API 26 rules.metadata name ohos.arkui.UIMaterial.state value disable, or per component with uiMaterial.Material.empty.API 26 V2 behavior-scope notes (official docs updated 2026/06/17):
fastConvertToJSObject sibling-text preservation, ArkUI rawDeltaX/rawDeltaY, Stage-only ArkUI API constraints, home NavDestination queryNavDestinationInfo / onResult, @ReusableV2 dynamic reuse identifiers, ArkWeb Chromium 132 → 144, READ_IMAGEVIDEO permission behavior, and notofonts small-language font updates.targetSdkVersion >= 26.0.0: In-House package-change event controls, JSVM Wasm jitless default behavior, NodeAdapter.onAttachToNode, attributed-string paragraph style with leading CustomSpan / ImageAttachment, LayoutPolicy.matchParent, EmbeddedComponent focus, WithTheme, NODE_SWIPER_EVENT_ON_CONTENT_DID_SCROLL, component shadow blur radius, getUidRxBytes / getUidTxBytes, permission policy changes, 32vp form-control touch targets, built-in text style optimization, immersive material defaults, and half-modal centered dialog max height.DevEco Studio 26.0.0 Beta1 additions:
UI Verification tool, and custom Commands.apiCompatibilityCheck, tsImportSoCheck, module nativeLib.enableSoDirCollection, syncNative, Hvigor getAllDependencyInfo, AppGallery package re-signing, Linux emulator support, and ohpmrc auto_skip_install, metadata_cache_effective, metadata_cache, plus exact-version metadata queries.ohpm-repo 5.5.1 no longer depends on node-fetch; plugins that relied on that bundled dependency must replace it or install node-fetch@2.7.0 themselves.DevEco Testing 26.0.0 Beta1:
HarmonyOS AI development tools and capability highlights (officially surfaced 2026/06):
7.0.0.23 for early compatibility validation.For tool selection, capability boundaries, and answer rules, read references/ai-development-tools.md.
Huawei's release docs did not add a newer SDK after 26.0.0 Beta1, but the documentation change log added several developer-facing guides worth surfacing in answers:
hidumper.No newer HarmonyOS 7 SDK was published, but several official guides now affect implementation and review answers:
APIAVAILABLE for APIs newer than compatibleSdkVersion. Correct link dependencies, weak-library configuration for libraries absent on old devices, runtime fallbacks, and tests on both the oldest compatible device and the new-API device are mandatory. Compilation alone is insufficient because a missing dependency can fail only at runtime. Read references/native-api-compatibility.md.--no-daemon, protect signing secrets, install signed HAPs with HDC, and enable caseSensitiveCheck to expose filename/import mismatches hidden by Windows or macOS. Read references/build-sign-release.md.@ohos.hiviewdfx.jsLeakWatcher primarily in development; production use should be limited to a small gray-release population. Combine it with JS Heap snapshots, HWASan/AddrSanitizer, AppFreeze, and HiAppEvent according to the suspected layer. Read references/performance.md.ContainerReader when layout must react to the containing component rather than the window. API 26 also documents centralized reuse pools for @Reusable / @ReusableV2; reuse requires explicit lifecycle cleanup and state reset. Read references/arkui-components.md.MyApp/
├─ AppScope/
│ ├─ app.json5 # global app config (bundleName, icon, label)
│ └─ resources/ # shared resources
├─ entry/ # main HAP (entry module)
│ ├─ src/main/
│ │ ├─ ets/
│ │ │ ├─ entryability/EntryAbility.ets # UIAbility subclass
│ │ │ ├─ entrybackupability/
│ │ │ └─ pages/Index.ets # ArkUI pages
│ │ ├─ resources/ # strings, colors, media, profiles
│ │ └─ module.json5 # module config, abilities, permissions
│ └─ build-profile.json5
├─ oh-package.json5 # ohpm dependencies
└─ build-profile.json5 # project-level
ServiceExtensionAbility — background services (system apps)FormExtensionAbility — home-screen widget (服务卡片)WorkSchedulerExtensionAbility — deferred tasksInputMethodExtensionAbility, WallpaperExtensionAbility, BackupExtensionAbility, etc.import { UIAbility, Want, AbilityConstant } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { }
onDestroy(): void { }
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => { /* ... */ });
}
onWindowStageDestroy(): void { }
onForeground(): void { }
onBackground(): void { }
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void { }
}
import { common, Want } from '@kit.AbilityKit';
const ctx = getContext(this) as common.UIAbilityContext;
const want: Want = {
bundleName: 'com.example.app',
abilityName: 'DetailAbility',
parameters: { id: 42 }
};
ctx.startAbility(want);
// or startAbilityForResult for a return value
{
"module": {
"name": "entry",
"type": "entry", // entry | feature | shared
"srcEntry": "./ets/entryability/EntryAbility.ets",
"deviceTypes": ["phone", "tablet", "2in1"],
"abilities": [{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"skills": [{
"actions": ["action.system.home"],
"entities": ["entity.system.home"]
}]
}],
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" }
]
}
}
ArkTS = TypeScript MINUS dynamic patterns, PLUS stricter static typing for AOT perf.
Prohibited / changed vs TS:
any / unknown as everyday escape hatches — declare real typesobj.foo = bar on untyped object is a compile error)Function.prototype.bind, call, apply with reshaped thisObject.keys/Object.assign for arbitrary reshapingRecord<K,V> for true dictionary mapsclass / interface with explicit fields// Prefer:
class User { constructor(public id: number, public name: string) {} }
// Over:
// const user: any = { id: 1, name: 'A' }
| Element | Convention | Example |
|---|---|---|
| Classes, Structs, Enums | UpperCamelCase | PersonInfo, ColorType |
| Variables, Parameters, Methods | lowerCamelCase | userName, getUserInfo() |
| Constants | UPPER_SNAKE_CASE | MAX_VALUE, DEFAULT_TIMEOUT |
| Boolean variables | Prefix with is, has, can | isVisible, hasPermission |
Formatting: 2-space indent, max 120 chars/line, K&R braces, always use {} for if/for/while.
const for unchanging values — enables engine optimizationlet n = 1; n = 1.1; causes boxing overheadInt8Array, Float32Array) for numerical computationarr[9999] = 0 forces hash-table storage (much slower)[1, "a", 2] deoptimizesforEach, map, filter, reduce) — internally optimizedbuild() pure and declarative — no side effects, load data in aboutToAppear()HashMap instead of Record for key-value operations — faster lookup/insertexport { foo } from './module'import lazy { Foo } from './heavy') for modules not needed at startup@Entry // marks route/page entry
@Component
struct Index {
@State count: number = 0;
build() {
Column({ space: 12 }) {
Text(`Count: ${this.count}`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
Button('Increment')
.onClick(() => { this.count++; })
}
.width('100%').height('100%').justifyContent(FlexAlign.Center)
}
}
All @Component:
| Callback | When | Notes |
|---|---|---|
aboutToAppear() | After created, BEFORE build() | Can change state here; changes apply in first build() |
onDidBuild() | After build() completes | Do NOT change state or call animateTo(). API 12+ |
aboutToDisappear() | Before destruction | Do NOT change state (especially @Link). No async/await |
aboutToReuse(params) | Reusable component re-added from cache | Update state with new params. API 10+ |
aboutToRecycle() | Component moving to reuse cache | Release heavy resources. API 10+ |
Only @Entry:
| Callback | When |
|---|---|
onPageShow() | Each time page is displayed |
onPageHide() | Each time page is hidden |
onBackPress(): boolean | User taps Back. Return true to override default |
Execution order (cold start):
Parent aboutToAppear → Parent build → Parent onDidBuild → Child aboutToAppear → Child build → Child onDidBuild → onPageShow
| Container | When to use | Performance |
|---|---|---|
Column / Row | Linear arrangement | Best — single-pass layout |
Stack | Overlapping / stacking | Good |
Flex | Items need stretch/shrink | Slower — extra pass for flexGrow/flexShrink |
RelativeContainer | Complex layouts, avoid deep nesting | Good — flat structure |
GridRow / GridCol | Responsive multi-device grids | Good |
List | Scrollable list with recycling | Best for long lists (with LazyForEach) |
Column/Row alignment:
justifyContent(FlexAlign.Start | .Center | .End | .SpaceBetween | .SpaceAround | .SpaceEvenly)alignItems(HorizontalAlign.Start | .Center | .End), Row → alignItems(VerticalAlign.Top | .Center | .Bottom)Stack: alignContent takes 9 positions (TopStart, Top, TopEnd, Start, Center, End, BottomStart, Bottom, BottomEnd). zIndex controls layer order.
Flex vs Column/Row: Flex requires re-layout for flexShrink/flexGrow. Always prefer Column/Row when you don't need flex behavior.
RelativeContainer: Use __container__ as anchor ID for the container itself. Each child needs .id(). Set .alignRules({ top: { anchor: 'id', align: VerticalAlign.Bottom } }).
Blank(): Fills remaining space in Row/Column — use for "label ... value" layouts: Row() { Text('Name'); Blank(); Text('Value') }.
displayPriority: Lower-priority children auto-hide when container shrinks. Row() { A().displayPriority(1); B().displayPriority(3); C().displayPriority(1) } — A and C hide first.
layoutWeight: Proportional sizing — Row() { Column().layoutWeight(1); Column().layoutWeight(2) } gives 1:2 ratio.
AttributeModifier — reusable style object:
class PrimaryButtonModifier implements AttributeModifier<ButtonAttribute> {
applyNormalAttribute(instance: ButtonAttribute): void {
instance.width('100%').height(48).fontSize(16).fontColor(Color.White).backgroundColor('#007DFF');
}
applyPressedAttribute(instance: ButtonAttribute): void {
instance.backgroundColor('#0056B3');
}
}
// Usage:
Button('Submit').attributeModifier(new PrimaryButtonModifier())
LazyForEach for large lists (only renders visible items):
class MyDataSource implements IDataSource {
private data: string[] = []
totalCount(): number { return this.data.length }
getData(index: number): string { return this.data[index] }
registerDataChangeListener(listener: DataChangeListener): void { /* ... */ }
unregisterDataChangeListener(listener: DataChangeListener): void { /* ... */ }
}
List() {
LazyForEach(this.dataSource, (item: string, index: number) => {
ListItem() { Text(item) }
}, (item: string) => item) // key generator — MUST produce unique keys
}
.cachedCount(5) // preload 5 items off-screen
@Reusable components (69% faster component creation):
@Reusable
@Component
struct MyListItem {
@State message: Message = new Message('default')
aboutToReuse(params: Record<string, ESObject>) {
this.message = params.message as Message
}
aboutToRecycle() { /* release heavy resources */ }
build() { Text(this.message.value) }
}
Rules: only works within same parent; don't nest @Reusable inside @Reusable; combine with LazyForEach.
Layout performance rules:
if/else over .visibility() — hidden components still participate in layoutRelativeContainer to flatten deep Row/Column/Flex hierarchies (documented 26% improvement)List inside Scroll — without them ALL children load at once@StorageLink for frequently-changing data — propagates to all subscribersonVisibleAreaChange — trigger logic when component enters/leaves viewport:
Image(item.url)
.onVisibleAreaChange([0.0, 1.0], (isVisible: boolean, currentRatio: number) => {
if (isVisible && currentRatio >= 1.0) { this.loadHighRes(); } // fully visible
if (!isVisible) { this.releaseImage(); } // off screen
})
Use cases: lazy image loading, video auto-play/pause on scroll, exposure analytics.
Explicit animation (animateTo) — state changes in closure animate:
this.getUIContext()?.animateTo({
duration: 300,
curve: Curve.EaseOut,
onFinish: () => { console.info('done') }
}, () => {
this.width = 200 // this change animates
})
Property animation (.animation()) — implicit, applied to preceding attributes:
Text('Hello')
.width(this.myWidth)
.animation({ duration: 500, curve: Curve.EaseIn })
Curve values: Curve.Linear | .Ease | .EaseIn | .EaseOut | .EaseInOut | .FastOutSlowIn | .Friction | .Sharp | .Smooth
Spring curves (string): 'spring(velocity,mass,stiffness,damping)', 'springMotion(response,dampingFraction)', 'responsiveSpringMotion(response,dampingFraction)'
Shared element transition:
// Bind same geometryTransition ID on source and target
Image($r('app.media.photo')).geometryTransition('picture')
// Wrap state change in animateTo
this.getUIContext()?.animateTo({ duration: 300 }, () => { this.isExpanded = !this.isExpanded })
Keyframe animation (keyframeAnimateTo) — multi-step sequences:
this.getUIContext()?.keyframeAnimateTo({ iterations: 2 }, [
{ duration: 100, event: () => { this.translateX = 10; } },
{ duration: 100, event: () => { this.translateX = -10; } },
{ duration: 100, event: () => { this.translateX = 0; } },
]);
Animation performance tips: Prefer transform properties (scale/translate/rotate/opacity) over layout properties (width/height/margin) — transforms skip re-layout.
@Entry @Component
struct MainPage {
@State currentIndex: number = 0;
@Builder tabBuilder(index: number, title: string, icon: Resource) {
Column() {
SymbolGlyph(icon).fontSize(24)
.fontColor([this.currentIndex === index ? '#007DFF' : '#99000000'])
Text(title).fontSize(10).margin({ top: 4 })
.fontColor(this.currentIndex === index ? '#007DFF' : '#99000000')
}.justifyContent(FlexAlign.Center).height('100%').width('100%')
}
build() {
Tabs({ barPosition: BarPosition.End }) { // BarPosition.Start for top
TabContent() { HomePage() }
.tabBar(this.tabBuilder(0, 'Home', $r('sys.symbol.house')))
TabContent() { MinePage() }
.tabBar(this.tabBuilder(1, 'Me', $r('sys.symbol.person')))
}
.barHeight(56)
.onChange((index) => { this.currentIndex = index; })
.scrollable(false) // disable swipe between tabs
}
}
Glass-blur tab bar: .barOverlap(true).barBackgroundBlurStyle(BlurStyle.Thin).
Swiper() {
ForEach(this.banners, (item: BannerItem) => {
Image(item.url).width('100%').height(180).borderRadius(12)
})
}
.loop(true)
.autoPlay(true)
.interval(3000)
.indicator(new DotIndicator()
.color('#33000000').selectedColor('#007DFF')
.itemWidth(8).selectedItemWidth(16))
WaterFlow({ scroller: this.scroller }) {
LazyForEach(this.dataSource, (item: CardItem) => {
FlowItem() {
Column() {
Image(item.image).width('100%').borderRadius(8)
Text(item.title).fontSize(14).padding(8)
}
}
}, (item: CardItem) => item.id)
}
.columnsTemplate('1fr 1fr') // 2 columns
.columnsGap(8)
.rowsGap(8)
.cachedCount(10)
Grid() {
ForEach(this.items, (item: GridItemData) => {
GridItem() {
Column() {
Image(item.icon).width(40).height(40)
Text(item.name).fontSize(12).margin({ top: 4 })
}
}
})
}
.columnsTemplate('1fr 1fr 1fr 1fr') // 4 columns
.rowsGap(12)
.columnsGap(12)
.height(200)
TextInput({ placeholder: 'Enter username' })
.type(InputType.Normal) // .Email, .Number, .Password, .PhoneNumber
.maxLength(20)
.onChange((value: string) => { this.username = value; })
.onSubmit((enterKey: EnterKeyType) => { /* handle submit */ })
TextArea({ placeholder: 'Enter description', text: $this.desc })
.maxLength(200)
.showCounter(true) // character count indicator
Two-way binding with $: TextInput({ text: $this.value }) — no onChange needed.
import { router } from '@kit.ArkUI';
// Push to new page (with params)
router.pushUrl({
url: 'pages/Detail',
params: { id: '123', title: 'Hello' }
});
// Get params on target page
const params = router.getParams() as Record<string, string>;
// Go back
router.back();
// Replace current page (no back stack)
router.replaceUrl({ url: 'pages/Login' });
Note: For Navigation-based apps, prefer
NavPathStack.pushPath()over Router.
// Alert dialog
AlertDialog.show({
title: 'Confirm',
message: 'Delete this item?',
primaryButton: { value: 'Cancel', action: () => {} },
secondaryButton: { value: 'Delete', fontColor: Color.Red,
action: () => { this.deleteItem(); }
},
});
// Toast
this.getUIContext().getPromptAction().showToast({
message: 'Operation successful',
duration: 2000,
});
| Component | Key Props | Example |
|---|---|---|
Checkbox | select, onChange((val: boolean) => {}) | Checkbox({ name: 'agree' }).select(this.agreed) |
Toggle | type(ToggleType.Switch), isOn, onChange | Toggle({ type: ToggleType.Switch, isOn: $this.on }) |
Radio | value, group, checked, onChange | Radio({ value: 'male', group: 'gender' }).checked(true) |
Select | options: SelectOption[], selected, value, onSelect | Select([{value:'A'},{value:'B'}]).selected(0) |
Slider | value, min, max, step, onChange | Slider({ value: $this.val, min: 0, max: 100 }) |
DatePicker | start, end, selected, onChange | DatePicker({ selected: this.date }).onChange((v) => {}) |
TimePicker | selected, useMilitaryTime, onChange | TimePicker({ selected: this.time }) |
Search | value, placeholder, onSubmit, onChange | Search({ value: $this.keyword, placeholder: 'Search' }) |
Progress | value, total, type(ProgressType.Linear) | Progress({ value: 60, total: 100 }) |
LoadingProgress | — | LoadingProgress().width(48).color('#007DFF') |
// In UIAbility — emit event
this.context.eventHub.emit('dataReady', { items: [...] });
// In page — subscribe
const context = getContext(this) as common.UIAbilityContext;
context.eventHub.on('dataReady', (data: Record<string, ESObject>) => {
this.items = data.items;
});
// Unsubscribe
context.eventHub.off('dataReady');
HarmonyOS 6.0 (API 23) introduces system-level "Immersive Light Perception" visual effects. Users enable via Settings → Desktop & Personalization → Immersive Light Effect (强/均衡/弱). Developers achieve similar effects through these ArkUI attributes:
BlurStyle enum (API 9–11):
| Name | Since | Level |
|---|---|---|
Thin / Regular / Thick | API 9 | Material blur |
BACKGROUND_THIN / BACKGROUND_REGULAR / BACKGROUND_THICK / BACKGROUND_ULTRA_THICK | API 10 | Depth-of-field (min→max) |
COMPONENT_ULTRA_THIN / COMPONENT_THIN / COMPONENT_REGULAR / COMPONENT_THICK / COMPONENT_ULTRA_THICK | API 11 | Component-level material |
NONE | API 10 | No blur |
backgroundBlurStyle (API 9+):
Column() { /* content */ }
.backgroundBlurStyle(BlurStyle.Thin, {
colorMode: ThemeColorMode.LIGHT, // SYSTEM | LIGHT | DARK
adaptiveColor: AdaptiveColor.DEFAULT, // DEFAULT | AVERAGE
scale: 1.0 // 0.0–1.0 (blur intensity)
})
foregroundBlurStyle (API 10+):
Image($r('app.media.photo'))
.foregroundBlurStyle(BlurStyle.Regular)
backgroundEffect (API 11+) — fine-grained control:
Column() { /* content */ }
.backgroundEffect({
radius: 20, // blur radius
saturation: 15, // [0, 50] recommended
brightness: 0.6, // [0, 2] recommended
color: '#80FFFFFF' // mask color
})
blur / backdropBlur (API 7+) — numeric radius:
Column() { /* content */ }
.backdropBlur(20, { grayscale: [30, 50] }) // background blur
.blur(10) // foreground blur
backgroundBrightness (API 12+):
Column() { /* content */ }
.backgroundBrightness({ rate: 0.5, lightUpDegree: 0.2 })
Visual effect filters (API 12+):
import { uiEffect } from '@kit.ArkGraphics2D';
const blurFilter = uiEffect.createFilter().blur(10);
Column() { /* content */ }
.backgroundFilter(blurFilter) // background filter
.foregroundFilter(blurFilter) // content filter
pointLight (API 11+, System API only — NOT available for third-party apps):
// System apps only! Supports: Image, Column, Flex, Row, Stack, Button, Toggle
Flex()
.pointLight({
lightSource: { positionX: '50%', positionY: '50%', positionZ: 80, intensity: 2, color: Color.White },
illuminated: IlluminatedType.BORDER, // NONE | BORDER | CONTENT | BORDER_CONTENT
bloom: 0.5 // luminous intensity 0–1
})
Up to 12 light sources can illuminate a single component. HarmonyOS 6.0 adds dual-edge flow light and UV background flow light effects.
systemMaterialEffect (HDS layer, API 23+, HarmonyOS-only SDK):
import { hdsMaterial } from '@kit.ArkUI';
Column() { /* content */ }
.systemMaterialEffect({
materialType: hdsMaterial.MaterialType.ADAPTIVE,
materialLevel: hdsMaterial.MaterialLevel.ADAPTIVE
})
Note: hdsMaterial is part of the closed-source HarmonyOS Design System (HDS), not OpenHarmony. Requires HarmonyOS 6.0 SDK (API 23+).
| Decorator | Scope | Purpose |
|---|---|---|
@State | within component | Owned mutable state; triggers re-render |
@Prop | parent → child | One-way copy (child has local copy) |
@Link | parent ↔ child | Two-way binding (use $var to pass) |
@Provide / @Consume | ancestor → descendant | Cross-level implicit binding by key |
@Observed (class) + @ObjectLink (prop) | class instances | Observe changes to class properties |
@Watch('handler') | any of above | Callback on value change |
@StorageLink / @StorageProp | app-wide AppStorage | Global reactive state |
@LocalStorageLink / @LocalStorageProp | page-scoped | Scoped reactive state |
Passing @Link:
@Entry @Component struct Parent {
@State val: number = 0;
build() { Child({ val: $val }) } // $ prefix for @Link
}
@Component struct Child {
@Link val: number;
build() { Button(`${this.val}`).onClick(() => this.val++) }
}
Observing class objects:
@Observed class Task { constructor(public title: string, public done: boolean) {} }
@Component struct TaskRow {
@ObjectLink task: Task; // re-renders when task.title/done changes
build() { Text(this.task.title) }
}
Arrays of @Observed instances require @ObjectLink in the row component — parent @State tasks: Task[] only reacts to array mutations (push/splice/reassign), not per-item changes.
State management performance rules (from official docs):
@State variables that directly affect UI@Prop creates deep copy on every update — for large objects, prefer @Link (by reference) or @ObjectLink@Link is preferred for inter-component communication — avoids unnecessary re-renders@Observed + @ObjectLink for nested objects — fine-grained property observation@ObjectLink is READ-ONLY — cannot reassign whole object (this.task = new Task() breaks binding)@StorageLink for frequently-changing data — global state changes propagate to ALL subscribers@State/@Prop/@Link observe ONLY first-level properties. Nested changes are NOT detected. Array: only push/splice/reassign/length, NOT item mutations.V2 decorators have graduated from experimental to stable as of HarmonyOS 6.1 (API 23). Official recommendation: migrate to V2 for new projects.
| V1 | V2 replacement | Change |
|---|---|---|
@Component | @ComponentV2 | Clearer semantics |
@State | @Local | Cannot be initialized externally — internal state only |
@Prop | @Param + @Once | Read-only inputs; @Once for one-time init |
@Link | @Param + @Event | Two-way: input via @Param, output via callback @Event |
@Observed + @ObjectLink | @ObservedV2 + @Trace | Deep observation across multiple nested levels |
@Watch | @Monitor('prop') | More precise deep listener |
AppStorage | AppStorageV2 | Unified with @ObservedV2 + @Trace |
| (none) | PersistenceV2 | Persistent storage with V2 observation; auto-saved to disk |
@Provide / @Consume | @Provider() / @Consumer() | Renamed; same semantics |
@ObservedV2
class UserInfo {
@Trace name: string = ''; // changes to this trigger UI refresh
@Trace age: number = 0; // changes to this trigger UI refresh
address: string = ''; // NO @Trace → changes do NOT trigger refresh
}
Rules: @ObservedV2 and @Trace must be used together (either alone has no effect). Only @Trace-decorated properties participate in UI rendering.
AppStorageV2 — global reactive state:
import { AppStorageV2 } from '@kit.ArkUI';
@ObservedV2
class UserState { @Trace name: string = 'Guest'; }
// Connect (creates if not exists)
const user = AppStorageV2.connect(UserState, 'user', () => new UserState())!;
// In component
@ComponentV2
struct Header {
user: UserState = AppStorageV2.connect(UserState, 'user')!;
build() { Text(this.user.name) }
}
PersistenceV2 — auto-persisted state (survives app restart):
import { PersistenceV2, Type } from '@kit.ArkUI';
@ObservedV2
class Settings {
@Trace @Type(String) theme: string = 'light';
@Trace @Type(Number) fontSize: number = 14;
}
// Connect — auto-loads from disk if exists, writes on change
const settings = PersistenceV2.connect(Settings, 'app_settings', () => new Settings())!;
// Optional: error/success callback
PersistenceV2.notifyOnError((key, reason, msg) => { console.error(reason, msg); });
@Typedecorator is required for PersistenceV2 to serialize correctly.
Separates state logic from UI components entirely. Works with @ObservedV2 + @Trace.
import { StateStore } from '@kit.ArkUI';
@ObservedV2
class CounterStore {
@Trace count: number = 0;
increment(): void {
this.count++;
}
}
// Create global store (do this once, e.g. in EntryAbility or top-level)
const counterStore = StateStore.createStore(new CounterStore());
// In any component — read state
@Entry
@Component
struct CounterPage {
build() {
Column() {
Text(`Count: ${counterStore.getState().count}`)
Button('Add').onClick(() => {
counterStore.getState().increment();
})
}
}
}
When to use: Multiple pages/components share the same state; state logic is complex; need thread-safe updates (TaskPool workers can safely update StateStore).
Docs: https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-global-state-management-state-store
@Builder function Label(text: string) { Text(text).fontSize(16) }
@Styles function Card() { .padding(12).borderRadius(12).backgroundColor('#FFF') }
@Extend(Text) function title() { .fontSize(22).fontWeight(FontWeight.Bold) }
// Usage:
Text('Hello').title()
Column() { Label('x') } .Card()
@Entry @Component struct App {
@Provide('pathStack') pathStack: NavPathStack = new NavPathStack();
build() {
Navigation(this.pathStack) {
Button('Go').onClick(() => this.pathStack.pushPath({ name: 'Detail', param: 42 }))
}
.navDestination((name: string, param: Object) => {
if (name === 'Detail') Detail({ id: param as number })
})
}
}
The older router module (@ohos.router) still works but is being phased out — Navigation + NavPathStack is the official replacement since API 12+. Huawei publishes a transition guide for migrating from router to Navigation. For new projects, always use Navigation; for legacy code, migrate when convenient.
NavPathStack full API:
// Push
pathStack.pushPath({ name: 'Page', param: data });
pathStack.pushPathByName('Page', data);
pathStack.pushPathByName('Page', data, (popInfo) => {
console.info('Pop result: ' + JSON.stringify(popInfo.result));
});
// Pop, Replace, Remove
pathStack.pop();
pathStack.replacePath({ name: 'Page', param: data });
pathStack.removeIndex(0);
pathStack.movePageToTop('Page');
// Query
pathStack.getParamByIndex(index);
pathStack.getParamByName('Page');
pathStack.getAllPathName();
pathStack.size();
Route interception:
pathStack.setInterception({
willShow: (from, to, operation) => { /* validate/redirect */ },
didShow: (from, to, operation) => { /* analytics */ }
});
Display modes: Stack (single column), Split (two columns), Auto (adaptive, 600vp threshold).
import { UIAbility, Want, common, abilityAccessCtrl } from '@kit.AbilityKit';
import { window, display, promptAction } from '@kit.ArkUI';
import { http, webSocket, connection } from '@kit.NetworkKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit'; // photo/video picker
import { fileIo as fs } from '@kit.CoreFileKit';
import { geoLocationManager } from '@kit.LocationKit';
import { relationalStore, preferences, distributedKVStore } from '@kit.ArkData';
import { notificationManager } from '@kit.NotificationKit';
import { speechRecognizer } from '@kit.CoreSpeechKit'; // ASR / TTS
import { hilog } from '@kit.PerformanceAnalysisKit';
应用框架 Application Framework
| Kit | Import key | Purpose |
|---|---|---|
| Ability Kit | AbilityKit | UIAbility, ExtensionAbility, Want, context, routing |
| Accessibility Kit | AccessibilityKit | Screen reader, universal design, a11y services |
| ArkData | ArkData | relationalStore, preferences, distributedKVStore |
| ArkTS | — | Language spec & Ark compiler tooling |
| ArkUI | ArkUI | window, display, promptAction, Navigation, components |
| ArkWeb | ArkWeb | WebView / Web component embedding |
| Background Tasks Kit | BackgroundTasksKit | Deferred/scheduled background work, transient tasks |
| Core File Kit | CoreFileKit | fileIo, picker, sandbox paths |
| Form Kit | FormKit | FormExtensionAbility, home-screen service cards/widgets |
| IME Kit | IMEKit | Input method engine development |
| IPC Kit | IPCKit | Inter-process communication (Parcel, RemoteObject) |
| Localization Kit | LocalizationKit | i18n, l10n, RTL, pseudo-localization |
应用服务 Application Services
| Kit | Import key | Purpose |
|---|---|---|
| Account Kit | AccountKit | One-click Huawei ID login, OAuth 2.0 |
| Ads Kit | AdsKit | Advertising SDK (banner, native, interstitial) |
| App Linking Kit | AppLinkingKit | Deep links, deferred deep links |
| Calendar Kit | CalendarKit | Calendar events, reminders |
| Contacts Kit | ContactsKit | Contact read/write/search |
| Content Embed Kit | ContentEmbedKit | Cross-app document embedding and collaborative editing |
| IAP Kit | IAPKit | In-app purchases (consumable, non-consumable, subscription) |
| Live View Kit | LiveViewKit | Live activities on lock screen / notification center |
| Location Kit | LocationKit | geoLocationManager, geofencing, geocoding |
| Map Kit | MapKit | Huawei Maps SDK, routing, place search |
| Notification Kit | NotificationKit | Local + push notifications, badges |
| Payment Kit | PaymentKit | Huawei Pay transaction processing |
| Push Kit | PushKit | Push notification delivery service |
| Scan Kit | ScanKit | QR/barcode scanning |
| Share Kit | ShareKit | Cross-app content sharing |
| Call Service Kit | CallServiceKit | Enterprise call identification and service lookup |
| Weather Service Kit | WeatherServiceKit | Weather data (current/daily/hourly/alerts/indices/tides) |
| Pen Kit | Penkit | Stylus / handwriting component (M-Pencil devices) |
| Wear Engine | WearEngine | Phone↔watch communication, device discovery |
| Health Service Kit | HealthServiceKit | Health data services |
系统 System
| Kit | Import key | Purpose |
|---|---|---|
| Asset Store Kit | AssetStoreKit | Secure credential/secret storage |
| Basic Services Kit | BasicServicesKit | Battery, vibration, thermal, brightness, clipboard |
| Connectivity Kit | ConnectivityKit | Bluetooth, Wi-Fi, NFC, USB, hotspot |
| Crypto Architecture Kit | CryptoArchitectureKit | Encryption, key management, certificates |
| Device Security Kit | DeviceSecurityKit | Code-signature queries, digital shield, security events |
| Distributed Service Kit | DistributedServiceKit | deviceManager, cross-device discovery |
| Enterprise Threat Protection Kit | EnterpriseThreatProtectionKit | Enterprise file threat scan, isolation, restore, delete |
| MDM Kit | MDMKit | Enterprise device, app, and settings management |
| Network Boost Kit | NetworkBoostKit | Network transfer optimization and low-power transfer mode |
| NearLink Kit | NearLinkKit | NearLink device capability and partner device management |
| Screen Time Guard Kit | ScreenTimeGuardKit | Screen-time authorization and app-control policy |
媒体 Media
| Kit | Import key | Purpose |
|---|---|---|
| Audio Kit | AudioKit | Audio playback, recording, routing, focus |
| AVCodec Kit | AVCodecKit | Hardware-accelerated encode/decode (H.264, H.265, AAC…) |
| Camera Kit | CameraKit | Camera preview, photo capture, video recording |
| Image Kit | ImageKit | Image decoding, transformation, EXIF |
| Media Kit | MediaKit | AVPlayer, AVRecorder (unified playback/recording) |
| Media Library Kit | MediaLibraryKit | photoAccessHelper, media library CRUD |
| Scan Kit | ScanKit | QR/barcode scanning |
图形 Graphics
| Kit | Import key | Purpose |
|---|---|---|
| ArkGraphics 2D | ArkGraphics2D | 2D Canvas drawing, effects, blur, shadow |
| ArkGraphics 3D | ArkGraphics3D | 3D scene graph, glTF rendering |
| UI Design Kit | UIDesignKit | hdsDrawable icon processing, HdsNavigation component |
| XComponent | (ArkUI built-in) | Native OpenGL ES / Vulkan surface via NAPI |
AI
| Kit | Import key | Purpose |
|---|---|---|
| Core Speech Kit | CoreSpeechKit | On-device ASR / TTS engine |
| Core Vision Kit | CoreVisionKit | OCR, face detection, image segmentation, super-resolution |
| CANN Kit | CANNKit | AI compute acceleration; API 24 adds PC LLM inference support |
| FAST Kit | FASTKit | Concurrent hash tables, vector operations, filters |
| Intents Kit | IntentsKit | Intent recognition (30+ domains, 60+ built-in intents) |
| MindSpore Lite | MindSporeLiteKit | Edge model inference (Caffe/TF/ONNX/MindIR) |
| Natural Language Kit | NaturalLanguageKit | Word segmentation, NER, keyword extraction |
Performance & DevTools
| Kit | Import key | Purpose |
|---|---|---|
| Performance Analysis Kit | PerformanceAnalysisKit | hilog, HiAppEvent, crash analysis |
picker.PhotoViewPicker is deprecated)import { photoAccessHelper } from '@kit.MediaLibraryKit';
const phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
const picker = new photoAccessHelper.PhotoViewPicker();
const result = await picker.select({
MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE,
maxSelectNumber: 9
});
const uris: string[] = result.photoUris;
import { picker } from '@kit.CoreFileKit';
// Pick document files (PDF, DOCX, etc.)
const documentPicker = new picker.DocumentViewPicker(context);
const result = await documentPicker.select({
maxSelectNumber: 5,
fileSuffixFilters: ['.pdf', '.docx', '.xlsx'], // optional filter
});
const uris: string[] = result; // file URIs with temporary read permission
// Save file to user-chosen location
const savePicker = new picker.DocumentViewPicker(context);
const saveResult = await savePicker.save({
newFileNames: ['export.pdf'],
});
const destUri: string = saveResult[0];
import { speechRecognizer } from '@kit.CoreSpeechKit';
const engine = await speechRecognizer.createEngine({
language: 'zh-CN',
online: 0 // 0=on-device, 1=cloud
});
// Audio MUST be: PCM, 16kHz, mono, 16-bit
// Chunk size: 640 or 1280 bytes, write every 20ms or 40ms
engine.startListening({
sessionId: `asr_${Date.now()}`,
audioInfo: {
audioType: 'pcm',
sampleRate: 16000,
soundChannel: 1,
sampleBit: 16
}
});
import { http } from '@kit.NetworkKit';
async function fetchJson(url: string): Promise<string> {
const req = http.createHttp();
try {
const res = await req.request(url, {
method: http.RequestMethod.GET,
header: { 'Content-Type': 'application/json' },
connectTimeout: 5000, readTimeout: 10000
});
return res.result as string;
} finally {
req.destroy();
}
}
import { connection } from '@kit.NetworkKit';
// Check current network state
const hasNet = connection.hasDefaultNetSync();
// Monitor network changes
const netCon = connection.createNetConnection();
netCon.on('netAvailable', () => { /* network restored */ });
netCon.on('netLost', () => { /* network lost */ });
netCon.on('netCapabilitiesChange', (info: connection.NetCapabilityInfo) => {
const isWifi = info.netCap.bearerTypes.includes(connection.NetBearType.BEARER_WIFI);
const isCellular = info.netCap.bearerTypes.includes(connection.NetBearType.BEARER_CELLULAR);
});
netCon.register(() => {}); // activate subscriptions
// Cleanup
netCon.unregister(() => {});
import { webSocket } from '@kit.NetworkKit';
const ws = webSocket.createWebSocket();
ws.on('open', () => { ws.send('hello'); });
ws.on('message', (err, data: string | ArrayBuffer) => {
console.info('received:', typeof data === 'string' ? data : 'binary');
});
ws.on('close', (err, { code, reason }) => { /* closed */ });
ws.on('error', (err) => { /* handle */ });
ws.connect('wss://example.com/ws', { header: { 'Authorization': 'Bearer xxx' } });
// Send
ws.send(JSON.stringify({ type: 'chat', text: 'hi' }));
// Close
ws.close();
import { common } from '@kit.AbilityKit';
const context = getContext(this) as common.UIAbilityContext;
// Open navigation app with destination
context.startAbilityByType('navigation', {
sceneType: 1,
destinationLatitude: 39.9042,
destinationLongitude: 116.4074,
destinationName: 'Beijing',
} as Record<string, Object>);
// Open browser
context.startAbilityByType('browser', {
uri: 'https://example.com',
} as Record<string, Object>);
Supported types: navigation, browser, email, finance, transit, etc.
Declare in module.json5 → requestPermissions. For user-granted permissions, request at runtime via abilityAccessCtrl.createAtManager().requestPermissionsFromUser(context, [...]).
Installation-free, small (≤10MB) apps launched from:
FormExtensionAbility)Configured via module.json5 with "installationFree": true. Build-time they produce .app packages containing multiple HAPs.
continueAbility / onContinue/onNewWant lifecycle| Type | Purpose |
|---|---|
| HAP (Harmony Ability Package) | Runnable module: entry or feature type |
| HSP (Harmony Shared Package) | In-app shared module, dynamic-linked at runtime |
| HAR (Harmony Archive) | Static library, packaged into HAP at build time |
Distribute via AppGallery Connect (华为应用市场).
Official samples: https://developer.huawei.com/consumer/cn/samples/
HarmonyOS Symbol is a 1500+ vector icon font with multi-layer color and 7 animation types.
SymbolGlyph($r('sys.symbol.bell_fill'))
.fontSize(24)
.fontColor([Color.Blue, Color.Green])
.symbolEffect(new BounceSymbolEffect(EffectScope.WHOLE), true)
Confirmed working names (SDK 6.0.1): xmark · plus · minus · checkmark · chevron_right · chevron_left · star · star_fill · bell · bell_fill · doc · video · mic · mic_fill · clock · trash · pencil · camera · person
Names that do NOT exist (common mistakes): photo · doc_richtext · sparkles · checklist · image (use doc or camera instead) · location_fill (use text label instead)
Find valid names: https://developer.huawei.com/consumer/cn/design/harmonyos-symbol
DevEco Studio 6.x requires additional Hvigor infrastructure files. Without them the IDE shows "工程结构及配置需要升级".
Current API 24 Release toolchain (HarmonyOS 6.1.1, 2026/05/26):
Ohos_sdk_public 6.1.1.125, API 24 Release)compileSdkVersion: 6.1.1(24)targetSdkVersion: 4.0.0(10) ~ 6.1.1(24)Recommended workflow: Always let DevEco Studio generate the scaffold (新建项目 → Empty Ability), then copy your source files in. This avoids stale Hvigor wrapper/plugin versions.
// root
import { appTasks } from '@ohos/hvigor-ohos-plugin';
export default { system: appTasks, plugins:[] }
// entry/hvigorfile.ts
import { hapTasks } from '@ohos/hvigor-ohos-plugin';
export default { system: hapTasks, plugins:[] }
{
// Use the exact version generated by DevEco Studio for your SDK.
// API 24 Release uses Hvigor 6.24.2.
"hvigorVersion": "6.24.2",
"dependencies": {
// Keep this aligned with the DevEco-generated scaffold.
"@ohos/hvigor-ohos-plugin": "<generated-by-deveco>"
}
}
SDK versions go in one place only — either under app OR inside each product, not both. Mixing them causes "Sync Failed".
API 24 DevEco Studio adds strictCheckerOnly under project-level build-profile.json5 strictMode for running only strict syntax checks on .ets files, reducing end-to-end compile time during checks:
{
"app": {
"compileSdkVersion": "6.1.1(24)",
"compatibleSdkVersion": "4.0.0(10)",
"targetSdkVersion": "6.1.1(24)",
"strictMode": {
"strictCheckerOnly": true
}
}
}
API 24 also adds project-level oh-package.json5 properties for multi-environment dependency management. Use it for environment-specific dependency values instead of duplicating package files.
// ❌
parseOutput(): { text: string; tags: string[] } { ... }
// ✅
interface ParsedOutput { text: string; tags: string[]; }
parseOutput(): ParsedOutput { ... }
any / unknownconst task = JSON.parse(rawStr) as AgentTask; // ✅ cast immediately
navDestination requires a @Builder function reference// ❌ inline lambda
.navDestination((name, param) => { if (name==='X') MyPage() })
// ✅ top-level @Builder
@Builder function PageRouter(name: string) { if (name==='X') MyPage() }
Navigation(stack) { ... }.navDestination(PageRouter)
@Entry build() root must be a containerWrap in Stack(), Column(), or Row() — a custom component alone is not a container.
display.width is pixels, not vpfunction isTablet(): boolean {
const dm = display.getDefaultDisplaySync();
return (dm.width / dm.densityPixels) >= 840;
}
reason + usedScene{
"name": "ohos.permission.READ_MEDIA",
"reason": "$string:permission_media_reason",
"usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
}
INTERNET is system_grant — no extra fields needed.
Don't mix FA and Stage models — FA is legacy; HarmonyOS NEXT only supports Stage.
this in ArkUI callbacks — arrow functions are required; regular function () {} loses this.
@State on nested objects — changes to nested props don't trigger updates; use @Observed/@ObjectLink or reassign the whole object.
Array item updates — replace the item (arr[i] = newItem) or use @Observed on the item class.
Resource references — use $r('app.string.foo'), $r('app.media.icon'), not string paths.
getContext(this) inside a component returns the UIAbilityContext; cast explicitly.
Async in build() is forbidden — load data in aboutToAppear() and store in @State.
Permissions must be declared AND requested at runtime for user-grant permissions.
ohpm is the package manager (similar to npm) — dependencies live in oh-package.json5.
Preview on device — DevEco Previewer doesn't fully simulate; always test on real HarmonyOS device or emulator.
Navigation has no hideSideBar — use .hideBackButton(true) instead.
promptAction.showToast() is deprecated — use getUIContext().getPromptAction().showToast(...) instead; wrap in try-catch for safety.
Floating FAB button blocks last list item — use Navigation.menus() for primary action buttons, or add bottom padding to List equal to FAB height + margin.
Named callbacks for on/off — anonymous functions can't be unregistered. Always store references:
// BAD: can't off() an anonymous function
session.on('stateChange', (state) => { ... });
// GOOD: named reference
const cb = (state: StateType) => { ... };
session.on('stateChange', cb);
session.off('stateChange', cb);
Batch state mutations — multiple @State changes trigger multiple re-renders. Accumulate in a temp variable, assign once:
// BAD: 3 re-renders
this.list.push(a); this.list.push(b); this.list.push(c);
// GOOD: 1 re-render
const tmp = [...this.list, a, b, c];
this.list = tmp;
State decorator selection priority — @State+@Prop/@Link/@ObjectLink (parent-child) > @Provide+@Consume (deep nesting) > LocalStorage (page-level) > AppStorage (global). Avoid AppStorage for frequently-changing data.
Unit conversion — px2vp(px) / vp2px(vp) via this.getUIContext().px2vp(value). Screen density: display.getDefaultDisplaySync().densityPixels.
Keep screen on — win.setWindowKeepScreenOn(true) during video playback / navigation; reset on pause.
| Type | Description |
|---|---|
| JS_ERROR | ArkTS/JS runtime exceptions (most common) — TypeError: Cannot read property 'x' of undefined |
| CPP_CRASH | Native C/C++ crash (SIGSEGV, SIGABRT) |
| APP_FREEZE | Main thread blocked >6s (ANR equivalent). Root causes: thread locks (57%), system resources (14%), heavy main-thread work (9%) |
| OOM | Out-of-memory kill |
Global error handler:
import { errorManager } from '@kit.AbilityKit';
const observer: errorManager.ErrorObserver = {
onUnhandledException(errMsg: string): void {
console.error('Uncaught: ' + errMsg);
},
onException?(errObject: Error): void { // API 10+
console.error(errObject.name + ': ' + errObject.message);
}
};
const observerId = errorManager.on('error', observer);
Crash event subscription (HiAppEvent):
import { hiAppEvent } from '@kit.PerformanceAnalysisKit';
hiAppEvent.addWatcher({
name: "crashWatcher",
appEventFilters: [{
domain: hiAppEvent.domain.OS,
names: [hiAppEvent.event.APP_CRASH]
}],
onReceive: (domain, appEventGroups) => { /* process crash */ }
});
| Type | API | Duration | Use case |
|---|---|---|---|
| Transient | requestSuspendDelay | ~3 min max | Save data, upload logs |
| Continuous | startBackgroundRunning | Unlimited (needs notification) | Music, navigation, recording |
| Deferred | workScheduler.startWork | System-determined | Timed sync, cleanup |
| Agent Reminders | ReminderRequestTimer | System-managed | Alarms, timers |
Continuous task example:
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { wantAgent, WantAgent } from '@kit.AbilityKit';
// module.json5: "abilities": [{ "backgroundModes": ["audioRecording"] }]
// permission: ohos.permission.KEEP_BACKGROUND_RUNNING
const info: wantAgent.WantAgentInfo = {
wants: [{ bundleName: 'com.example.app', abilityName: 'MainAbility' }],
actionType: wantAgent.OperationType.START_ABILITY,
requestCode: 0, actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
};
const agent = await wantAgent.getWantAgent(info);
backgroundTaskManager.startBackgroundRunning(
this.context, backgroundTaskManager.BackgroundMode.AUDIO_RECORDING, agent
);
9 background modes: dataTransfer · audioPlayback · audioRecording · location · bluetoothInteraction · multiDeviceConnection · taskKeeping (2-in-1 only)
Deferred task frequency by user activity: Active=2h, Frequent=4h, Regular=24h, Rare=48h, Never used=prohibited.
exported: false for non-interactive abilitiesAsset Store Kit for sensitive short data (passwords, tokens)InputType (.USER_NAME, .Password) for system-level protectionPermission check + request pattern:
import { abilityAccessCtrl, bundleManager } from '@kit.AbilityKit';
// Check
const atManager = abilityAccessCtrl.createAtManager();
const bundleInfo = await bundleManager.getBundleInfoForSelf(
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
const status = await atManager.checkAccessToken(
bundleInfo.appInfo.accessTokenId, 'ohos.permission.CAMERA');
// Step 1 — Request from user
const result = await atManager.requestPermissionsFromUser(context,
['ohos.permission.CAMERA', 'ohos.permission.MICROPHONE']);
if (result.authResults[0] === 0) {
// Granted
} else if (result.dialogShownResults?.[0]) {
// User saw dialog but denied — show in-app guidance, don't re-pop
} else {
// Step 2 — Fallback: open settings dialog (user previously denied permanently)
atManager.requestPermissionOnSetting(context,
['ohos.permission.CAMERA']).then((statuses) => {
// statuses[0]: 0 = granted, -1 = denied
});
}
Data encryption levels: EL1 (device-level) → EL2 (user-level, default) → EL3 (accessible while locked) → EL4 (inaccessible when locked)
Network security config — HTTPS/certificate pinning for production apps:
Create src/main/resources/base/profile/network_config.json:
{
"network-security-config": {
"domain-config": [{
"domains": [{ "include-subdomains": true, "name": "api.example.com" }],
"trust-anchors": [{ "certificates": "/data/storage/el1/bundle/entry/resources/resfile/ca_cert.pem" }]
}]
}
}
Reference in module.json5: "metadata": [{ "name": "NetworkSecurityConfig", "resource": "$profile:network_config" }].
Package: @ohos/hypium (Mocha-style). Three sub-frameworks: JsUnit (unit), UiTest (UI automation), PerfTest (performance).
import { describe, it, expect, beforeAll, beforeEach, afterEach, afterAll } from '@ohos/hypium';
export default function abilityTest() {
describe('MyTestSuite', () => {
beforeAll(() => { /* once before all */ });
beforeEach(() => { /* before each */ });
afterEach(() => { /* after each */ });
afterAll(() => { /* once after all */ });
it('sync_test', 0, () => {
expect(1 + 1).assertEqual(2);
});
it('async_test', 0, async (done: Function) => {
let result = await someAsyncOp();
expect(result).assertContain('expected');
done();
});
});
}
Key assertions: assertEqual(v) · assertContain(v) · assertTrue() · assertFalse() · assertNull() · assertUndefined() · assertNaN() · assertInstanceOf(type) · assertThrowError(fn) · assertDeepEquals(v) · assertClose(v, tolerance) · assertLarger(v) · assertLess(v) · not() (negation) · assertPromiseIsResolved() · assertPromiseIsRejected()
Test files in entry/src/ohosTest/ets/test/. For UI automation, see the UiTest section below.
仓颉 (Cangjie) is Huawei's new language (beta) — use ArkTS for all production apps until Cangjie is stable.
| Feature | TaskPool | Worker |
|---|---|---|
| Thread management | Automatic (create/reuse/destroy) | Manual lifecycle |
| Max threads | Auto-scaled to physical cores | Max 64 per process |
| Task duration limit | 3 minutes (excluding async I/O) | Unlimited |
| Priority | HIGH / MEDIUM / LOW / IDLE | API 18+ only |
| Cancellation | Supported | Not supported |
| Thread reuse | Yes | No |
| Task groups | Yes | No |
| Delayed/periodic | Yes | No |
Use Worker when: tasks exceed 3 minutes, need persistent state, or strongly associated synchronous tasks.
.ets filesimport { taskpool } from '@kit.ArkTS';
@Concurrent
function heavyCalc(n: number): number { return n * n; }
const result = await taskpool.execute(heavyCalc, 42);
// With priority
const task = new taskpool.Task(heavyCalc, 42);
await taskpool.execute(task, taskpool.Priority.HIGH);
// Delayed and periodic
taskpool.executeDelayed(heavyCalc, 2000, 42); // after 2s
taskpool.executePeriodically(heavyCalc, 5000, 42); // every 5s
// TaskGroup — execute multiple tasks as a group
const group = new taskpool.TaskGroup();
group.addTask(heavyCalc, 10);
group.addTask(heavyCalc, 20);
group.addTask(heavyCalc, 30);
const results = await taskpool.execute(group); // returns array of results
Long-time tasks: async code (Promise/IO) in TaskPool has NO time limit (only CPU-bound sync code is capped at 3 minutes). HarmonyOS 6.0 officially supports long-running async tasks in TaskPool.
Objects on SharedHeap (process-level, all threads can access) — 100x faster than serialization for 1MB data.
@Sendable
class SharedData {
value: number = 0; // must be explicitly initialized
}
Constraints: Can only inherit from Sendable classes. Property types limited to: primitives, Sendable classes, @arkts.collections containers. Cannot add/delete properties at runtime. No computed properties. No # private (use private keyword).
// Main thread
const worker = new worker.ThreadWorker('entry/ets/workers/myWorker.ets');
worker.postMessage({ type: 'compute', data: payload });
worker.onmessage = (e) => { /* handle result */ };
worker.terminate();
// Worker thread (myWorker.ets)
const workerPort = worker.workerPort;
workerPort.onmessage = (e) => {
const result = processData(e.data);
workerPort.postMessage(result);
};
Both TaskPool and Worker: Cannot access AppStorage or UI libraries from worker threads. Different thread contexts prevent context object sharing.
ohpm install @ohos/axios # install a library
ohpm install # install all from oh-package.json5
oh-package.json5 example:
{
"name": "entry",
"dependencies": {
"@ohos/axios": "^2.0.0"
}
}
OHPM Central Repository: https://developer.huawei.com/consumer/cn/deveco-service
@ohos/axios — HTTP client (ohpm install @ohos/axios, requires ohos.permission.INTERNET):
import axios, { AxiosResponse, AxiosError, InternalAxiosRequestConfig } from '@ohos/axios';
// GET (generic response type)
interface UserInfo { id: number; name: string }
axios.get<UserInfo, AxiosResponse<UserInfo>, null>('/user', { params: { ID: 12345 } })
.then((res: AxiosResponse<UserInfo>) => console.info(res.data.name))
.catch((err: AxiosError) => console.error(err.message));
// POST
interface NewUser { firstName: string; lastName: string }
axios.post<string, AxiosResponse<string>, NewUser>('/user',
{ firstName: 'Fred', lastName: 'Flintstone' });
// Create instance (recommended for project-wide use)
const http = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: { 'X-Custom-Header': 'value' },
});
// Interceptors
http.interceptors.request.use((config: InternalAxiosRequestConfig) => {
config.headers['Authorization'] = 'Bearer ' + AppStorage.get<string>('token');
return config;
}, (err: AxiosError) => Promise.reject(err));
http.interceptors.response.use(
(res: AxiosResponse) => res,
(err: AxiosError) => {
if (err.response?.status === 401) { /* redirect to login */ }
return Promise.reject(err);
}
);
// File download
import fs from '@ohos.file.fs';
const filePath = getContext(this).cacheDir + '/file.jpg';
axios({ url: 'https://example.com/file.jpg', method: 'get', filePath,
onDownloadProgress: (e) => {
console.info('Progress: ' + (e.loaded && e.total ? Math.ceil(e.loaded / e.total * 100) : 0) + '%');
}
});
// File upload (FormData)
import { FormData } from '@ohos/axios';
const form = new FormData();
form.append('file', 'internal://cache/photo.jpg');
axios.post<string, AxiosResponse<string>, FormData>('/upload', form, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (e) => console.info(e.loaded + '/' + e.total),
});
// Cancel request
const ctrl = new AbortController();
axios.get('/slow', { signal: ctrl.signal });
ctrl.abort();
responseType:'string'|'object'|'array_buffer'. Mutual TLS requires API 11+.
@ohos/pulltorefresh — pull-down refresh + load-more (ohpm install @ohos/pulltorefresh, supports List/Scroll/Tabs/Grid/WaterFlow, API 12+):
import { PullToRefresh, PullToRefreshType } from '@ohos/pulltorefresh';
@Component
struct NewsPage {
@State data: string[] = ['Item 1', 'Item 2', 'Item 3'];
@State refreshing: boolean = false;
build() {
PullToRefresh({
pullToRefreshType: PullToRefreshType.LIST,
refreshing: $refreshing,
customList: () => { this.ListBuilder(); },
onRefresh: () => new Promise<string>((resolve) => {
setTimeout(() => {
this.data = ['Refreshed 1', 'Refreshed 2'];
resolve('Done');
}, 1000);
}),
onLoadMore: () => new Promise<string>((resolve) => {
setTimeout(() => {
for (let i = 0; i < 10; i++) this.data.push('New ' + this.data.length);
resolve('');
}, 1000);
}),
customLoad: null,
customRefresh: null,
})
}
@Builder ListBuilder() {
List() {
ForEach(this.data, (item: string) => {
ListItem() { Text(item).padding(16) }
})
}
}
}
@ohos/lottie — JSON animation (ohpm install @ohos/lottie, also consider @ohos/lottie-turbo for 30%+ perf):
import lottie, { AnimationItem } from '@ohos/lottie';
@Component
struct LottieDemo {
private canvasCtx = new CanvasRenderingContext2D(new RenderingContextSettings(true));
private animItem: AnimationItem | null = null;
private animName: string = 'myAnim';
aboutToDisappear(): void { lottie.destroy(); }
build() {
Canvas(this.canvasCtx).width(200).height(200)
.onReady(() => {
this.canvasCtx.imageSmoothingEnabled = true;
lottie.destroy(this.animName);
this.animItem = lottie.loadAnimation({
container: this.canvasCtx, renderer: 'canvas',
loop: true, autoplay: false, name: this.animName,
contentMode: 'Contain',
path: 'common/animation.json', // relative to ets parent folder
frameRate: 30,
});
this.animItem.addEventListener('DOMLoaded', () => {
this.animItem?.play();
});
})
}
}
// Controls: .play() .pause() .stop() .goToAndPlay(frame, true) .setSpeed(2)
Obfuscation: add
-keep ./oh_modules/@ohos/lottieto obfuscation-rules.txt
@ohos/imageknife — image loading with cache (ohpm install @ohos/imageknife, API 18+; successor: imageknifepro):
import { ImageKnifeComponent, ImageKnifeAnimatorComponent } from '@ohos/imageknife';
import { ImageKnife } from '@ohos/imageknife';
// Init file cache once (in EntryAbility.onCreate)
await ImageKnife.getInstance().initFileCache(context, 256, 256 * 1024 * 1024);
// Basic usage
ImageKnifeComponent({
imageKnifeOption: {
loadSrc: 'https://example.com/image.png',
placeholderSrc: $r('app.media.loading'),
errorholderSrc: $r('app.media.error'),
objectFit: ImageFit.Auto,
border: { radius: 50 }, // circular crop
onLoadListener: {
onLoadSuccess: (data) => { console.info('Loaded'); return data; },
onLoadFailed: (err) => console.error('Failed: ' + err),
},
progressListener: (progress: number) => console.info('Progress: ' + progress),
}
}).width(100).height(100)
// GIF / animated WebP
ImageKnifeAnimatorComponent({
imageKnifeOption: { loadSrc: 'https://example.com/anim.gif' }
}).width(300).height(300)
Obfuscation: add
-keep ./oh_modules/@ohos/imageknifeto obfuscation-rules.txt
dayjs — date formatting (ohpm install dayjs, standard npm API):
import dayjs from 'dayjs';
dayjs().format('YYYY-MM-DD HH:mm:ss');
dayjs('2024-01-01').fromNow(); // requires relativeTime plugin
dayjs().add(7, 'day').format('YYYY-MM-DD');
dayjs().diff(dayjs('2024-01-01'), 'day'); // days difference
hdc shell, hdc file send, hdc hiloghilog.info(0x0001, 'TAG', 'message %{public}s', arg)build-profile.json5hvigorw assembleApp430+ open-source HarmonyOS samples at https://gitcode.com/HarmonyOS_Samples/<name>. Key projects by category:
Full catalog:
https://gitcode.com/HarmonyOS_Samples— clone any sample withgit clone https://gitcode.com/HarmonyOS_Samples/<name>.git
These errors were verified against a real Mate 70 Pro build. All entries below caused ArkTS Compiler Error at assembleDevHqf.
DataChangeListener requires both new and deprecated method namesIn API 21, any class that implements DataChangeListener must include ALL of:
// New names (current)
onDataReloaded(): void
onDataAdd(index: number): void
onDataDelete(index: number): void
onDataChange(index: number): void
onDataMove(from: number, to: number): void
// Deprecated aliases — still required by the interface in API 21
onDataAdded(index: number): void
onDataDeleted(index: number): void
onDataChanged(index: number): void
onDataMoved(from: number, to: number): void
onDatasetChange(dataOperations: DataOperation[]): void
This applies to every class including test stubs (NoopListener, etc).
notificationManager.addSlot() — pass SlotType, not NotificationSlot// ❌ API 9 style
await notificationManager.addSlot({ type: SlotType.SOCIAL_COMMUNICATION, desc: '...' })
// ✅ API 12+ style
await notificationManager.addSlot(notificationManager.SlotType.SOCIAL_COMMUNICATION)
Also: always annotate the constant explicitly to prevent type narrowing to literal type:
// ❌ infers as SlotType.SOCIAL_COMMUNICATION — not assignable to SlotType
const SLOT_ID = notificationManager.SlotType.SOCIAL_COMMUNICATION
// ✅
const SLOT_ID: notificationManager.SlotType = notificationManager.SlotType.SOCIAL_COMMUNICATION
NotificationRequest.slotType — incompatible SlotType modulesNotificationRequest.slotType is typed as @ohos.notification.SlotType (old module), while notificationManager.SlotType is from @ohos.notificationManager (new module). They are nominally different types — assigning new to old is a compile error. Just omit slotType (it is optional):
const request: notificationManager.NotificationRequest = {
id: NOTIFICATION_ID,
// slotType omitted — routes to the slot created by addSlot() automatically
content: { ... }
}
NotificationSlotLevel does not exist on notificationManager in API 21 — remove any usage.
Permissions type — @ohos.bundleManager not importableimport type { Permissions } from '@ohos.bundleManager' fails with "Cannot find module". bundleManager is accessible only via @kit.AbilityKit. Workaround: declare a local subset type whose values are all valid Permissions literals:
// ✅ subtype of Permissions — assignable to the SDK's Permissions parameter
type AppPermission = 'ohos.permission.CAMERA' | 'ohos.permission.APPROXIMATELY_LOCATION'
async function hasPermission(name: AppPermission): Promise<boolean> {
const atManager = abilityAccessCtrl.createAtManager()
const bundleInfo = await bundleManager.getBundleInfoForSelf(
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION)
const status = atManager.checkAccessTokenSync(bundleInfo.appInfo.accessTokenId, name)
return status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
}
requestPermissionsFromUser — void & Promise overload intersectionThe function has both a callback overload (returns void) and a Promise overload (returns Promise<PermissionRequestResult>). TypeScript sometimes intersects them to void & Promise<…>, causing .authResults to not exist. Additionally, abilityAccessCtrl.PermissionRequestResult is not exported from the namespace in API 21.
Fix: declare the result interface locally and cast:
interface PermResult {
authResults: abilityAccessCtrl.GrantStatus[]
}
async function requestPermission(ctx: common.UIAbilityContext, name: AppPermission): Promise<boolean> {
const atManager = abilityAccessCtrl.createAtManager()
const result = await (atManager.requestPermissionsFromUser(ctx, [name]) as Promise<PermResult>)
return result.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
}
getContext(this) is deprecatedReplace in all component methods:
// ❌
const ctx = getContext(this) as common.UIAbilityContext
// ✅
const ctx = this.getUIContext().getHostContext() as common.UIAbilityContext
Module-level (non-component) functions must receive ctx as a parameter instead.
import { display } from '@kit.ArkUI'
export const BP_SM = 'sm' // < 600 vp — phone portrait
export const BP_MD = 'md' // < 840 vp — phone landscape / small tablet
export const BP_LG = 'lg' // >= 840 vp — large tablet / foldable unfolded
export function getBreakpoint(): string {
try {
const d = display.getDefaultDisplaySync()
const widthVp = d.width / d.densityPixels
if (widthVp < 600) return BP_SM
if (widthVp < 840) return BP_MD
return BP_LG
} catch (e) {
return BP_SM
}
}
// display.on callback receives (id: number) — NOT empty params
private displayListener: (id: number) => void = (_id: number) => {
this.breakpoint = getBreakpoint()
}
aboutToAppear(): void {
this.breakpoint = getBreakpoint()
display.on('change', this.displayListener)
}
aboutToDisappear(): void {
display.off('change', this.displayListener)
}
Always use if/else to switch between phone and tablet layouts. .visibility(Visibility.None) hides but still lays out — wastes resources and can cause measurement bugs.
if (this.breakpoint === BP_SM) {
// Phone: single-column List + LazyForEach
List({ space: 8 }) { LazyForEach(...) }
} else {
// Tablet/foldable: GridRow with responsive columns
Scroll() {
GridRow({
columns: { sm: 1, md: 2, lg: 3 },
gutter: { x: 12, y: 12 },
breakpoints: { value: ['600vp', '840vp'] }
}) {
ForEach(this.gridItems, (item) => { GridCol() { MyCard({ record: item }) } })
}
}
}
GridRow/GridCol does not support LazyForEachGridRow/GridCol is a responsive layout container, not a lazy loader. Use ForEach inside it. To keep the grid reactive to data changes, maintain a @State gridItems: T[] that is updated via a DataChangeListener:
class GridRefreshListener implements DataChangeListener {
private updateFn: () => void
constructor(fn: () => void) { this.updateFn = fn }
// Must implement ALL methods — see API 21 DataChangeListener note above
onDataReloaded(): void { this.updateFn() }
onDataAdd(_i: number): void { this.updateFn() }
onDataAdded(_i: number): void { this.updateFn() }
// ... all other methods
onDataChange(_i: number): void { /* @ObjectLink handles per-item updates */ }
onDataChanged(_i: number): void { }
}
// In @Entry @Component:
@State gridItems: MyRecord[] = []
private gridListener = new GridRefreshListener(() => {
this.gridItems = [...this.dataSource.getAllData()]
})
aboutToAppear() { this.dataSource.registerDataChangeListener(this.gridListener) }
aboutToDisappear() { this.dataSource.unregisterDataChangeListener(this.gridListener) }
@Provide/@ConsumeDeclare in the @Entry root, consume in any NavDestination child:
// Root
@Provide('breakpoint') breakpoint: string = BP_SM
// Detail page (NavDestination)
@Consume('breakpoint') breakpoint: string
@Provide/@Consume works across Navigation/NavDestination because they are in the same component subtree.
import { geoLocationManager } from '@kit.LocationKit'
// Permission required: ohos.permission.APPROXIMATELY_LOCATION (user_grant)
// module.json5: reason + usedScene required
const pos = await geoLocationManager.getCurrentLocation({
priority: geoLocationManager.LocationRequestPriority.FIRST_FIX,
scenario: geoLocationManager.LocationRequestScenario.UNSET,
timeoutMs: 10000
})
const addresses = await geoLocationManager.getAddressesFromLocation({
latitude: pos.latitude,
longitude: pos.longitude,
maxItems: 1
})
// Build readable location string from GeoAddress fields:
// administrativeArea (省) → subAdministrativeArea (市) → locality (区) → subLocality → placeName
Requires
ohos.permission.LOCATION(orAPPROXIMATELY_LOCATION) if using device location.
import { weatherService } from '@kit.WeatherServiceKit';
const weatherRequest: weatherService.WeatherRequest = {
location: { latitude: 39.9042, longitude: 116.4074 }, // Beijing
limitedDatasets: [
weatherService.Dataset.CURRENT, // current conditions
weatherService.Dataset.DAILY, // daily forecast
weatherService.Dataset.HOURLY, // hourly forecast
weatherService.Dataset.ALERTS, // severe weather alerts
weatherService.Dataset.INDICES, // life indices (UV, air quality...)
weatherService.Dataset.TIDES, // coastal tides
weatherService.Dataset.MINUTE, // minute-level precipitation
],
};
try {
const weather = await weatherService.getWeather(weatherRequest);
// weather.currentWeather.temperature, .humidity, .conditionCode, ...
// weather.dailyForecast?.days[] (each: tempMax/tempMin/precipitation/sunrise/sunset)
// weather.hourlyForecast?.hours[]
// weather.weatherAlerts?.alerts[] (severity, summary)
} catch (err) {
console.error('Weather fetch failed:', err);
}
import { notificationManager } from '@kit.NotificationKit'
// 1. Create slot (call once at app init)
await notificationManager.addSlot(notificationManager.SlotType.SOCIAL_COMMUNICATION)
// 2. Check and request notification enable
const enabled = await notificationManager.isNotificationEnabled()
if (!enabled) await notificationManager.requestEnableNotification() // deprecated but functional
// 3. Publish
const request: notificationManager.NotificationRequest = {
id: 1001,
// OMIT slotType — see API 21 type mismatch note above
content: {
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: { title: '标题', text: '正文', additionalText: '副文本' }
},
deliveryTime: Date.now(),
showDeliveryTime: true,
tapDismissed: true
}
await notificationManager.publish(request)
// 4. Cancel
await notificationManager.cancel(1001)
import { relationalStore } from '@kit.ArkData'
import { common } from '@kit.AbilityKit'
// Init (call in aboutToAppear / UIAbility.onCreate)
const store = await relationalStore.getRdbStore(ctx, {
name: 'my_db.db',
securityLevel: relationalStore.SecurityLevel.S1
})
await store.executeSql('CREATE TABLE IF NOT EXISTS records (...)')
// Insert
const values: relationalStore.ValuesBucket = { id: '1', title: 'Hello' }
await store.insert('records', values)
// Query (ordered)
const predicates = new relationalStore.RdbPredicates('records')
predicates.orderByDesc('timestamp')
const rs = await store.query(predicates, ['id', 'title', 'timestamp'])
while (rs.goToNextRow()) {
const id = rs.getString(rs.getColumnIndex('id'))
}
rs.close()
// Update
const predicates2 = new relationalStore.RdbPredicates('records')
predicates2.equalTo('id', '1')
await store.update({ title: 'Updated' }, predicates2)
// Delete
const predicates3 = new relationalStore.RdbPredicates('records')
predicates3.equalTo('id', '1')
await store.delete(predicates3)
import { preferences } from '@kit.ArkData'
const store = await preferences.getPreferences(ctx, 'user_settings')
// Read (with default)
const val = (await store.get('sort_order', 'time_desc')) as string
// Write + flush
await store.put('sort_order', 'time_asc')
await store.flush()
import { Driver, ON, Component } from '@ohos.UiTest'
import AbilityDelegatorRegistry from '@ohos.app.ability.abilityDelegatorRegistry'
const DELEGATOR = AbilityDelegatorRegistry.getAbilityDelegator()
// Launch app before tests
beforeAll(async (done: Function) => {
await DELEGATOR.startAbility({ bundleName: 'com.example.app', abilityName: 'EntryAbility' })
await new Promise(r => setTimeout(r, 2000)) // wait for UI to render
done()
})
it('example', 0, async (done: Function) => {
const driver = Driver.create()
// Find by text / type / content description
const btn = await driver.findComponent(ON.text('发布'))
const input = await driver.findComponent(ON.type('TextInput'))
const items = await driver.findComponents(ON.type('SymbolGlyph'))
// Actions — ALL must be awaited
await btn.click()
await input.inputText('hello')
// Wait after state-changing actions
await new Promise(r => setTimeout(r, 800))
// Component refs become stale after state changes — re-find
const btnAfter = await driver.findComponent(ON.text('发布'))
// Get bounds for position-based selection
const bounds = await btn.getBounds() // { top, left, right, bottom }
// Navigate back
await driver.pressBack()
done()
})
Key rules:
it() must be awaitedfindComponentDriver.create() fresh per it block (don't share across tests)sleep / setTimeout after navigation to let the new page render before assertingEmbed web content in ArkTS via the Web component from @kit.ArkWeb.
import { webview } from '@kit.ArkWeb';
@Entry
@Component
struct BrowserPage {
controller: webview.WebviewController = new webview.WebviewController();
build() {
Column() {
Web({ src: 'https://example.com', controller: this.controller })
.javaScriptAccess(true)
.domStorageAccess(true)
.fileAccess(true)
.onPageBegin((event) => { console.log('loading:', event?.url); })
.onPageEnd((event) => { console.log('loaded:', event?.url); })
.onErrorReceive((event) => { console.error('web error:', event?.error.getErrorInfo()); })
.darkMode(WebDarkMode.Auto) // follow system dark mode
.forceDarkAccess(true)
}
}
}
javaScriptProxy)// Register ArkTS object callable from JS
Web({ src: 'https://example.com', controller: this.controller })
.javaScriptProxy({
object: {
callNative: (msg: string) => {
console.log('from JS:', msg);
return 'ArkTS received: ' + msg;
}
},
name: 'NativeBridge',
methodList: ['callNative'],
controller: this.controller
})
// In the web page: window.NativeBridge.callNative('hello');
// Call JS from ArkTS
this.controller.runJavaScript('window.updateUI("data")', (err, result) => {
console.log('JS result:', result);
});
// Append to default UA
webview.WebviewController.setCustomUserAgent(
webview.WebviewController.getDefaultUserAgent() + ' MyApp/1.0'
);
// Manage cookies
import { webCookie } from '@kit.ArkWeb';
webCookie.setCookie('https://example.com', 'token=abc; path=/');
webCookie.saveCookieAsync(); // persist to disk
Web({ src: '...', controller: this.controller })
.onInterceptRequest((event) => {
if (event?.request.getRequestUrl().includes('/api/')) {
// Return custom response
const resp = new WebResourceResponse();
resp.setResponseData('{"intercepted":true}');
resp.setResponseMimeType('application/json');
resp.setResponseEncoding('utf-8');
resp.setResponseCode(200);
resp.setReasonMessage('OK');
return resp;
}
return null; // null = load normally
})
Service cards run in a sandboxed FormExtensionAbility process; they use a subset of ArkUI.
// entry/src/main/ets/formextensionability/EntryFormAbility.ets
import { FormExtensionAbility, formBindingData, FormInfo, formProvider } from '@kit.FormKit';
import { Want } from '@kit.AbilityKit';
export default class EntryFormAbility extends FormExtensionAbility {
onAddForm(want: Want) {
// Called when user adds card to home screen
const formData = { title: 'Hello', count: 0 };
return formBindingData.createFormBindingData(formData);
}
onCastToNormalForm(formId: string) { }
onUpdateForm(formId: string) {
// Periodic/requested update
const data = formBindingData.createFormBindingData({ count: Date.now() });
formProvider.updateForm(formId, data);
}
onRemoveForm(formId: string) { }
onFormEvent(formId: string, message: string) {
// Triggered by postCardAction in the card UI
console.log('card event:', formId, message);
}
}
EntryFormAbility/pages/Card.ets)// Cards are ArkUI components but with restricted APIs (no @State mutation from events)
// Use postCardAction to route events back to FormExtensionAbility
@Entry
@Component
struct CardPage {
@LocalStorageProp('title') title: string = '';
@LocalStorageProp('count') count: number = 0;
build() {
Column({ space: 8 }) {
Text(this.title).fontSize(16).fontWeight(FontWeight.Bold)
Text(`Count: ${this.count}`).fontSize(14)
Button('+1')
.onClick(() => {
postCardAction(this, {
action: 'message',
params: { event: 'increment' }
});
})
}.padding(12)
}
}
module.json5 — declare the form{
"extensionAbilities": [{
"name": "EntryFormAbility",
"srcEntry": "./ets/formextensionability/EntryFormAbility.ets",
"type": "form",
"metadata": [{
"name": "ohos.extension.form",
"resource": "$profile:form_config"
}]
}]
}
resources/base/profile/form_config.json{
"forms": [{
"name": "widget",
"displayName": "$string:widget_display_name",
"description": "$string:widget_desc",
"src": "./ets/formextensionability/pages/Card.ets",
"uiSyntax": "arkts",
"window": { "designWidth": 720, "autoDesignWidth": true },
"colorMode": "auto",
"isDynamic": true,
"updateEnabled": true,
"scheduledUpdateTime": "10:30",
"updateDuration": 1,
"defaultDimension": "2*2",
"supportDimensions": ["1*2", "2*2", "2*4"]
}]
}
Available from HarmonyOS 5.0+ (API 12+). Provides Huawei Design System components.
hdsDrawable — icon adaptive processingimport { hdsDrawable } from '@kit.UIDesignKit';
// Render app icon with system-consistent adaptive shape (squircle, circle, etc.)
const drawable = new hdsDrawable.HdsAdaptiveIconDrawable(
context, // UIAbilityContext or ApplicationContext
iconResource, // Resource ($r('app.media.icon'))
{ size: 48 } // options: size in vp
);
const pixelMap = await drawable.getPixelMap();
HdsNavigation — system-style navigation componentimport { HdsNavigation, HdsNavigationItem } from '@kit.UIDesignKit';
@Entry
@Component
struct MainPage {
@State currentIndex: number = 0;
private tabs: HdsNavigationItem[] = [
{ icon: $r('app.media.home'), selectedIcon: $r('app.media.home_filled'), label: '首页' },
{ icon: $r('app.media.mine'), selectedIcon: $r('app.media.mine_filled'), label: '我的' },
];
build() {
Column() {
// Page content
Blank()
HdsNavigation({
items: this.tabs,
selectedIndex: this.currentIndex,
onItemClick: (index: number) => { this.currentIndex = index; }
})
}.height('100%')
}
}
HdsNavigation supports: dynamic blur background, custom content areas (badges, dot indicators), message count badges on items.
import { mapCommon, map } from '@kit.MapKit';
import { AsyncCallback } from '@kit.BasicServicesKit';
@Entry
@Component
struct MapPage {
private mapOptions: mapCommon.MapOptions = {
position: {
target: { latitude: 39.9042, longitude: 116.4074 }, // Beijing
zoom: 12
}
};
private mapController?: map.MapComponentController;
build() {
Column() {
MapComponent({
mapOptions: this.mapOptions,
mapCallback: (err, controller) => {
if (!err) {
this.mapController = controller;
this.addMarker();
}
}
}).width('100%').layoutWeight(1)
}
}
private addMarker() {
this.mapController?.addMarker({
position: { latitude: 39.9042, longitude: 116.4074 },
title: 'Tiananmen',
snippet: 'Beijing city center'
});
}
}
Required permissions in module.json5: ohos.permission.LOCATION and ohos.permission.APPROXIMATELY_LOCATION.
HarmonyOS measures cold start as: app launch → first frame rendered. Target: < 1000ms on mid-range device.
import())// ❌ Eager — all modules parsed at startup even if unused
import { HeavyModule } from '../utils/HeavyModule';
// ✓ Lazy — parsed only when first used
let heavyModule: typeof import('../utils/HeavyModule') | null = null;
async function useHeavy() {
if (!heavyModule) {
heavyModule = await import('../utils/HeavyModule');
}
heavyModule.HeavyModule.doWork();
}
// EntryAbility.ets
onWindowStageCreate(windowStage: window.WindowStage) {
windowStage.loadContent('pages/Index', (err) => {
if (!err) {
// First frame committed — now safe to start network
AppStartupData.prefetch();
}
});
}
onCreate() / onWindowStageCreate() — use TaskPool for >10ms tasks@Reusable on list item components to avoid remeasure/relayout on first displayhilog calls inside tight rendering loops (I/O cost)onMemoryLevel callback// AbilityStage.ets or UIAbility.ets
onMemoryLevel(level: AbilityConstant.MemoryLevel): void {
if (level === AbilityConstant.MemoryLevel.MEMORY_LEVEL_CRITICAL) {
// Release non-essential caches immediately
ImageCache.instance.clear();
DataCache.instance.trimToSize(10);
} else if (level === AbilityConstant.MemoryLevel.MEMORY_LEVEL_LOW) {
DataCache.instance.trimToSize(50);
}
}
import { util } from '@kit.ArkTS';
class ImageCache {
static instance = new ImageCache();
private lru = new util.LRUCache<string, PixelMap>(50); // max 50 entries
put(key: string, pm: PixelMap) { this.lru.put(key, pm); }
get(key: string): PixelMap | undefined { return this.lru.get(key); }
clear() { this.lru.clear(); }
trimToSize(n: number) {
while (this.lru.length > n) {
this.lru.afterRemoval(false, this.lru.keys()[0], undefined, undefined);
}
}
}
import { image } from '@kit.ImageKit';
// Create PixelMap as purgeable — OS can reclaim when memory is tight,
// and will regenerate it from the source on next access
const pixelMap = await image.createPixelMap(buffer, {
size: { width: 1920, height: 1080 },
editable: false
});
// No extra API needed — PixelMap is automatically purgeable when editable=false
// and created from a decodable source (file path or buffer)
aboutToDisappear() / onBackground() to prevent leaksthis in long-lived closures (keeps component alive)image.ImageSource + lazy decode for thumbnails — don't decode full resolution@Sendable objects avoid copy but transfer ownershipSimplest way to capture a photo or video — launches the system camera UI. No camera permission required.
import { camera, cameraPicker as picker } from '@kit.CameraKit';
import { fileIo, fileUri } from '@kit.CoreFileKit';
// Create a temp file to receive the capture result
const pathDir = getContext(this).filesDir;
const filePath = pathDir + `/${Date.now()}.tmp`;
fileIo.createRandomAccessFileSync(filePath, fileIo.OpenMode.CREATE);
const pickerProfile: picker.PickerProfile = {
cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK,
saveUri: fileUri.getUriFromPath(filePath) // omit to save to media library
};
// Launch system camera — user takes photo/video and confirms
const result = await picker.pick(
getContext(this),
[picker.PickerMediaType.PHOTO, picker.PickerMediaType.VIDEO],
pickerProfile
);
if (result.resultCode === 0) {
const uri = result.resultUri; // file URI of captured media
const isPhoto = result.mediaType === picker.PickerMediaType.PHOTO;
}
Requires ohos.permission.CAMERA. Flow: CameraManager → CameraInput → Session → PreviewOutput/PhotoOutput.
import { camera } from '@kit.CameraKit';
// 1. Get camera manager and device
const cameraManager = camera.getCameraManager(context);
const cameras = cameraManager.getSupportedCameras();
const cameraDevice = cameras[0];
// 2. Create input and open
const cameraInput = cameraManager.createCameraInput(cameraDevice);
await cameraInput.open();
// 3. Get output capabilities
const capability = cameraManager.getSupportedOutputCapability(
cameraDevice, camera.SceneMode.NORMAL_PHOTO
);
// 4. Create preview output (surfaceId from XComponent)
const previewOutput = cameraManager.createPreviewOutput(
capability.previewProfiles[0], surfaceId
);
// 5. Create photo output
const photoOutput = cameraManager.createPhotoOutput(capability.photoProfiles[0]);
// 6. Build and start session
const session = cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession;
session.beginConfig();
session.addInput(cameraInput);
session.addOutput(previewOutput);
session.addOutput(photoOutput);
await session.commitConfig();
await session.start();
// 7. Capture photo
photoOutput.on('photoAvailable', (err, photo) => {
const imageObj = photo.main;
imageObj.getComponent(image.ComponentType.JPEG, (err, component) => {
const buffer: ArrayBuffer = component.byteBuffer!;
// Save buffer to file, then release
imageObj.release();
});
});
photoOutput.capture({ quality: camera.QualityLevel.QUALITY_LEVEL_HIGH });
// 8. Cleanup: session.stop() → cameraInput.close() → previewOutput.release() → session.release()
@Entry @Component
struct CameraPreview {
private ctrl = new XComponentController();
private surfaceId = '';
build() {
XComponent({ type: XComponentType.SURFACE, controller: this.ctrl })
.onLoad(() => {
this.surfaceId = this.ctrl.getXComponentSurfaceId();
// Use this.surfaceId to create previewOutput
})
.width('100%').height('100%')
}
}
Key permissions: ohos.permission.CAMERA (photo/video), ohos.permission.MICROPHONE (audio recording).
import { audio } from '@kit.AudioKit';
const rendererOptions: audio.AudioRendererOptions = {
streamInfo: {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000,
channels: audio.AudioChannel.CHANNEL_2,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
},
rendererInfo: {
usage: audio.StreamUsage.STREAM_USAGE_MUSIC, // determines volume type & focus
rendererFlags: 0
}
};
const renderer = await audio.createAudioRenderer(rendererOptions);
renderer.on('writeData', (buffer: ArrayBuffer) => {
// Fill buffer with PCM data
return audio.AudioDataCallbackResult.VALID;
});
await renderer.start();
// ... renderer.pause() / renderer.stop() / renderer.release()
System manages audio focus automatically based on StreamUsage. Listen for focus changes:
renderer.on('audioInterrupt', (event: audio.InterruptEvent) => {
if (event.forceType === audio.InterruptForceType.INTERRUPT_FORCE) {
switch (event.hintType) {
case audio.InterruptHint.INTERRUPT_HINT_PAUSE:
// System paused us — update UI to paused state
break;
case audio.InterruptHint.INTERRUPT_HINT_STOP:
// Permanently lost focus — stop playback
break;
case audio.InterruptHint.INTERRUPT_HINT_DUCK:
// Volume lowered to 20% — optional UI update
break;
case audio.InterruptHint.INTERRUPT_HINT_UNDUCK:
// Volume restored
break;
}
} else if (event.hintType === audio.InterruptHint.INTERRUPT_HINT_RESUME) {
// Can resume playback (SHARE type — app decides)
await renderer.start();
}
});
| StreamUsage | Volume type | Typical use |
|---|---|---|
MUSIC / MOVIE / AUDIOBOOK / GAME | Media | Music, video, audiobook, game BGM |
VOICE_COMMUNICATION | Voice call | VoIP calls |
RINGTONE / NOTIFICATION | Ringtone | Incoming call, notifications |
ALARM | Alarm | Alarms (plays on speaker even with BT) |
NAVIGATION | — | Nav voice (ducks music, doesn't pause) |
const audioManager = audio.getAudioManager();
const sessionManager = audioManager.getSessionManager();
// Activate session with custom concurrency mode
const strategy: audio.AudioSessionStrategy = {
concurrencyMode: audio.AudioConcurrencyMode.CONCURRENCY_PAUSE_OTHERS
};
await sessionManager.activateAudioSession(strategy);
// Monitor deactivation
sessionManager.on('audioSessionDeactivated', (event) => {
console.info('Session deactivated:', event.reason);
});
// When done
await sessionManager.deactivateAudioSession();
Concurrency modes: CONCURRENCY_DEFAULT, CONCURRENCY_MIX_WITH_OTHERS, CONCURRENCY_DUCK_OTHERS, CONCURRENCY_PAUSE_OTHERS.
Apps playing audio in background must:
MUSIC/MOVIE/AUDIOBOOK/GAME): integrate AVSession AND request long-running task (AUDIO_PLAYBACK)AUDIO_PLAYBACK long-running task onlyEnable in build-profile.json5 → arkOptions.obfuscation. ArkGuard supports name obfuscation, code compression, and comment removal for ArkTS/TS/JS.
# obfuscation-rules.txt
-enable-property-obfuscation # obfuscate property names
-enable-toplevel-obfuscation # obfuscate top-level scope names
-enable-export-obfuscation # obfuscate import/export names
-enable-filename-obfuscation # obfuscate file/folder names
-enable-string-property-obfuscation # also obfuscate string literal property names
-compact # remove whitespace and newlines
-remove-log # delete console.* statements
-print-namecache ./nameCache.json # save name mapping (keep for crash analysis!)
-keep-property-name)These properties must be whitelisted to avoid runtime errors:
-keep-property-name
# 1. Dynamic property access
# obj['x' + i] → keep x0, x1, x2 ...
# Object.defineProperty(obj, 'y', {}) → keep y
# 2. JSON parsing fields
# JSON.parse / JSON.stringify → keep all serialized field names
# 3. Network request fields
# http.request extraData: { username, password } → keep field names
# 4. Database fields (ValuesBucket keys)
# relationalStore column names → keep
# 5. NAPI / .so interop
# import from 'libentry.so' → keep exported function names
# 6. ArkUI @Component @State properties are auto-preserved (no action needed)
# SDK API names are auto-preserved (no action needed)
-keep-file-name)-keep-file-name
# System-loaded files that must not be renamed:
# - Worker thread files
# - Ability entry files (auto-collected since DevEco 5.0.3.500)
# - Dynamic import paths: const m = await import('./SomePath')
# - System route table paths (pageSourceFile in route_map.json, auto since API 20)
@KeepSymbol)// Since API 19: use comments to mark names as non-obfuscatable
// @KeepSymbol
class ImportantClass {
// @KeepSymbol
criticalProp: string = '';
}
name, ALL properties named name across all files are keptenum members are auto-collected into whitelist when building HAR with property obfuscationnameCache.json per release — needed to decode obfuscated crash stacks-compact removes all newlines → crash stack line numbers become useless (column info not provided in release builds)'ohos.want.action.home') are NOT auto-whitelisted — add them manually if used as property keysLaunches the system scan UI. Camera is pre-authorized — no permission request required.
import { scanCore, scanBarcode } from '@kit.ScanKit';
const options: scanBarcode.ScanOptions = {
scanTypes: [scanCore.ScanType.ALL],
enableMultiMode: true,
enableAlbum: true
};
try {
const result = await scanBarcode.startScanForResult(
getContext(this), // or this.getUIContext().getHostContext()
options
);
// result.originalValue — decoded string
// result.scanType — code type (QR, EAN-13, etc.)
console.info('Scan result:', result.originalValue);
} catch (err) {
console.error('Scan failed:', err.code, err.message);
}
Supported code types: QR Code, Data Matrix, PDF417, Aztec, EAN-8, EAN-13, UPC-A, UPC-E, Codabar, Code 39/93/128, ITF-14.
import { scanCore, scanBarcode, detectBarcode } from '@kit.ScanKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
// Pick an image from gallery
const picker = new photoAccessHelper.PhotoViewPicker();
const pickerResult = await picker.select({
MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE,
maxSelectNumber: 1
});
// Decode barcode from selected image
const inputImage: detectBarcode.InputImage = { uri: pickerResult.photoUris[0] };
const results = await detectBarcode.decode(inputImage, {
scanTypes: [scanCore.ScanType.ALL],
enableMultiMode: true
});
// results is Array<scanBarcode.ScanResult>
ohos.permission.CAMERA)Use customScan from @kit.ScanKit for full control over the scan UI:
import { scanCore, scanBarcode, customScan } from '@kit.ScanKit';
// 1. Init
customScan.init({ scanTypes: [scanCore.ScanType.ALL], enableMultiMode: true });
// 2. Start with XComponent surfaceId
const viewControl: customScan.ViewControl = { width, height, surfaceId };
const results = await customScan.start(viewControl);
// 3. Control: customScan.openFlashLight() / closeFlashLight()
// customScan.setZoom(2.0) / getZoom()
// customScan.setFocusPoint({x, y}) / resetFocus()
// customScan.stop() / rescan()
// 4. Release when done
await customScan.release();
import { scanCore, generateBarcode } from '@kit.ScanKit';
import { image } from '@kit.ImageKit';
const pixelMap: image.PixelMap = await generateBarcode.createBarcode('https://example.com', {
scanType: scanCore.ScanType.QR_CODE,
height: 400,
width: 400
});
// Use pixelMap directly in Image component: Image(this.pixelMap)
Supports generating: QR Code, EAN-8, EAN-13, UPC-A, UPC-E, Codabar, Code 39/93/128, ITF-14, Data Matrix, PDF417, Aztec.
module.json5{
"module": {
"name": "entry",
"type": "entry",
"metadata": [{
"name": "client_id",
"value": "YOUR_CLIENT_ID" // from AGC console
}]
}
}
Retrieves phone number + UnionID in a single tap. Requires manual signing + AGC permission approval.
import { authentication } from '@kit.AccountKit';
import { util } from '@kit.ArkTS';
// Step 1: Get anonymous phone number (for display on login page)
const authRequest = new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest();
authRequest.scopes = ['quickLoginAnonymousPhone'];
authRequest.state = util.generateRandomUUID();
authRequest.forceAuthorization = false; // must be false for one-click login
const controller = new authentication.AuthenticationController();
const response = await controller.executeRequest(authRequest);
const anonymousPhone = response.data?.extraInfo?.quickLoginAnonymousPhone as string;
// Display anonymousPhone on login page, e.g. "188****1234"
// Step 2: Show LoginWithHuaweiIDButton — user taps to get Authorization Code
import { loginComponentManager, LoginWithHuaweiIDButton } from '@kit.AccountKit';
LoginWithHuaweiIDButton({
params: {
style: loginComponentManager.Style.BUTTON_RED,
borderRadius: 24,
loginType: loginComponentManager.LoginType.QUICK_LOGIN, // one-click login
supportDarkMode: true
},
controller: this.controller // LoginWithHuaweiIDButtonController
})
// In controller callback: response.authorizationCode → send to server
// Server calls /oauth2/v6/quickLogin/getPhoneNumber to get full phone + UnionID
Obfuscation whitelist — if property obfuscation is enabled, add to obfuscation-rules.txt:
-keep-property-name
quickLoginAnonymousPhone
Retrieves UnionID/OpenID via LoginWithHuaweiIDButton or API. Supports enterprise + individual developers.
// Using LoginWithHuaweiIDButton component (recommended)
LoginWithHuaweiIDButton({
params: {
style: loginComponentManager.Style.BUTTON_RED,
borderRadius: 24,
loginType: loginComponentManager.LoginType.ID, // standard login
supportDarkMode: true
},
controller: this.controller
})
// callback returns authorizationCode → server exchanges for UnionID/OpenID
// Using custom button with API
import { authentication } from '@kit.AccountKit';
const loginRequest = new authentication.HuaweiIDProvider().createLoginWithHuaweiIDRequest();
loginRequest.forceLogin = true; // true = show login page if not logged in
loginRequest.state = util.generateRandomUUID();
const controller = new authentication.AuthenticationController(getContext(this));
const response = await controller.executeRequest(loginRequest);
const authCode = response.data?.authorizationCode;
// Send authCode to server → server gets UnionID/OpenID via Access Token
No user interaction — retrieves UnionID for returning users (reinstall, device switch).
const loginRequest = new authentication.HuaweiIDProvider().createLoginWithHuaweiIDRequest();
loginRequest.forceLogin = false; // false = silent, no UI if not logged in
loginRequest.state = util.generateRandomUUID();
const controller = new authentication.AuthenticationController();
const response = await controller.executeRequest(loginRequest);
const authCode = response.data?.authorizationCode;
// If error.code === 1001502001 → user not logged in, show other login methods
| Code | Meaning | Action |
|---|---|---|
1001502001 | Huawei Account not logged in | Show other login methods |
1001502005 | Network error | Retry or show other methods |
1001502012 | User cancelled | No action needed |
1001500001 | Certificate fingerprint check failed | Check signing config |
1001502014 | Missing scopes/permissions | Check AGC permission approval |
1005300001 | User did not agree to protocol | Show agreement dialog |
| ID type | Scope | Use case |
|---|---|---|
| OpenID | Per-app unique | Identify user within one app |
| UnionID | Per-developer unique | Identify user across multiple apps by same developer |
| GroupUnionID | Per-account-group unique | Identify user across affiliated developers |
For cross-platform user data continuity, prefer UnionID over OpenID.
Enable cross-device task handoff: migrate UIAbility state from device A to device B.
module.json5{
"module": {
"abilities": [{
"name": "EntryAbility",
"continuable": true // enable cross-device migration
}]
}
}
// In UIAbility
onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult {
// Save data to migrate (keep under 100KB, use distributed data object for larger data)
wantParam['currentPage'] = 'DetailPage';
wantParam['articleId'] = this.articleId;
// Check target app version compatibility
const targetVersion = wantParam['version'] as number;
if (targetVersion < 2) {
return AbilityConstant.OnContinueResult.MISMATCH;
}
return AbilityConstant.OnContinueResult.AGREE;
}
// Cold start
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
// Restore migrated data
this.articleId = want.parameters?.['articleId'] as number;
// Restore page stack
this.context.restoreWindowStage(this.storage);
}
}
// Hot start (single-instance)
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam) {
if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
this.articleId = want.parameters?.['articleId'] as number;
this.context.restoreWindowStage(this.storage);
}
}
// Disable migration on certain pages
this.context.setMissionContinueState(AbilityConstant.ContinueState.INACTIVE);
// Re-enable when on a migrateable page
this.context.setMissionContinueState(AbilityConstant.ContinueState.ACTIVE);
Use continueType in module.json5 to link different Abilities across devices:
// Device A
{ "name": "PhoneAbility", "continueType": ["myApp_main"] }
// Device B
{ "name": "TabletAbility", "continueType": ["myApp_main"] }
Call in onCreate() of your UIAbility. Token identifies device+app for push messages.
import { pushService } from '@kit.PushKit';
// In EntryAbility.onCreate()
pushService.getToken().then((token: string) => {
console.info('Push token:', token);
// Upload token to your app server for sending push messages
}).catch((err: BusinessError) => {
console.error('Failed to get push token:', err.code, err.message);
});
Prerequisites: Enable Push Service in AGC console first, otherwise getToken() returns error 1000900010.
Token changes on: app reinstall, factory reset, deleteToken() + re-getToken(). Always call getToken() on each app launch to keep server-side token fresh.
Configure a UIAbility with action.ohos.push.listener to receive token updates and push data:
"skills": [{ "actions": ["action.ohos.push.listener"] }]
Push Kit supports: notification messages, voice broadcast, card refresh, background messages, live view, in-app call messages.
Critical: All apps playing audio/video in background must create an AVSession. Without it, the system will force-pause your audio when the app goes to background.
import { avSession as AVSessionManager } from '@kit.AVSessionKit';
// Create session — type: 'audio' | 'video' | 'voice_call'
const session = await AVSessionManager.createAVSession(context, 'MyPlayer', 'audio');
// Set metadata (required — without it, playback controls won't appear)
await session.setAVMetadata({
assetId: 'song_001',
title: 'Song Title',
artist: 'Artist Name',
mediaImage: 'https://example.com/cover.jpg',
duration: 240000 // ms
});
// Set playback state
await session.setAVPlaybackState({
state: AVSessionManager.PlaybackState.PLAYBACK_STATE_PLAY,
position: { elapsedTime: 0, updateTime: Date.now() },
speed: 1.0
});
// Register control commands BEFORE activating
session.on('play', () => { /* resume playback */ });
session.on('pause', () => { /* pause playback */ });
session.on('playNext', () => { /* next track */ });
session.on('playPrevious', () => { /* previous track */ });
session.on('seek', (position: number) => { /* seek to position ms */ });
// Activate — must be called AFTER metadata + commands are set
await session.activate();
For media streams (MUSIC/MOVIE/AUDIOBOOK/GAME):
AUDIO_PLAYBACK long-running task via Background Tasks KitUse session.off() to unregister commands your app doesn't support. The system playback center will gray out corresponding buttons.
session.off('playPrevious'); // no "previous" button
session.off('toggleFavorite'); // no favorite button
$r() and $rawfile()resources/
├─ base/ # default resources (always matched)
│ ├─ element/ # string.json, color.json, float.json, etc.
│ ├─ media/ # images, audio, video
│ └─ profile/ # custom JSON config files
├─ zh_CN/element/ # Chinese locale override
├─ en_US/element/ # English locale override
├─ dark/element/ # dark mode override
├─ rawfile/ # raw files (not compiled, accessed by path)
└─ resfile/ # installed to sandbox, read-only access
Qualifier order: MCC_MNC → language_script_region → orientation → device → colorMode → density.
// App resources: $r('app.type.name')
Text($r('app.string.hello_world'))
.fontSize($r('app.float.text_size_body'))
.fontColor($r('app.color.primary'))
Image($r('app.media.app_icon'))
// With format args: $r('app.string.greeting', 'Alice', 5)
// For string "Hello, %1$s! You have %2$d messages."
Text($r('app.string.greeting', 'Alice', 5))
// Plural: $r('app.plural.item_count', count, count)
Text($r('app.plural.item_count', 2, 2)) // "2 items"
// Raw files: $rawfile('path/relative/to/rawfile/')
Image($rawfile('images/banner.png'))
// System resources: $r('sys.type.name')
Text('Hello')
.fontColor($r('sys.color.ohos_id_color_emphasize'))
.fontSize($r('sys.float.ohos_id_text_size_headline1'))
// Cross-HSP module resources: $r('[moduleName].type.name')
Text($r('[library].string.shared_text'))
const resMgr = getContext(this).resourceManager;
const str = resMgr.getStringByNameSync('hello_world');
const rawFd = resMgr.getRawFd('data.json'); // returns {fd, offset, length}
| Context property | Path | Purpose |
|---|---|---|
filesDir | base/files/ | Persistent app data (survives app updates) |
cacheDir | base/cache/ | Cache (system may auto-clean when space low) |
tempDir | base/temp/ | Temp files (cleaned on app exit) |
databaseDir | database/ | Database files (relationalStore, etc.) |
preferencesDir | base/preferences/ | Preferences KV store |
bundleCodeDir | bundle/ | Installed HAP resources (read-only) |
distributedFilesDir | distributedfiles/ | Cross-device shared files |
For physical goods/services only (hotels, rides, bills). Virtual goods use IAP Kit instead.
import { paymentService } from '@kit.PaymentKit';
import { common } from '@kit.AbilityKit';
// orderStr is built by your server after calling Huawei Pay pre-order API
// Contains: app_id, merc_no, prepay_id, timestamp, noncestr, sign
const orderStr = '{"app_id":"...","merc_no":"...","prepay_id":"...","timestamp":"...","noncestr":"...","sign":"..."}';
const context = getContext(this) as common.UIAbilityContext;
paymentService.requestPayment(context, orderStr)
.then(() => { console.info('Payment succeeded'); })
.catch((err: BusinessError) => { console.error('Payment failed:', err.code, err.message); });
Server flow: your server calls /api/v2/aggr/preorder/create/app → gets prepayId → builds signed orderStr → returns to client.
Supports: Phone, Tablet, PC/2in1. China mainland only.
On-device AI capabilities from @kit.CoreVisionKit. China mainland only, no simulator support.
import { textRecognition } from '@kit.CoreVisionKit';
// Initialize once (e.g., in aboutToAppear)
await textRecognition.init();
// Recognize text from PixelMap
const visionInfo: textRecognition.VisionInfo = { pixelMap: myPixelMap };
const result = await textRecognition.recognizeText(visionInfo, {
isDirectionDetectionSupported: false
});
console.info('OCR result:', result.value); // full recognized text string
// Release when done (e.g., in aboutToDisappear)
await textRecognition.release();
Supports: Chinese (simplified/traditional), English, Japanese, Korean. Input: JPEG/PNG, 720p+ recommended.
import { faceDetector } from '@kit.CoreVisionKit';
await faceDetector.init();
const faces: faceDetector.Face[] = await faceDetector.detect({ pixelMap: myPixelMap });
// Each face: faceRectangle, landmark positions, euler angles, confidence
await faceDetector.release();
import { subjectSegmentation } from '@kit.CoreVisionKit';
await subjectSegmentation.init();
const result = await subjectSegmentation.doSegmentation(
{ pixelMap: myPixelMap },
{ maxCount: 5, enableSubjectDetails: true, enableSubjectForegroundImage: true }
);
// result.fullSubject.foregroundImage — PixelMap with background removed
// result.subjectCount, result.subjectDetails[i].subjectRectangle
await subjectSegmentation.release();
Core file operations via @kit.CoreFileKit. All paths should come from Context properties (filesDir, cacheDir, etc.).
import { fileIo as fs } from '@kit.CoreFileKit';
const context = getContext(this);
// Write a file
const filePath = context.filesDir + '/data.json';
const file = fs.openSync(filePath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
fs.writeSync(file.fd, JSON.stringify({ key: 'value' }));
fs.closeSync(file);
// Read a file
const readFile = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
const buf = new ArrayBuffer(4096);
const readLen = fs.readSync(readFile.fd, buf);
const content = String.fromCharCode(...new Uint8Array(buf.slice(0, readLen)));
fs.closeSync(readFile);
// Check existence
const exists = fs.accessSync(filePath);
// List directory
const entries = fs.listFileSync(context.filesDir);
// Copy file
fs.copyFileSync(filePath, context.cacheDir + '/data_backup.json');
// Delete file
fs.unlinkSync(filePath);
// Stat file (size, mtime)
const stat = fs.statSync(filePath);
console.info(`size: ${stat.size}, mtime: ${stat.mtime}`);
// After picker returns a URI (temporary read-only permission)
const file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
const buf = new ArrayBuffer(4096);
const len = fs.readSync(file.fd, buf);
fs.closeSync(file);
AVPlayer from @kit.MediaKit handles mp4/mp3/mkv/mpeg-ts etc. — just provide the source, no manual decode needed.
import { media } from '@kit.MediaKit';
// Create player
const avPlayer = await media.createAVPlayer();
// Set callbacks
avPlayer.on('stateChange', (state: string) => {
switch (state) {
case 'initialized': // source set, prepare now
avPlayer.prepare();
break;
case 'prepared': // ready to play
avPlayer.play();
break;
case 'completed': // playback finished
avPlayer.release();
break;
}
});
avPlayer.on('error', (err) => {
console.error('AVPlayer error:', err.message);
avPlayer.release();
});
// Set source — local file (fd)
const file = fs.openSync(context.filesDir + '/video.mp4', fs.OpenMode.READ_ONLY);
avPlayer.fdSrc = { fd: file.fd, offset: 0, length: fs.statSync(file.fd).size };
// Or network URL
avPlayer.url = 'https://example.com/audio.mp3';
avPlayer.on('stateChange', (state: string) => {
if (state === 'initialized') {
avPlayer.surfaceId = xComponentSurfaceId; // from XComponent.onLoad
avPlayer.prepare();
} else if (state === 'prepared') {
avPlayer.play();
}
});
avPlayer.url = 'https://example.com/video.mp4';
avPlayer.pause();
avPlayer.play();
avPlayer.seek(30000); // seek to 30s (ms)
avPlayer.setSpeed(media.PlaybackSpeed.SPEED_FORWARD_2_00_X);
avPlayer.setVolume(0.5); // 0.0 ~ 1.0
avPlayer.stop(); // stop → can prepare() again
avPlayer.release(); // release all resources
import { media } from '@kit.MediaKit';
const recorder = await media.createAVRecorder();
const config: media.AVRecorderConfig = {
audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,
videoSourceType: media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV,
profile: {
audioBitrate: 48000,
audioChannels: 2,
audioCodec: media.CodecMimeType.AUDIO_AAC,
audioSampleRate: 48000,
fileFormat: media.ContainerFormatType.CFT_MPEG_4,
videoBitrate: 2000000,
videoCodec: media.CodecMimeType.VIDEO_AVC,
videoFrameWidth: 1920,
videoFrameHeight: 1080,
videoFrameRate: 30
},
url: `fd://${file.fd}`, // file descriptor for output
rotation: 0
};
await recorder.prepare(config);
// For video: const surfaceId = await recorder.getInputSurface();
await recorder.start();
// ... recording ...
await recorder.stop();
await recorder.release();
For background playback: must create AVSession + request AUDIO_PLAYBACK long-running task (see AVSession Kit section).
import { image } from '@kit.ImageKit';
import { fileIo as fs } from '@kit.CoreFileKit';
// From file path
const imageSource = image.createImageSource(context.filesDir + '/photo.jpg');
const pixelMap = await imageSource.createPixelMap({
editable: true,
desiredPixelFormat: image.PixelMapFormat.RGBA_8888
});
// From file descriptor
const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
const imageSource2 = image.createImageSource(file.fd);
// From ArrayBuffer (e.g., from network response)
const imageSource3 = image.createImageSource(arrayBuffer);
// From rawfile
const rawFd = context.resourceManager.getRawFd('image.png');
const imageSource4 = image.createImageSource(rawFd);
// Get image info
const info = await pixelMap.getImageInfo();
console.info(`${info.size.width} x ${info.size.height}`);
pixelMap.crop({ x: 0, y: 0, size: { width: 400, height: 400 } }); // crop
pixelMap.scale(0.5, 0.5); // scale to 50%
pixelMap.rotate(90); // rotate 90° clockwise
pixelMap.flip(true, false); // horizontal flip
pixelMap.flip(false, true); // vertical flip
pixelMap.opacity(0.5); // set 50% opacity
pixelMap.translate(100, 100); // offset by 100px
import { image } from '@kit.ImageKit';
const packer = image.createImagePacker();
const packOpts: image.PackingOption = { format: 'image/jpeg', quality: 90 };
const data: ArrayBuffer = await packer.packing(pixelMap, packOpts);
// Write data to file via fs.writeSync
packer.release();
pixelMap.release();
imageSource.release();
{
"module": {
"abilities": [{
"name": "EntryAbility",
"skills": [{
"entities": ["entity.system.home", "entity.system.browsable"],
"actions": ["ohos.want.action.home", "ohos.want.action.viewData"],
"uris": [{ "scheme": "https", "host": "example.com", "path": "/detail" }],
"domainVerify": true // enable App Linking verification
}]
}]
}
}
import { url } from '@kit.ArkTS';
export default class EntryAbility extends UIAbility {
private targetPage: string = '';
private linkParams: Record<string, string> = {};
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
this.parseUri(want);
}
private parseUri(want: Want): void {
if (want?.uri) {
const urlObj = url.URL.parseURL(want.uri);
this.linkParams = Object.fromEntries(urlObj.params.entries());
this.targetPage = urlObj.pathname; // e.g. "/detail"
}
}
onWindowStageCreate(windowStage: window.WindowStage): void {
const page = this.targetPage === '/detail' ? 'pages/Detail' : 'pages/Index';
if (this.linkParams['id']) {
AppStorage.setOrCreate('linkId', this.linkParams['id']);
}
windowStage.loadContent(page);
}
}
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
this.parseUri(want);
if (this.linkParams['id']) {
AppStorage.setOrCreate('linkId', this.linkParams['id']);
AppStorage.setOrCreate('newWantFlag', true); // notify UI to navigate
}
}
Page listens for newWantFlag via @StorageLink + @Watch and navigates accordingly.
import { systemShare } from '@kit.ShareKit';
import { uniformTypeDescriptor } from '@kit.ArkData';
// Share a hyperlink
const shareData = new systemShare.SharedData({
utd: uniformTypeDescriptor.UniformDataType.HYPERLINK,
content: 'https://example.com/article/123',
title: 'Article Title',
description: 'Article preview text',
});
const controller = new systemShare.ShareController(shareData);
controller.show(this.context, {
previewMode: systemShare.SharePreviewMode.DEFAULT,
selectionMode: systemShare.SelectionMode.SINGLE,
});
Supported UTD types: HYPERLINK, PLAIN_TEXT, HTML, IMAGE (pass uri from file), FILE.
import { ComponentContent } from '@kit.ArkUI';
// 1. Define dialog content via @Builder
@Builder
function dialogContentBuilder(params: { message: string; close: () => void }) {
Column({ space: 16 }) {
Text(params.message).fontSize(16)
Button('OK').onClick(() => params.close())
}
.padding(24)
.backgroundColor(Color.White)
.borderRadius(16)
}
// 2. Open dialog
const uiContext = this.getUIContext();
const contentNode = new ComponentContent(uiContext, wrapBuilder(dialogContentBuilder), {
message: 'Hello',
close: () => uiContext.getPromptAction().closeCustomDialog(contentNode),
});
uiContext.getPromptAction().openCustomDialog(contentNode, {
alignment: DialogAlignment.Center,
isModal: true,
autoCancel: true, // tap outside to close
});
const uiContext = this.getUIContext();
const sheetNode = new ComponentContent(uiContext, wrapBuilder(sheetBuilder), params);
uiContext.openBindSheet(sheetNode, {
title: { title: 'Select Option' },
height: SheetSize.MEDIUM,
preferType: SheetType.BOTTOM,
detents: [SheetSize.MEDIUM, SheetSize.LARGE, 200], // draggable heights
backgroundColor: '#F1F3F5',
}, targetComponentId);
@State isPresented: boolean = false;
@Builder
fullScreenContent() {
Column() {
Text('Full Screen Modal').fontSize(20)
Button('Close').onClick(() => { this.isPresented = false; })
}.width('100%').height('100%').backgroundColor(Color.White)
}
// Trigger:
Button('Show').onClick(() => { this.isPresented = true; })
.bindContentCover($this.isPresented, this.fullScreenContent(), {
modalTransition: ModalTransition.DEFAULT, // .NONE, .ALPHA
})
Note: Do NOT use the deprecated
CustomDialogor@ohos.promptAction— useUIContext.getPromptAction().openCustomDialog(),UIContext.openBindSheet(), and.bindContentCover()instead.
import { KeyboardAvoidMode } from '@kit.ArkUI';
// OFFSET = page lifts up (default); RESIZE = page compresses; NONE = keyboard overlaps
windowStage.getMainWindowSync().getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.RESIZE);
Row() { /* title bar — should stay fixed */ }
.expandSafeArea([SafeAreaType.KEYBOARD])
.zIndex(1)
import { window } from '@kit.ArkUI';
window.getLastWindow(this.getUIContext().getHostContext()).then(win => {
win.on('keyboardHeightChange', (height: number) => {
this.keyboardHeight = this.getUIContext().px2vp(height);
});
});
TextInput().defaultFocus(true) // auto-focus on page load
this.getUIContext().getFocusController().requestFocus('inputId'); // programmatic focus
this.getUIContext().getFocusController().clearFocus(); // dismiss keyboard
Place light-mode colors/images in resources/base/, dark-mode variants (same filenames) in resources/dark/:
resources/base/element/color.json → { "color": [{ "name": "bg_color", "value": "#FFFFFF" }] }
resources/dark/element/color.json → { "color": [{ "name": "bg_color", "value": "#1A1A1A" }] }
resources/base/media/icon.png → light icon
resources/dark/media/icon.png → dark icon
Usage: $r('app.color.bg_color') / $r('app.media.icon') — auto-switches with system theme.
// In EntryAbility — store current mode
onCreate(): void {
AppStorage.setOrCreate('currentColorMode', this.context.config.colorMode);
}
onConfigurationUpdate(newConfig: Configuration): void {
AppStorage.setOrCreate('currentColorMode', newConfig.colorMode);
}
// In component — watch for changes
@StorageProp('currentColorMode') @Watch('onColorModeChange')
currentColorMode: number = ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET;
onColorModeChange(): void {
const isDark = this.currentColorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK;
// update status bar, custom logic, etc.
}
this.getUIContext().getHostContext()?.getApplicationContext()
.setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_DARK); // or COLOR_MODE_LIGHT / COLOR_MODE_NOT_SET
import { request } from '@kit.BasicServicesKit';
const config: request.agent.Config = {
action: request.agent.Action.UPLOAD,
url: 'https://example.com/upload',
mode: request.agent.Mode.BACKGROUND,
method: 'POST',
data: formItems, // Array of FormItem
};
const task = await request.agent.create(context, config);
task.on('progress', (progress) => { /* track */ });
task.on('completed', (progress) => { /* done */ });
await task.start();
await task.pause(); // pause
await task.resume(); // resume with breakpoint
const config: request.agent.Config = {
action: request.agent.Action.DOWNLOAD,
url: 'https://example.com/file.zip',
mode: request.agent.Mode.BACKGROUND,
saveas: `./downloads/file.zip`,
overwrite: true,
gauge: true,
};
const task = await request.agent.create(context, config);
task.on('progress', (progress) => { /* track */ });
await task.start();
resources/base/profile/shortcuts_config.json{
"shortcuts": [
{
"shortcutId": "id_scan",
"label": "$string:shortcut_scan",
"icon": "$media:ic_scan",
"wants": [{
"bundleName": "com.example.myapp",
"moduleName": "entry",
"abilityName": "EntryAbility",
"parameters": { "shortcutKey": "ScanPage" }
}]
}
]
}
"abilities": [{
"name": "EntryAbility",
"metadata": [{
"name": "ohos.ability.shortcuts",
"resource": "$profile:shortcuts_config"
}]
}]
// In EntryAbility — onCreate / onNewWant
const shortcutKey = want?.parameters?.shortcutKey as string;
if (shortcutKey === 'ScanPage') {
AppStorage.setOrCreate('targetPage', 'pages/Scan');
}
Page reads @StorageProp('targetPage') and navigates accordingly.
Text($r('app.string.string_in_har'))
Image($r('app.media.image_in_har'))
// Better performance with .id for resourceManager:
this.context.resourceManager.getStringSync($r('app.string.string_in_har').id);
Text($r('[hsp1].string.string_in_hsp'))
Image($r('[hsp1].media.image_in_hsp'))
Or via createModuleContext:
import { common } from '@kit.AbilityKit';
const hspContext = await common.application.createModuleContext(this.context, 'hsp1');
const str = hspContext.resourceManager.getStringByNameSync('string_in_hsp');
// 1. Register font (in EntryAbility onWindowStageCreate or component aboutToAppear)
const uiContext = windowStage.getMainWindowSync().getUIContext();
uiContext.getFont().registerFont({
familyName: 'MyCustomFont',
familySrc: $rawfile('MyCustomFont.ttf'),
});
// 2. Use in component
Text('Hello').fontFamily('MyCustomFont')
Font size follow/ignore system setting — configure in profile/configuration.json:
{ "configuration": { "fontSizeScale": "followSystem", "fontSizeMaxScale": "2" } }
Reference in app.json5: "configuration": "$profile:configuration".
import { window } from '@kit.ArkUI';
// Get window instance
const win = await window.getLastWindow(this.context);
// Set orientation
win.setPreferredOrientation(window.Orientation.USER_ROTATION_LANDSCAPE); // enter landscape
win.setPreferredOrientation(window.Orientation.USER_ROTATION_PORTRAIT); // back to portrait
win.setPreferredOrientation(window.Orientation.AUTO_ROTATION); // follow sensor
// Monitor window size for layout adaptation
win.on('windowSizeChange', (size) => {
const orientation = display.getDefaultDisplaySync().orientation;
this.isLandscape = (orientation === display.Orientation.LANDSCAPE ||
orientation === display.Orientation.LANDSCAPE_INVERTED);
});
Also configurable in module.json5: "abilities": [{ "orientation": "portrait" }].
Options: portrait, landscape, auto_rotation, auto_rotation_landscape, follow_desktop.
import { pasteboard } from '@kit.BasicServicesKit';
// Write text to clipboard
const pasteData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, 'Hello World');
const board = pasteboard.getSystemPasteboard();
await board.setData(pasteData);
// Read from clipboard
const data = await board.getData();
if (data.hasType(pasteboard.MIMETYPE_TEXT_PLAIN)) {
const text = data.getPrimaryText();
}
Supported MIME types: MIMETYPE_TEXT_PLAIN, MIMETYPE_TEXT_HTML, MIMETYPE_TEXT_URI, MIMETYPE_PIXELMAP.
Stack() {
BottomComponent().hitTestBehavior(HitTestMode.None) // skip self, pass to sibling
TopComponent().hitTestBehavior(HitTestMode.Transparent) // self responds AND passes to sibling
}
| Mode | Behavior |
|---|---|
Default | Self responds, blocks siblings |
Transparent | Self responds, does NOT block siblings |
None | Skips self, passes to siblings |
Block | Only self responds, stops all propagation |
// Parent takes priority over child for same gesture type
ParentComponent()
.priorityGesture(TapGesture().onAction(() => { /* parent handles */ }))
// Both parent and child respond simultaneously
ParentComponent()
.parallelGesture(PanGesture().onAction(() => { /* parent also handles */ }))
// Block child gestures entirely
ParentComponent()
.gesture(TapGesture(), GestureMask.IgnoreInternal)
// Sequential: all must succeed in order
GestureGroup(GestureMode.Sequence, LongPressGesture(), PanGesture())
// Parallel: all run simultaneously
GestureGroup(GestureMode.Parallel, PinchGesture(), RotationGesture())
// Exclusive: first to succeed wins
GestureGroup(GestureMode.Exclusive, TapGesture(), SwipeGesture())
Note: System gestures (onClick, onTouch, drag, bindMenu) always win over custom gestures of the same type.
// Method 1 — expandSafeArea (simplest, component-level)
Column() { /* content */ }
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
// Method 2 — window-level fullscreen (affects all pages)
const win = windowStage.getMainWindowSync();
win.setWindowLayoutFullScreen(true);
import { window } from '@kit.ArkUI';
const win = await window.getLastWindow(context);
const systemAvoid = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
const topHeight = px2vp(systemAvoid.topRect.height); // status bar height
const bottomHeight = px2vp(systemAvoid.bottomRect.height); // navigation bar height
// Listen for changes (e.g. split screen, fold/unfold)
win.on('avoidAreaChange', (options: window.AvoidAreaOptions) => {
if (options.type === window.AvoidAreaType.TYPE_SYSTEM) {
// update top/bottom padding
}
});
win.setSpecificSystemBarEnabled('status', false); // hide status bar
win.setSpecificSystemBarEnabled('navigationIndicator', false); // hide nav indicator
win.setWindowSystemBarProperties({
statusBarContentColor: '#FFFFFF', // white text for dark backgrounds
});
ListItem() { /* content */ }
.swipeAction({
end: {
builder: () => {
Button('Delete').backgroundColor(Color.Red)
.onClick(() => {
animateTo({ duration: 300 }, () => {
this.dataList.splice(index, 1);
});
})
},
actionAreaDistance: 56,
},
edgeEffect: SwipeEdgeEffect.Spring,
})
List() {
ForEach(this.dataList, (item: string, index: number) => {
ListItem() { Text(item) }
})
}
.onItemDragStart((event: ItemDragInfo, itemIndex: number) => {
this.dragIndex = itemIndex;
})
.onItemDragMove((event: ItemDragInfo, itemIndex: number, insertIndex: number) => {
animateTo({ duration: 200 }, () => {
const tmp = this.dataList.splice(this.dragIndex, 1);
this.dataList.splice(insertIndex, 0, tmp[0]);
this.dragIndex = insertIndex;
});
})
Refresh({ refreshing: $this.isRefreshing }) {
List() { /* items */ }
}
.onRefreshing(() => {
// fetch new data...
this.isRefreshing = false;
})
const scroller = new Scroller();
List({ scroller }) { /* items */ }
// After new message:
scroller.scrollEdge(Edge.Bottom);
// Or scroll to specific index:
scroller.scrollToIndex(this.messages.length - 1);
List() { LazyForEach(this.dataSource, ...) }
.maintainVisibleContentPosition(true) // new items at top don't shift visible content
@Builder sectionHeader(title: string) {
Text(title).fontSize(14).fontColor('#99000000')
.width('100%').padding({ left: 16, top: 8, bottom: 8 })
.backgroundColor('#F1F3F5')
}
List() {
ForEach(this.groups, (group: GroupData) => {
ListItemGroup({ header: this.sectionHeader(group.title) }) {
ForEach(group.items, (item: ItemData) => {
ListItem() { Text(item.name) }
})
}
})
}
.sticky(StickyStyle.Header) // header sticks to top when scrolling
List() { LazyForEach(this.dataSource, ...) }
.onReachEnd(() => {
if (!this.isLoading) {
this.isLoading = true;
this.loadNextPage().then(() => { this.isLoading = false; });
}
})