
Harmonyos App
- 447 installs
- 253 repo stars
- Updated August 4, 2026
- majiayu000/claude-arsenal
harmonyos-app is a Claude Code skill that scaffolds and implements HarmonyOS mobile apps with ArkUI screens, lifecycle, permissions, and device APIs for developers building client-side features.
About
harmonyos-app is a Mobile Development skill from majiayu000/claude-arsenal that guides scaffolding and implementation of HarmonyOS applications during active feature work. The skill covers ArkUI screen composition, application lifecycle management, permission declarations, and device API integration on Huawei's HarmonyOS stack. Developers reach for harmonyos-app when starting a new HarmonyOS project or adding client-side screens and native capabilities without manually assembling boilerplate from scattered docs. It targets engineers working in ArkUI who need structured guidance for permissions, lifecycle events, and device-facing APIs during day-to-day mobile feature development.
- HarmonyOS ArkUI screen patterns
- App lifecycle and permissions
- Device API integration guidance
- Mobile navigation scaffolding
- Platform-specific UI conventions
Harmonyos App by the numbers
- 447 all-time installs (skills.sh)
- Ranked #318 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/majiayu000/claude-arsenal --skill harmonyos-appAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 447 |
|---|---|
| repo stars | ★ 253 |
| Last updated | August 4, 2026 |
| Repository | majiayu000/claude-arsenal ↗ |
How do you scaffold HarmonyOS ArkUI apps?
Scaffold and implement HarmonyOS mobile apps—ArkUI screens, lifecycle, permissions, and device APIs—during active client-side feature development.
Who is it for?
Mobile developers building HarmonyOS apps who need ArkUI scaffolding, lifecycle setup, and device API guidance during feature work.
Skip if: Developers targeting iOS, Android, or React Native who do not ship on HarmonyOS or ArkUI.
When should I use this skill?
User is building a HarmonyOS app, adding ArkUI screens, configuring lifecycle or permissions, or integrating HarmonyOS device APIs.
What you get
HarmonyOS app scaffold, ArkUI screens, lifecycle handlers, permission configs, and device API integrations
- ArkUI screen files
- Lifecycle configuration
- Permission declarations
Files
HarmonyOS Application Development
Core Principles
- ArkTS First — Use ArkTS with strict type safety, no
anyor dynamic types - Declarative UI — Build UI with ArkUI's declarative components and state management
- Stage Model — Use modern Stage model (UIAbility), not legacy FA model
- Distributed by Design — Leverage cross-device capabilities from the start
- Atomic Services — Consider atomic services and cards for lightweight experiences
- One-time Development — Design for multi-device adaptation (phone, tablet, watch, TV)
---
Hard Rules (Must Follow)
These rules are mandatory. Violating them means the skill is not working correctly.
No Dynamic Types
ArkTS prohibits dynamic typing. Never use `any`, type assertions, or dynamic property access.
// ❌ FORBIDDEN: Dynamic types
let data: any = fetchData();
let obj: object = {};
obj['dynamicKey'] = value; // Dynamic property access
(someVar as SomeType).method(); // Type assertion
// ✅ REQUIRED: Strict typing
interface UserData {
id: string;
name: string;
}
let data: UserData = fetchData();
// Use Record for dynamic keys
let obj: Record<string, string> = {};
obj['key'] = value; // OK with Record typeNo Direct State Mutation
Never mutate @State/@Prop variables directly in nested objects. Use immutable updates.
// ❌ FORBIDDEN: Direct mutation
@State user: User = { name: 'John', age: 25 };
updateAge() {
this.user.age = 26; // UI won't update!
}
// ✅ REQUIRED: Immutable update
updateAge() {
this.user = { ...this.user, age: 26 }; // Creates new object, triggers UI update
}
// For arrays
@State items: string[] = ['a', 'b'];
// ❌ FORBIDDEN
this.items.push('c'); // UI won't update
// ✅ REQUIRED
this.items = [...this.items, 'c'];Stage Model Only
Always use Stage model (UIAbility). Never use deprecated FA model (PageAbility).
// ❌ FORBIDDEN: FA Model (deprecated)
// config.json with "pages" array
export default {
onCreate() { ... } // PageAbility lifecycle
}
// ✅ REQUIRED: Stage Model
// module.json5 with abilities configuration
import { UIAbility } from '@kit.AbilityKit';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// Modern Stage model lifecycle
}
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index');
}
}Component Reusability
Extract reusable UI into @Component. No inline complex UI in build() methods.
// ❌ FORBIDDEN: Monolithic build method
@Entry
@Component
struct MainPage {
build() {
Column() {
// 200+ lines of inline UI...
Row() {
Image($r('app.media.avatar'))
Column() {
Text(this.user.name)
Text(this.user.email)
}
}
// More inline UI...
}
}
}
// ✅ REQUIRED: Extract components
@Component
struct UserCard {
@Prop user: User;
build() {
Row() {
Image($r('app.media.avatar'))
Column() {
Text(this.user.name)
Text(this.user.email)
}
}
}
}
@Entry
@Component
struct MainPage {
@State user: User = { name: 'John', email: 'john@example.com' };
build() {
Column() {
UserCard({ user: this.user })
}
}
}---
Quick Reference
When to Use What
| Scenario | Pattern | Example |
|---|---|---|
| Component-local state | @State | Counter, form inputs |
| Parent-to-child data | @Prop | Read-only child data |
| Two-way binding | @Link | Shared mutable state |
| Cross-component state | @Provide/@Consume | Theme, user context |
| Persistent state | PersistentStorage | User preferences |
| App-wide state | AppStorage | Global state |
| Complex state logic | @Observed/@ObjectLink | Nested object updates |
State Decorator Selection
@State → Component owns the state, triggers re-render on change
@Prop → Parent passes value, child gets copy (one-way)
@Link → Parent passes reference, child can modify (two-way)
@Provide → Ancestor provides value to all descendants
@Consume → Descendant consumes value from ancestor
@StorageLink → Syncs with AppStorage, two-way binding
@StorageProp → Syncs with AppStorage, one-way binding
@Observed → Class decorator for observable objects
@ObjectLink → Links to @Observed object in parent---
Project Structure
Recommended Architecture
MyApp/
├── entry/ # Main entry module
│ ├── src/main/
│ │ ├── ets/
│ │ │ ├── entryability/ # UIAbility definitions
│ │ │ │ └── EntryAbility.ets
│ │ │ ├── pages/ # Page components
│ │ │ │ ├── Index.ets
│ │ │ │ └── Detail.ets
│ │ │ ├── components/ # Reusable UI components
│ │ │ │ ├── common/ # Common components
│ │ │ │ └── business/ # Business-specific components
│ │ │ ├── viewmodel/ # ViewModels (MVVM)
│ │ │ ├── model/ # Data models
│ │ │ ├── service/ # Business logic services
│ │ │ ├── repository/ # Data access layer
│ │ │ ├── utils/ # Utility functions
│ │ │ └── constants/ # Constants and configs
│ │ ├── resources/ # Resources (strings, images)
│ │ └── module.json5 # Module configuration
│ └── build-profile.json5
├── common/ # Shared library module
│ └── src/main/ets/
├── features/ # Feature modules
│ ├── feature_home/
│ └── feature_profile/
└── build-profile.json5 # Project configurationLayer Separation
┌─────────────────────────────────────┐
│ UI Layer (Pages) │ ArkUI Components
├─────────────────────────────────────┤
│ ViewModel Layer │ State management, UI logic
├─────────────────────────────────────┤
│ Service Layer │ Business logic
├─────────────────────────────────────┤
│ Repository Layer │ Data access abstraction
├─────────────────────────────────────┤
│ Data Sources (Local/Remote) │ Preferences, RDB, Network
└─────────────────────────────────────┘---
ArkUI Component Patterns
Basic Component Structure
import { router } from '@kit.ArkUI';
@Component
export struct ProductCard {
// Props from parent
@Prop product: Product;
@Prop onAddToCart: (product: Product) => void;
// Local state
@State isExpanded: boolean = false;
// Computed values (use getters)
get formattedPrice(): string {
return `¥${this.product.price.toFixed(2)}`;
}
// Lifecycle
aboutToAppear(): void {
console.info('ProductCard appearing');
}
aboutToDisappear(): void {
console.info('ProductCard disappearing');
}
// Event handlers
private handleTap(): void {
router.pushUrl({ url: 'pages/ProductDetail', params: { id: this.product.id } });
}
private handleAddToCart(): void {
this.onAddToCart(this.product);
}
// UI builder
build() {
Column() {
Image(this.product.imageUrl)
.width('100%')
.aspectRatio(1)
.objectFit(ImageFit.Cover)
Text(this.product.name)
.fontSize(16)
.fontWeight(FontWeight.Medium)
Text(this.formattedPrice)
.fontSize(14)
.fontColor('#FF6B00')
Button('Add to Cart')
.onClick(() => this.handleAddToCart())
}
.padding(12)
.backgroundColor(Color.White)
.borderRadius(8)
.onClick(() => this.handleTap())
}
}List with LazyForEach
import { BasicDataSource } from '../utils/BasicDataSource';
class ProductDataSource extends BasicDataSource<Product> {
private products: Product[] = [];
totalCount(): number {
return this.products.length;
}
getData(index: number): Product {
return this.products[index];
}
addData(product: Product): void {
this.products.push(product);
this.notifyDataAdd(this.products.length - 1);
}
updateData(index: number, product: Product): void {
this.products[index] = product;
this.notifyDataChange(index);
}
}
@Component
struct ProductList {
private dataSource: ProductDataSource = new ProductDataSource();
build() {
List() {
LazyForEach(this.dataSource, (product: Product, index: number) => {
ListItem() {
ProductCard({ product: product })
}
}, (product: Product) => product.id) // Key generator
}
.lanes(2) // Grid with 2 columns
.cachedCount(4) // Cache 4 items for smooth scrolling
}
}Custom Dialog
@CustomDialog
struct ConfirmDialog {
controller: CustomDialogController;
title: string = 'Confirm';
message: string = '';
onConfirm: () => void = () => {};
build() {
Column() {
Text(this.title)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
Text(this.message)
.fontSize(16)
.margin({ bottom: 24 })
Row() {
Button('Cancel')
.onClick(() => this.controller.close())
.backgroundColor(Color.Gray)
.margin({ right: 16 })
Button('Confirm')
.onClick(() => {
this.onConfirm();
this.controller.close();
})
}
}
.padding(24)
}
}
// Usage
@Entry
@Component
struct MainPage {
dialogController: CustomDialogController = new CustomDialogController({
builder: ConfirmDialog({
title: 'Delete Item',
message: 'Are you sure you want to delete this item?',
onConfirm: () => this.deleteItem()
}),
autoCancel: true
});
private deleteItem(): void {
// Delete logic
}
build() {
Button('Delete')
.onClick(() => this.dialogController.open())
}
}---
Extended Reference
Detailed material starting at ## State Management Patterns has been moved to `reference/extended.md` to keep this skill concise. Load that reference when the task requires the moved examples, command catalogs, checklists, platform details, or implementation templates.
ArkTS Language Guide
ArkTS is a TypeScript superset optimized for HarmonyOS with static typing enforcement and UI declaration extensions.
Key Differences from TypeScript
Prohibited Features
// ❌ These TypeScript features are NOT allowed in ArkTS
// 1. any type
let data: any; // Error!
// 2. unknown type with assertions
let value: unknown;
(value as string).length; // Error!
// 3. Dynamic property access
let obj = {};
obj['key'] = value; // Error! (unless Record type)
// 4. Structural typing for classes
class A { x: number = 0; }
class B { x: number = 0; }
let a: A = new B(); // Error! Classes must be explicitly related
// 5. typeof for types
type T = typeof someVariable; // Error!
// 6. keyof operator
type Keys = keyof SomeType; // Error!
// 7. Indexed access types
type Value = SomeType['key']; // Error!
// 8. Conditional types
type Check<T> = T extends string ? 'yes' : 'no'; // Error!
// 9. Mapped types
type Readonly<T> = { readonly [P in keyof T]: T[P] }; // Error!
// 10. Symbol and unique symbol
const sym = Symbol('key'); // Error!Allowed Patterns
// ✅ These patterns are supported
// 1. Explicit types
let data: string = 'hello';
let count: number = 42;
// 2. Interfaces
interface User {
id: string;
name: string;
age?: number; // Optional properties OK
}
// 3. Type aliases (basic)
type UserId = string;
type Callback = (data: string) => void;
// 4. Generics (basic)
class Container<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
// 5. Union types (basic)
type Status = 'loading' | 'success' | 'error';
let status: Status = 'loading';
// 6. Record type for dynamic keys
let map: Record<string, number> = {};
map['key1'] = 100; // OK with Record
// 7. Enums
enum Color {
Red,
Green,
Blue
}
// 8. Class inheritance
class Animal {
name: string = '';
}
class Dog extends Animal {
breed: string = '';
}Type System
Primitive Types
// Numbers
let integer: number = 42;
let float: number = 3.14;
// Strings
let text: string = 'Hello';
let template: string = `Value: ${integer}`;
// Booleans
let flag: boolean = true;
// Arrays
let numbers: number[] = [1, 2, 3];
let strings: Array<string> = ['a', 'b', 'c'];
// Tuples
let tuple: [string, number] = ['age', 25];
// Null and undefined
let nullable: string | null = null;
let optional: string | undefined = undefined;Object Types
// Interface
interface Product {
readonly id: string; // Read-only
name: string;
price: number;
description?: string; // Optional
}
// Implementation
const product: Product = {
id: 'prod_001',
name: 'Phone',
price: 999
};
// Type alias
type Point = {
x: number;
y: number;
};Function Types
// Function declarations
function add(a: number, b: number): number {
return a + b;
}
// Arrow functions
const multiply = (a: number, b: number): number => a * b;
// Optional parameters
function greet(name: string, greeting?: string): string {
return `${greeting ?? 'Hello'}, ${name}`;
}
// Default parameters
function createUser(name: string, role: string = 'user'): User {
return { name, role };
}
// Rest parameters
function sum(...numbers: number[]): number {
return numbers.reduce((a, b) => a + b, 0);
}
// Callback types
type ClickHandler = (event: ClickEvent) => void;
function setOnClick(handler: ClickHandler): void {
// ...
}Generics
// Generic function
function identity<T>(value: T): T {
return value;
}
// Generic interface
interface Repository<T> {
getById(id: string): Promise<T>;
save(item: T): Promise<void>;
delete(id: string): Promise<void>;
}
// Generic class
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
}
// Generic constraints (basic)
interface HasId {
id: string;
}
class EntityRepository<T extends HasId> {
private entities: Map<string, T> = new Map();
save(entity: T): void {
this.entities.set(entity.id, entity);
}
}Classes
Class Declaration
class User {
// Properties with default values (required in ArkTS)
private id: string = '';
public name: string = '';
protected email: string = '';
readonly createdAt: Date = new Date();
// Static members
static userCount: number = 0;
// Constructor
constructor(id: string, name: string, email: string) {
this.id = id;
this.name = name;
this.email = email;
User.userCount++;
}
// Methods
public getDisplayName(): string {
return this.name;
}
private validateEmail(): boolean {
return this.email.includes('@');
}
// Getter/Setter
get displayId(): string {
return `USER-${this.id}`;
}
set displayId(value: string) {
this.id = value.replace('USER-', '');
}
// Static method
static createGuest(): User {
return new User('guest', 'Guest', 'guest@example.com');
}
}Inheritance
// Base class
abstract class Shape {
abstract area(): number;
abstract perimeter(): number;
describe(): string {
return `Area: ${this.area()}, Perimeter: ${this.perimeter()}`;
}
}
// Derived class
class Rectangle extends Shape {
constructor(private width: number, private height: number) {
super();
}
area(): number {
return this.width * this.height;
}
perimeter(): number {
return 2 * (this.width + this.height);
}
}
// Interface implementation
interface Drawable {
draw(): void;
}
class Circle extends Shape implements Drawable {
constructor(private radius: number) {
super();
}
area(): number {
return Math.PI * this.radius ** 2;
}
perimeter(): number {
return 2 * Math.PI * this.radius;
}
draw(): void {
console.info(`Drawing circle with radius ${this.radius}`);
}
}Async/Await
// Async function
async function fetchUser(id: string): Promise<User> {
const response = await httpClient.get<User>(`/users/${id}`);
return response;
}
// Error handling
async function safeGetUser(id: string): Promise<User | null> {
try {
return await fetchUser(id);
} catch (error) {
console.error(`Failed to fetch user: ${(error as Error).message}`);
return null;
}
}
// Parallel execution
async function loadDashboard(): Promise<DashboardData> {
const [user, orders, notifications] = await Promise.all([
fetchUser('current'),
fetchOrders(),
fetchNotifications()
]);
return { user, orders, notifications };
}
// Sequential execution
async function processOrders(orderIds: string[]): Promise<void> {
for (const id of orderIds) {
await processOrder(id); // One at a time
}
}Module System
// Named exports
// utils/math.ets
export function add(a: number, b: number): number {
return a + b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
export const PI = 3.14159;
// Default export
// models/User.ets
export default class User {
constructor(public name: string) {}
}
// Named imports
import { add, multiply, PI } from '../utils/math';
// Default import
import User from '../models/User';
// Rename imports
import { add as sum } from '../utils/math';
// Import all
import * as MathUtils from '../utils/math';
MathUtils.add(1, 2);
// Re-export
// index.ets
export { add, multiply } from './math';
export { default as User } from './User';Best Practices
1. Always Initialize Properties
// ❌ Bad
class User {
name: string; // Error: not initialized
}
// ✅ Good
class User {
name: string = '';
}
// ✅ Also good: initialize in constructor
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}2. Use Explicit Return Types
// ❌ Bad
function getUser(id: string) {
return { id, name: 'John' };
}
// ✅ Good
function getUser(id: string): User {
return { id, name: 'John' };
}3. Prefer Interfaces Over Type Aliases for Objects
// ✅ Preferred for object types
interface User {
id: string;
name: string;
}
// Use type for unions, primitives, tuples
type Status = 'active' | 'inactive';
type Coordinate = [number, number];4. Use Record for Dynamic Keys
// ❌ Bad
let cache = {};
cache['key'] = value; // Error!
// ✅ Good
let cache: Record<string, CacheEntry> = {};
cache['key'] = value; // OK5. Avoid Optional Chaining on Non-Nullable
// ❌ Bad: unnecessary optional chaining
const user: User = getUser();
console.log(user?.name); // user is not nullable
// ✅ Good
console.log(user.name);ArkUI Component Guide
ArkUI is the declarative UI framework for HarmonyOS applications.
Component Basics
Built-in Components
// Text
Text('Hello World')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
// Image
Image($r('app.media.icon'))
.width(100)
.height(100)
.objectFit(ImageFit.Cover)
// Button
Button('Click Me')
.type(ButtonType.Capsule)
.width(200)
.height(48)
.onClick(() => {
console.info('Button clicked');
})
// TextInput
TextInput({ placeholder: 'Enter text' })
.width('100%')
.height(48)
.onChange((value: string) => {
this.inputValue = value;
})Layout Containers
// Column - Vertical layout
Column() {
Text('Item 1')
Text('Item 2')
Text('Item 3')
}
.width('100%')
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.SpaceBetween)
// Row - Horizontal layout
Row() {
Image($r('app.media.avatar')).width(48).height(48)
Text('Username').margin({ left: 12 })
Blank() // Flexible space
Image($r('app.media.arrow'))
}
.width('100%')
.padding(16)
// Stack - Overlapping layout
Stack({ alignContent: Alignment.BottomEnd }) {
Image($r('app.media.photo'))
Badge({ count: 5 })
}
// Flex - Flexible layout
Flex({
direction: FlexDirection.Row,
wrap: FlexWrap.Wrap,
justifyContent: FlexAlign.SpaceAround
}) {
ForEach(this.items, (item: Item) => {
ItemCard({ item: item })
})
}List Components
// Basic List
List() {
ForEach(this.dataList, (item: DataItem, index: number) => {
ListItem() {
Text(item.name)
}
}, (item: DataItem) => item.id)
}
.width('100%')
.divider({ strokeWidth: 1, color: '#E8E8E8' })
// Swipe Actions
List() {
ForEach(this.items, (item: Item) => {
ListItem() {
ItemRow({ item: item })
}
.swipeAction({
end: this.DeleteButton(item.id)
})
})
}
@Builder
DeleteButton(id: string) {
Button('Delete')
.backgroundColor(Color.Red)
.onClick(() => this.deleteItem(id))
}
// Grid
Grid() {
ForEach(this.products, (product: Product) => {
GridItem() {
ProductCard({ product: product })
}
})
}
.columnsTemplate('1fr 1fr') // 2 columns
.rowsGap(12)
.columnsGap(12)
// WaterFlow (Masonry layout)
WaterFlow() {
ForEach(this.images, (image: ImageData) => {
FlowItem() {
Image(image.url)
.width('100%')
.aspectRatio(image.aspectRatio)
}
})
}
.columnsTemplate('1fr 1fr')Scroll Components
// Scroll
Scroll() {
Column() {
ForEach(this.items, (item: Item) => {
ItemCard({ item: item })
})
}
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Auto)
.edgeEffect(EdgeEffect.Spring)
// Swiper
Swiper() {
ForEach(this.banners, (banner: Banner) => {
Image(banner.imageUrl)
.width('100%')
.height(200)
})
}
.autoPlay(true)
.interval(3000)
.indicator(true)
// Tabs
Tabs({ barPosition: BarPosition.Start }) {
TabContent() {
HomeTab()
}.tabBar('Home')
TabContent() {
DiscoverTab()
}.tabBar('Discover')
TabContent() {
ProfileTab()
}.tabBar('Profile')
}
.barMode(BarMode.Fixed)
.onChange((index: number) => {
this.currentTab = index;
})Custom Components
Basic Structure
@Component
struct UserCard {
// Props from parent
@Prop username: string = '';
@Prop avatarUrl: string = '';
// Local state
@State isFollowing: boolean = false;
build() {
Row() {
Image(this.avatarUrl)
.width(48)
.height(48)
.borderRadius(24)
Column() {
Text(this.username)
.fontSize(16)
.fontWeight(FontWeight.Medium)
}
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
Blank()
Button(this.isFollowing ? 'Following' : 'Follow')
.onClick(() => {
this.isFollowing = !this.isFollowing;
})
}
.width('100%')
.padding(16)
}
}@Builder Functions
@Component
struct ProductList {
@State products: Product[] = [];
// Private builder
@Builder
ProductItem(product: Product) {
Row() {
Image(product.imageUrl)
.width(80)
.height(80)
Column() {
Text(product.name)
Text(`$${product.price}`)
.fontColor('#FF6B00')
}
}
}
// Builder with parameter
@Builder
SectionHeader(title: string) {
Text(title)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ top: 16, bottom: 8 })
}
build() {
List() {
ListItem() {
this.SectionHeader('Featured Products')
}
ForEach(this.products, (product: Product) => {
ListItem() {
this.ProductItem(product)
}
})
}
}
}@BuilderParam (Slots)
// Card component with slot
@Component
struct Card {
@BuilderParam content: () => void = this.defaultContent;
@BuilderParam footer: () => void = this.defaultFooter;
@Builder
defaultContent() {
Text('Default content')
}
@Builder
defaultFooter() {}
build() {
Column() {
// Content slot
this.content()
// Footer slot
this.footer()
}
.padding(16)
.backgroundColor(Color.White)
.borderRadius(8)
}
}
// Usage
@Component
struct ProductPage {
build() {
Card() {
// Content
Column() {
Image($r('app.media.product'))
Text('Product Name')
}
}
.footer(() => {
Row() {
Button('Add to Cart')
Button('Buy Now')
}
})
}
}@Styles and @Extend
// Reusable styles
@Styles
function cardStyle() {
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 8, color: '#1A000000' })
.padding(16)
}
@Styles
function centerStyle() {
.width('100%')
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
}
// Extend specific component
@Extend(Text)
function titleStyle() {
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A1A')
}
@Extend(Button)
function primaryButton() {
.type(ButtonType.Capsule)
.backgroundColor('#007AFF')
.fontColor(Color.White)
.width('100%')
.height(48)
}
// Usage
@Component
struct StyledPage {
build() {
Column() {
Column() {
Text('Welcome')
.titleStyle()
Text('Description here')
}
.cardStyle()
Button('Get Started')
.primaryButton()
}
.centerStyle()
}
}Animations
Attribute Animation
@Component
struct AnimatedButton {
@State scale: number = 1;
@State opacity: number = 1;
build() {
Button('Animated')
.scale({ x: this.scale, y: this.scale })
.opacity(this.opacity)
.animation({
duration: 300,
curve: Curve.EaseInOut
})
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) {
this.scale = 0.95;
this.opacity = 0.8;
} else if (event.type === TouchType.Up) {
this.scale = 1;
this.opacity = 1;
}
})
}
}Explicit Animation
@Component
struct ExplicitAnimation {
@State rotateAngle: number = 0;
@State translateY: number = 0;
build() {
Column() {
Image($r('app.media.icon'))
.rotate({ angle: this.rotateAngle })
.translate({ y: this.translateY })
Button('Animate')
.onClick(() => {
animateTo({
duration: 1000,
curve: Curve.EaseInOut,
iterations: 1,
playMode: PlayMode.Normal
}, () => {
this.rotateAngle = 360;
this.translateY = 100;
})
})
}
}
}Transition Animation
@Component
struct TransitionDemo {
@State isVisible: boolean = false;
build() {
Column() {
Button('Toggle')
.onClick(() => {
this.isVisible = !this.isVisible;
})
if (this.isVisible) {
Text('Animated Content')
.transition({
type: TransitionType.Insert,
opacity: 0,
translate: { y: 50 }
})
.transition({
type: TransitionType.Delete,
opacity: 0,
scale: { x: 0.8, y: 0.8 }
})
}
}
}
}Gestures
@Component
struct GestureDemo {
@State offsetX: number = 0;
@State offsetY: number = 0;
@State scale: number = 1;
build() {
Column() {
Image($r('app.media.photo'))
.translate({ x: this.offsetX, y: this.offsetY })
.scale({ x: this.scale, y: this.scale })
// Pan gesture
.gesture(
PanGesture()
.onActionUpdate((event: GestureEvent) => {
this.offsetX = event.offsetX;
this.offsetY = event.offsetY;
})
)
// Pinch gesture
.gesture(
PinchGesture({ fingers: 2 })
.onActionUpdate((event: GestureEvent) => {
this.scale = event.scale;
})
)
// Combined gestures
.gesture(
GestureGroup(GestureMode.Parallel,
TapGesture({ count: 2 })
.onAction(() => {
this.scale = this.scale === 1 ? 2 : 1;
}),
LongPressGesture()
.onAction(() => {
// Show context menu
})
)
)
}
}
}Dialog and Popup
@Component
struct DialogDemo {
dialogController: CustomDialogController = new CustomDialogController({
builder: ConfirmDialog({
title: 'Confirm',
message: 'Are you sure?',
onConfirm: () => this.handleConfirm(),
onCancel: () => this.dialogController.close()
}),
autoCancel: true,
alignment: DialogAlignment.Center
});
handleConfirm(): void {
// Handle confirmation
this.dialogController.close();
}
build() {
Button('Show Dialog')
.onClick(() => {
this.dialogController.open();
})
}
}
@CustomDialog
struct ConfirmDialog {
controller: CustomDialogController = new CustomDialogController({ builder: ConfirmDialog() });
title: string = '';
message: string = '';
onConfirm: () => void = () => {};
onCancel: () => void = () => {};
build() {
Column() {
Text(this.title)
.fontSize(18)
.fontWeight(FontWeight.Bold)
Text(this.message)
.margin({ top: 16 })
Row() {
Button('Cancel')
.onClick(() => this.onCancel())
Button('Confirm')
.onClick(() => this.onConfirm())
}
.margin({ top: 24 })
.justifyContent(FlexAlign.SpaceEvenly)
.width('100%')
}
.padding(24)
}
}Responsive Layout
@Component
struct ResponsiveLayout {
@StorageProp('currentBreakpoint') currentBreakpoint: string = 'sm';
build() {
GridRow({
columns: { sm: 4, md: 8, lg: 12 },
gutter: { x: 12, y: 12 }
}) {
GridCol({ span: { sm: 4, md: 4, lg: 3 } }) {
this.Sidebar()
}
GridCol({ span: { sm: 4, md: 4, lg: 9 } }) {
this.MainContent()
}
}
}
@Builder
Sidebar() {
Column() {
// Sidebar content
}
.visibility(this.currentBreakpoint === 'sm'
? Visibility.None
: Visibility.Visible)
}
@Builder
MainContent() {
Column() {
// Main content
}
}
}Best Practices
Component Design
// ✅ Good: Single responsibility, reusable
@Component
struct Avatar {
@Prop src: string = '';
@Prop size: number = 48;
@Prop borderRadius: number = 24;
build() {
Image(this.src)
.width(this.size)
.height(this.size)
.borderRadius(this.borderRadius)
.objectFit(ImageFit.Cover)
}
}
// ✅ Good: Composition over inheritance
@Component
struct UserProfile {
@Prop user: User = new User();
build() {
Row() {
Avatar({ src: this.user.avatar, size: 64 })
Column() {
Text(this.user.name)
Text(this.user.bio)
}
}
}
}Performance
// ✅ Good: Use LazyForEach for large lists
LazyForEach(this.dataSource, (item: Item) => {
ListItem() {
ItemCard({ item: item })
}
}, (item: Item) => item.id)
// ✅ Good: Provide key function for ForEach
ForEach(this.items, (item: Item, index: number) => {
ItemRow({ item: item })
}, (item: Item) => item.id) // Key function
// ✅ Good: Avoid unnecessary re-renders
@Component
struct OptimizedList {
@State @Watch('onDataChange') items: Item[] = [];
onDataChange(): void {
// Only called when items actually change
}
}Distributed Capabilities
HarmonyOS distributed capabilities enable seamless collaboration between devices in a Super Device ecosystem.
Overview
┌─────────────────────────────────────────────────────────────┐
│ Super Device Ecosystem │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Phone │◄──►│ Tablet │◄──►│ Watch │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ▲ ▲ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ TV │◄──►│ Car │ │
│ └──────────┘ └──────────┘ │
│ │
│ Capabilities: │
│ • Distributed Data • Cross-device Call │
│ • Distributed Objects • Device Discovery │
│ • Distributed Files • Ability Continuation │
│ │
└─────────────────────────────────────────────────────────────┘Device Discovery
Discovering Nearby Devices
import { distributedDeviceManager } from '@kit.DistributedServiceKit';
class DeviceDiscovery {
private deviceManager: distributedDeviceManager.DeviceManager | null = null;
async init(): Promise<void> {
this.deviceManager = distributedDeviceManager.createDeviceManager('com.example.app');
}
// Get trusted devices (already authenticated)
getTrustedDevices(): distributedDeviceManager.DeviceBasicInfo[] {
if (!this.deviceManager) return [];
return this.deviceManager.getAvailableDeviceListSync();
}
// Start discovery
startDiscovery(): void {
if (!this.deviceManager) return;
const discoverParam: distributedDeviceManager.DiscoveryParam = {
discoverTargetType: distributedDeviceManager.DiscoverTargetType.DEVICE
};
this.deviceManager.startDiscovering(discoverParam);
this.deviceManager.on('discoverSuccess', (data) => {
console.info(`Discovered device: ${data.device.deviceName}`);
});
this.deviceManager.on('discoverFailure', (reason) => {
console.error(`Discovery failed: ${reason}`);
});
}
// Stop discovery
stopDiscovery(): void {
this.deviceManager?.stopDiscovering();
}
// Authenticate device
async authenticateDevice(device: distributedDeviceManager.DeviceBasicInfo): Promise<void> {
if (!this.deviceManager) return;
const authParam: distributedDeviceManager.AuthParam = {
authType: distributedDeviceManager.AuthType.PIN_CODE,
extraInfo: {}
};
await this.deviceManager.authenticateDevice(device, authParam);
}
release(): void {
this.deviceManager?.release();
this.deviceManager = null;
}
}Device Selection UI
@Component
struct DeviceSelector {
@State devices: distributedDeviceManager.DeviceBasicInfo[] = [];
@State selectedDevice: distributedDeviceManager.DeviceBasicInfo | null = null;
private discovery: DeviceDiscovery = new DeviceDiscovery();
async aboutToAppear(): Promise<void> {
await this.discovery.init();
this.devices = this.discovery.getTrustedDevices();
}
aboutToDisappear(): void {
this.discovery.release();
}
build() {
Column() {
Text('Select Device')
.fontSize(20)
.fontWeight(FontWeight.Bold)
List() {
ForEach(this.devices, (device: distributedDeviceManager.DeviceBasicInfo) => {
ListItem() {
Row() {
Image(this.getDeviceIcon(device.deviceType))
.width(32)
.height(32)
Text(device.deviceName)
.margin({ left: 12 })
Blank()
if (this.selectedDevice?.deviceId === device.deviceId) {
Image($r('app.media.check'))
}
}
.width('100%')
.padding(16)
.onClick(() => {
this.selectedDevice = device;
})
}
})
}
}
}
getDeviceIcon(type: distributedDeviceManager.DeviceType): Resource {
switch (type) {
case distributedDeviceManager.DeviceType.PHONE:
return $r('app.media.phone');
case distributedDeviceManager.DeviceType.TABLET:
return $r('app.media.tablet');
case distributedDeviceManager.DeviceType.TV:
return $r('app.media.tv');
default:
return $r('app.media.device');
}
}
}Distributed Data
Distributed KV Store
import { distributedKVStore } from '@kit.ArkData';
class DistributedStorage {
private kvManager: distributedKVStore.KVManager | null = null;
private kvStore: distributedKVStore.SingleKVStore | null = null;
async init(context: common.UIAbilityContext): Promise<void> {
const config: distributedKVStore.KVManagerConfig = {
bundleName: 'com.example.app',
context: context
};
this.kvManager = distributedKVStore.createKVManager(config);
const options: distributedKVStore.Options = {
createIfMissing: true,
encrypt: false,
backup: false,
autoSync: true, // Auto sync across devices
kvStoreType: distributedKVStore.KVStoreType.SINGLE_VERSION,
securityLevel: distributedKVStore.SecurityLevel.S1
};
this.kvStore = await this.kvManager.getKVStore('shared_store', options);
}
// Put data
async put(key: string, value: string | number | boolean): Promise<void> {
await this.kvStore?.put(key, value);
}
// Get data
async get(key: string): Promise<string | number | boolean | null> {
try {
return await this.kvStore?.get(key);
} catch {
return null;
}
}
// Delete data
async delete(key: string): Promise<void> {
await this.kvStore?.delete(key);
}
// Subscribe to changes
subscribeToChanges(callback: (changes: distributedKVStore.ChangeNotification) => void): void {
this.kvStore?.on('dataChange', distributedKVStore.SubscribeType.SUBSCRIBE_TYPE_ALL, callback);
}
// Manual sync
async sync(deviceIds: string[]): Promise<void> {
await this.kvStore?.sync(deviceIds, distributedKVStore.SyncMode.PUSH_PULL);
}
close(): void {
this.kvManager?.closeKVStore('shared_store');
}
}Usage in Component
@Component
struct SyncedNotes {
@State notes: string = '';
private storage: DistributedStorage = new DistributedStorage();
async aboutToAppear(): Promise<void> {
const context = getContext(this) as common.UIAbilityContext;
await this.storage.init(context);
// Load existing notes
const savedNotes = await this.storage.get('notes');
if (savedNotes) {
this.notes = savedNotes as string;
}
// Subscribe to changes from other devices
this.storage.subscribeToChanges((changes) => {
for (const entry of changes.insertEntries) {
if (entry.key === 'notes') {
this.notes = entry.value.value as string;
}
}
for (const entry of changes.updateEntries) {
if (entry.key === 'notes') {
this.notes = entry.value.value as string;
}
}
});
}
build() {
Column() {
TextArea({ text: this.notes })
.width('100%')
.height(300)
.onChange((value: string) => {
this.notes = value;
this.storage.put('notes', value);
})
Text('Changes sync automatically to all devices')
.fontSize(12)
.fontColor('#888888')
}
}
}Distributed Objects
Creating Distributed Object
import { distributedDataObject } from '@kit.ArkData';
interface GameState {
score: number;
level: number;
playerPosition: { x: number; y: number };
}
class DistributedGameState {
private dataObject: distributedDataObject.DataObject | null = null;
private state: GameState = {
score: 0,
level: 1,
playerPosition: { x: 0, y: 0 }
};
async init(context: common.UIAbilityContext): Promise<void> {
this.dataObject = distributedDataObject.create(context, this.state);
// Set session ID for sync
await this.dataObject.setSessionId('game_session_001');
// Watch for changes
this.dataObject.on('change', (sessionId: string, fields: string[]) => {
console.info(`Fields changed: ${fields.join(', ')}`);
this.onStateChanged(fields);
});
this.dataObject.on('status', (sessionId: string, networkId: string, status: string) => {
console.info(`Sync status: ${status}`);
});
}
updateScore(score: number): void {
if (this.dataObject) {
(this.dataObject as Object)['score'] = score;
}
}
updatePosition(x: number, y: number): void {
if (this.dataObject) {
(this.dataObject as Object)['playerPosition'] = { x, y };
}
}
private onStateChanged(fields: string[]): void {
// Handle state changes from other devices
}
async leave(): Promise<void> {
await this.dataObject?.setSessionId('');
}
}Cross-Device Call
Starting Remote Ability
import { common, Want } from '@kit.AbilityKit';
class RemoteAbilityLauncher {
private context: common.UIAbilityContext;
constructor(context: common.UIAbilityContext) {
this.context = context;
}
// Start ability on remote device
async startRemoteAbility(deviceId: string): Promise<void> {
const want: Want = {
deviceId: deviceId,
bundleName: 'com.example.app',
abilityName: 'PlayerAbility',
parameters: {
videoUrl: 'https://example.com/video.mp4',
startPosition: 120
}
};
await this.context.startAbility(want);
}
// Start and get result from remote device
async startRemoteForResult(deviceId: string): Promise<common.AbilityResult> {
const want: Want = {
deviceId: deviceId,
bundleName: 'com.example.picker',
abilityName: 'FilePickerAbility'
};
return await this.context.startAbilityForResult(want);
}
// Connect to remote service
async connectRemoteService(deviceId: string): Promise<void> {
const want: Want = {
deviceId: deviceId,
bundleName: 'com.example.app',
abilityName: 'ComputeService'
};
const connection: common.ConnectOptions = {
onConnect: (elementName, remoteProxy) => {
console.info('Connected to remote service');
// Use remoteProxy to call remote methods
},
onDisconnect: (elementName) => {
console.info('Disconnected from remote service');
},
onFailed: (code) => {
console.error(`Connection failed: ${code}`);
}
};
const connectionId = this.context.connectServiceExtensionAbility(want, connection);
}
}Multi-Screen Collaboration
@Component
struct MultiScreenApp {
@State isRemoteDisplayActive: boolean = false;
private context = getContext(this) as common.UIAbilityContext;
build() {
Column() {
if (this.isRemoteDisplayActive) {
// Controller UI (on phone)
this.ControllerView()
} else {
// Full content (on this device)
this.ContentView()
}
Button('Cast to TV')
.onClick(() => this.castToTV())
}
}
@Builder
ControllerView() {
Column() {
Text('Playing on TV')
Row() {
Button('◀')
Button('⏸')
Button('▶')
}
}
}
@Builder
ContentView() {
// Video player content
}
async castToTV(): Promise<void> {
// Get TV device
const deviceManager = distributedDeviceManager.createDeviceManager('com.example.app');
const devices = deviceManager.getAvailableDeviceListSync();
const tvDevice = devices.find(d =>
d.deviceType === distributedDeviceManager.DeviceType.TV
);
if (tvDevice) {
const want: Want = {
deviceId: tvDevice.deviceId,
bundleName: 'com.example.app',
abilityName: 'PlayerAbility',
parameters: {
videoUrl: this.currentVideoUrl
}
};
await this.context.startAbility(want);
this.isRemoteDisplayActive = true;
}
}
}Ability Continuation
Enabling Continuation
// MainAbility.ets
import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit';
export default class MainAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// Check if this is a continuation
if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
// Restore state from continuation
this.restoreFromContinuation(want);
}
}
// Prepare data for continuation
onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult {
// Save current state
wantParam['currentPage'] = 'detail';
wantParam['articleId'] = '123';
wantParam['scrollPosition'] = 450;
return AbilityConstant.OnContinueResult.AGREE;
}
private restoreFromContinuation(want: Want): void {
const params = want.parameters;
if (params) {
const currentPage = params['currentPage'] as string;
const articleId = params['articleId'] as string;
const scrollPosition = params['scrollPosition'] as number;
// Navigate to saved state
router.pushUrl({
url: `pages/${currentPage}`,
params: { id: articleId, scroll: scrollPosition }
});
}
}
}Configuration
// module.json5
{
"module": {
"abilities": [{
"name": "MainAbility",
"continuable": true,
"launchType": "singleton"
}]
}
}Triggering Continuation
@Component
struct ContinuationDemo {
private context = getContext(this) as common.UIAbilityContext;
async continueToDevice(deviceId: string): Promise<void> {
// Continuation will trigger onContinue callback
await this.context.continueAbility({
deviceId: deviceId,
bundleName: 'com.example.app',
abilityName: 'MainAbility'
});
}
build() {
Button('Continue on Tablet')
.onClick(async () => {
const devices = await this.getAvailableDevices();
const tablet = devices.find(d =>
d.deviceType === distributedDeviceManager.DeviceType.TABLET
);
if (tablet) {
await this.continueToDevice(tablet.deviceId);
}
})
}
}Distributed File System
Sharing Files Across Devices
import { fileIo } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
class DistributedFileManager {
private context: common.UIAbilityContext;
constructor(context: common.UIAbilityContext) {
this.context = context;
}
// Get distributed file path
getDistributedPath(relativePath: string): string {
return `${this.context.distributedFilesDir}/${relativePath}`;
}
// Write file (will sync to other devices)
async writeDistributedFile(relativePath: string, content: string): Promise<void> {
const filePath = this.getDistributedPath(relativePath);
const file = fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
try {
fileIo.writeSync(file.fd, content);
} finally {
fileIo.closeSync(file);
}
}
// Read file (may come from another device)
async readDistributedFile(relativePath: string): Promise<string> {
const filePath = this.getDistributedPath(relativePath);
const file = fileIo.openSync(filePath, fileIo.OpenMode.READ_ONLY);
try {
const stat = fileIo.statSync(filePath);
const buffer = new ArrayBuffer(stat.size);
fileIo.readSync(file.fd, buffer);
return String.fromCharCode(...new Uint8Array(buffer));
} finally {
fileIo.closeSync(file);
}
}
// List distributed files
listDistributedFiles(): string[] {
const dir = this.context.distributedFilesDir;
return fileIo.listFileSync(dir);
}
}Best Practices
Security Considerations
// ✅ Good: Validate device trust before sharing sensitive data
async shareData(deviceId: string, data: SensitiveData): Promise<void> {
const device = this.getTrustedDevice(deviceId);
if (!device) {
throw new Error('Device not trusted');
}
// Only share with authenticated devices
if (device.authForm === distributedDeviceManager.AuthForm.IDENTICAL_ACCOUNT) {
await this.secureShare(deviceId, data);
}
}
// ✅ Good: Use appropriate security level
const options: distributedKVStore.Options = {
securityLevel: distributedKVStore.SecurityLevel.S3, // High security
encrypt: true // Encrypt data
};Error Handling
// ✅ Good: Handle network and device errors gracefully
async startRemoteAbility(deviceId: string): Promise<void> {
try {
await this.context.startAbility(want);
} catch (err) {
const error = err as BusinessError;
switch (error.code) {
case 16000050:
// Device offline
this.showToast('Device is not available');
break;
case 16000051:
// Network error
this.showToast('Network connection failed');
break;
case 16000001:
// Ability not found
this.showToast('App not installed on target device');
break;
default:
this.showToast('Failed to connect to device');
}
}
}Performance
// ✅ Good: Batch updates for distributed objects
updateGameState(updates: Partial<GameState>): void {
// Batch multiple changes
const dataObject = this.dataObject as Object;
Object.entries(updates).forEach(([key, value]) => {
dataObject[key] = value;
});
// Changes will be synced together
}
// ✅ Good: Use appropriate sync modes
async syncData(urgent: boolean): Promise<void> {
if (urgent) {
// Push immediately
await this.kvStore.sync(deviceIds, distributedKVStore.SyncMode.PUSH);
} else {
// Let system decide when to sync
await this.kvStore.sync(deviceIds, distributedKVStore.SyncMode.PUSH_PULL);
}
}State Management
// ✅ Good: Handle continuation state properly
@Component
struct ContinuableComponent {
@State articleContent: string = '';
@State scrollOffset: number = 0;
// Save state for continuation
getContinuationState(): Record<string, Object> {
return {
'articleContent': this.articleContent,
'scrollOffset': this.scrollOffset
};
}
// Restore state after continuation
restoreContinuationState(state: Record<string, Object>): void {
this.articleContent = state['articleContent'] as string;
this.scrollOffset = state['scrollOffset'] as number;
}
}Common Patterns
Device Handoff
// Phone to tablet handoff for video watching
class VideoHandoff {
async handoffToTablet(): Promise<void> {
const currentPosition = this.videoPlayer.getCurrentPosition();
const videoUrl = this.videoPlayer.getVideoUrl();
// Find tablet
const tablet = await this.findTablet();
if (!tablet) return;
// Start player on tablet with current position
const want: Want = {
deviceId: tablet.deviceId,
bundleName: 'com.example.video',
abilityName: 'PlayerAbility',
parameters: {
videoUrl: videoUrl,
startPosition: currentPosition
}
};
await this.context.startAbility(want);
// Pause local playback
this.videoPlayer.pause();
}
}Collaborative Editing
// Real-time document collaboration
class CollaborativeDocument {
private distributedObject: distributedDataObject.DataObject;
async init(): Promise<void> {
const document = {
content: '',
lastEditor: '',
version: 0
};
this.distributedObject = distributedDataObject.create(this.context, document);
await this.distributedObject.setSessionId('doc_session');
this.distributedObject.on('change', (_, fields) => {
if (fields.includes('content')) {
this.onContentChanged();
}
});
}
updateContent(content: string, editor: string): void {
const doc = this.distributedObject as Object;
doc['content'] = content;
doc['lastEditor'] = editor;
doc['version'] = (doc['version'] as number) + 1;
}
private onContentChanged(): void {
// Update UI with new content
const doc = this.distributedObject as Object;
this.refreshContent(doc['content'] as string);
}
}harmonyos-app Extended Reference
This file preserves detailed material moved out of SKILL.md for progressive disclosure. Load it only when the current task needs the specific examples, commands, templates, or checklists below.
Moved content starts at: ## State Management Patterns.
State Management Patterns
ViewModel Pattern
// viewmodel/ProductViewModel.ets
import { Product } from '../model/Product';
import { ProductRepository } from '../repository/ProductRepository';
@Observed
export class ProductViewModel {
products: Product[] = [];
isLoading: boolean = false;
errorMessage: string = '';
private repository: ProductRepository = new ProductRepository();
async loadProducts(): Promise<void> {
this.isLoading = true;
this.errorMessage = '';
try {
this.products = await this.repository.getProducts();
} catch (error) {
this.errorMessage = `Failed to load: ${error.message}`;
} finally {
this.isLoading = false;
}
}
async addProduct(product: Product): Promise<void> {
const created = await this.repository.createProduct(product);
this.products = [...this.products, created];
}
}
// pages/ProductPage.ets
@Entry
@Component
struct ProductPage {
@State viewModel: ProductViewModel = new ProductViewModel();
aboutToAppear(): void {
this.viewModel.loadProducts();
}
build() {
Column() {
if (this.viewModel.isLoading) {
LoadingProgress()
} else if (this.viewModel.errorMessage) {
Text(this.viewModel.errorMessage)
.fontColor(Color.Red)
} else {
ForEach(this.viewModel.products, (product: Product) => {
ProductCard({ product: product })
}, (product: Product) => product.id)
}
}
}
}AppStorage for Global State
// Initialize in EntryAbility
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// Initialize global state
AppStorage.setOrCreate('isLoggedIn', false);
AppStorage.setOrCreate('currentUser', null);
AppStorage.setOrCreate('theme', 'light');
}
}
// Access in components
@Entry
@Component
struct ProfilePage {
@StorageLink('isLoggedIn') isLoggedIn: boolean = false;
@StorageLink('currentUser') currentUser: User | null = null;
@StorageProp('theme') theme: string = 'light'; // Read-only
build() {
Column() {
if (this.isLoggedIn && this.currentUser) {
Text(`Welcome, ${this.currentUser.name}`)
} else {
Button('Login')
.onClick(() => {
// After login
this.isLoggedIn = true;
this.currentUser = { id: '1', name: 'John' };
})
}
}
}
}PersistentStorage for Preferences
// Initialize persistent storage
PersistentStorage.persistProp('userSettings', {
notifications: true,
darkMode: false,
language: 'zh-CN'
});
@Entry
@Component
struct SettingsPage {
@StorageLink('userSettings') settings: UserSettings = {
notifications: true,
darkMode: false,
language: 'zh-CN'
};
build() {
Column() {
Toggle({ type: ToggleType.Switch, isOn: this.settings.notifications })
.onChange((isOn: boolean) => {
this.settings = { ...this.settings, notifications: isOn };
})
Toggle({ type: ToggleType.Switch, isOn: this.settings.darkMode })
.onChange((isOn: boolean) => {
this.settings = { ...this.settings, darkMode: isOn };
})
}
}
}---
Navigation Patterns
Router Navigation
import { router } from '@kit.ArkUI';
// Navigate to page
router.pushUrl({
url: 'pages/Detail',
params: { productId: '123' }
});
// Navigate with result
router.pushUrl({
url: 'pages/SelectAddress'
}).then(() => {
// Navigation complete
});
// Get params in target page
@Entry
@Component
struct DetailPage {
@State productId: string = '';
aboutToAppear(): void {
const params = router.getParams() as Record<string, string>;
this.productId = params?.productId ?? '';
}
}
// Go back
router.back();
// Replace current page
router.replaceUrl({ url: 'pages/Home' });
// Clear stack and navigate
router.clear();
router.pushUrl({ url: 'pages/Login' });Navigation Component (Recommended for HarmonyOS NEXT)
@Entry
@Component
struct MainPage {
@Provide('navPathStack') navPathStack: NavPathStack = new NavPathStack();
@Builder
pageBuilder(name: string, params: object) {
if (name === 'detail') {
DetailPage({ params: params as DetailParams })
} else if (name === 'settings') {
SettingsPage()
}
}
build() {
Navigation(this.navPathStack) {
Column() {
Button('Go to Detail')
.onClick(() => {
this.navPathStack.pushPath({ name: 'detail', param: { id: '123' } });
})
}
}
.navDestination(this.pageBuilder)
.title('Home')
}
}
@Component
struct DetailPage {
@Consume('navPathStack') navPathStack: NavPathStack;
params: DetailParams = { id: '' };
build() {
NavDestination() {
Column() {
Text(`Product ID: ${this.params.id}`)
Button('Back')
.onClick(() => this.navPathStack.pop())
}
}
.title('Detail')
}
}---
Network Requests
HTTP Client
import { http } from '@kit.NetworkKit';
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
class HttpClient {
private baseUrl: string = 'https://api.example.com';
async get<T>(path: string): Promise<T> {
const httpRequest = http.createHttp();
try {
const response = await httpRequest.request(
`${this.baseUrl}${path}`,
{
method: http.RequestMethod.GET,
header: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${await this.getToken()}`
},
expectDataType: http.HttpDataType.OBJECT
}
);
if (response.responseCode === 200) {
const result = response.result as ApiResponse<T>;
if (result.code === 0) {
return result.data;
}
throw new Error(result.message);
}
throw new Error(`HTTP ${response.responseCode}`);
} finally {
httpRequest.destroy();
}
}
async post<T, R>(path: string, data: T): Promise<R> {
const httpRequest = http.createHttp();
try {
const response = await httpRequest.request(
`${this.baseUrl}${path}`,
{
method: http.RequestMethod.POST,
header: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${await this.getToken()}`
},
extraData: JSON.stringify(data),
expectDataType: http.HttpDataType.OBJECT
}
);
const result = response.result as ApiResponse<R>;
return result.data;
} finally {
httpRequest.destroy();
}
}
private async getToken(): Promise<string> {
return AppStorage.get('authToken') ?? '';
}
}
export const httpClient = new HttpClient();---
Distributed Capabilities
Cross-Device Data Sync
import { distributedKVStore } from '@kit.ArkData';
class DistributedStore {
private kvManager: distributedKVStore.KVManager | null = null;
private kvStore: distributedKVStore.SingleKVStore | null = null;
async init(context: Context): Promise<void> {
const config: distributedKVStore.KVManagerConfig = {
context: context,
bundleName: 'com.example.myapp'
};
this.kvManager = distributedKVStore.createKVManager(config);
const options: distributedKVStore.Options = {
createIfMissing: true,
encrypt: false,
backup: false,
autoSync: true, // Auto sync across devices
kvStoreType: distributedKVStore.KVStoreType.SINGLE_VERSION,
securityLevel: distributedKVStore.SecurityLevel.S1
};
this.kvStore = await this.kvManager.getKVStore('myStore', options);
}
async put(key: string, value: string): Promise<void> {
await this.kvStore?.put(key, value);
}
async get(key: string): Promise<string | null> {
try {
return await this.kvStore?.get(key) as string;
} catch {
return null;
}
}
// Subscribe to changes from other devices
subscribe(callback: (key: string, value: string) => void): void {
this.kvStore?.on('dataChange', distributedKVStore.SubscribeType.SUBSCRIBE_TYPE_ALL,
(data: distributedKVStore.ChangeNotification) => {
for (const entry of data.insertEntries) {
callback(entry.key, entry.value.value as string);
}
for (const entry of data.updateEntries) {
callback(entry.key, entry.value.value as string);
}
}
);
}
}Device Discovery and Connection
import { distributedDeviceManager } from '@kit.DistributedServiceKit';
class DeviceManager {
private deviceManager: distributedDeviceManager.DeviceManager | null = null;
async init(context: Context): Promise<void> {
this.deviceManager = distributedDeviceManager.createDeviceManager(
context.applicationInfo.name
);
}
getAvailableDevices(): distributedDeviceManager.DeviceBasicInfo[] {
return this.deviceManager?.getAvailableDeviceListSync() ?? [];
}
startDiscovery(): void {
const filter: distributedDeviceManager.DiscoveryFilter = {
discoverMode: distributedDeviceManager.DiscoverMode.DISCOVER_MODE_PASSIVE
};
this.deviceManager?.startDiscovering(filter);
this.deviceManager?.on('discoverSuccess', (data) => {
console.info(`Found device: ${data.device.deviceName}`);
});
}
stopDiscovery(): void {
this.deviceManager?.stopDiscovering();
}
}---
Multi-Device Adaptation
Responsive Layout
import { BreakpointSystem, BreakPointType } from '../utils/BreakpointSystem';
@Entry
@Component
struct AdaptivePage {
@StorageProp('currentBreakpoint') currentBreakpoint: string = 'sm';
build() {
GridRow({
columns: { sm: 4, md: 8, lg: 12 },
gutter: { x: 12, y: 12 }
}) {
GridCol({ span: { sm: 4, md: 4, lg: 3 } }) {
// Sidebar - full width on phone, 1/3 on tablet, 1/4 on desktop
Sidebar()
}
GridCol({ span: { sm: 4, md: 4, lg: 9 } }) {
// Content - full width on phone, 2/3 on tablet, 3/4 on desktop
MainContent()
}
}
}
}
// Breakpoint system
export class BreakpointSystem {
private static readonly BREAKPOINTS: Record<string, number> = {
'sm': 320, // Phone
'md': 600, // Foldable/Tablet
'lg': 840 // Desktop/TV
};
static register(context: UIContext): void {
context.getMediaQuery().matchMediaSync('(width >= 840vp)').on('change', (result) => {
AppStorage.setOrCreate('currentBreakpoint', result.matches ? 'lg' : 'md');
});
context.getMediaQuery().matchMediaSync('(width >= 600vp)').on('change', (result) => {
if (!result.matches) {
AppStorage.setOrCreate('currentBreakpoint', 'sm');
}
});
}
}---
Testing
Unit Testing
import { describe, it, expect, beforeEach } from '@ohos/hypium';
import { ProductViewModel } from '../viewmodel/ProductViewModel';
export default function ProductViewModelTest() {
describe('ProductViewModel', () => {
let viewModel: ProductViewModel;
beforeEach(() => {
viewModel = new ProductViewModel();
});
it('should load products successfully', async () => {
await viewModel.loadProducts();
expect(viewModel.products.length).assertLarger(0);
expect(viewModel.isLoading).assertFalse();
expect(viewModel.errorMessage).assertEqual('');
});
it('should add product to list', async () => {
const initialCount = viewModel.products.length;
const newProduct: Product = { id: 'test', name: 'Test Product', price: 99 };
await viewModel.addProduct(newProduct);
expect(viewModel.products.length).assertEqual(initialCount + 1);
});
});
}UI Testing
import { describe, it, expect } from '@ohos/hypium';
import { Driver, ON } from '@ohos.UiTest';
export default function ProductPageUITest() {
describe('ProductPage UI', () => {
it('should display product list', async () => {
const driver = Driver.create();
await driver.delayMs(1000);
// Find and verify list exists
const list = await driver.findComponent(ON.type('List'));
expect(list).not().assertNull();
// Verify list items
const items = await driver.findComponents(ON.type('ListItem'));
expect(items.length).assertLarger(0);
});
it('should navigate to detail on tap', async () => {
const driver = Driver.create();
// Find first product card
const card = await driver.findComponent(ON.type('ProductCard'));
await card.click();
await driver.delayMs(500);
// Verify navigation to detail page
const detailTitle = await driver.findComponent(ON.text('Product Detail'));
expect(detailTitle).not().assertNull();
});
});
}---
Checklist
## Project Setup
- [ ] Stage model used (not FA model)
- [ ] module.json5 properly configured
- [ ] Permissions declared in module.json5
- [ ] Resource files organized (strings, images)
## Code Quality
- [ ] No `any` types in codebase
- [ ] All state decorated with proper decorators
- [ ] No direct mutation of @State objects
- [ ] Components extracted for reusability
- [ ] Lifecycle methods used appropriately
## UI/UX
- [ ] LazyForEach used for long lists
- [ ] Loading states implemented
- [ ] Error handling with user feedback
- [ ] Multi-device layouts with GridRow/GridCol
- [ ] Accessibility attributes added
## State Management
- [ ] Clear state ownership (component vs global)
- [ ] @Observed/@ObjectLink for nested objects
- [ ] PersistentStorage for user preferences
- [ ] AppStorage for app-wide state
## Performance
- [ ] Images optimized and cached
- [ ] Unnecessary re-renders avoided
- [ ] Network requests with proper error handling
- [ ] Background tasks properly managed
## Testing
- [ ] Unit tests for ViewModels
- [ ] UI tests for critical flows
- [ ] Edge cases covered---
See Also
- arkts.md — ArkTS language guide and restrictions
- arkui.md — ArkUI components and styling
- stage-model.md — Stage model architecture
- distributed.md — Distributed capabilities guide
- project-template.md — Project template
Stage Model Architecture
Stage Model is the application model for HarmonyOS 3.1+, providing structured lifecycle management and component-based architecture.
Core Concepts
Application Components
┌─────────────────────────────────────────────────────┐
│ AbilityStage │
│ (Application-level lifecycle, shared resources) │
├─────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────┐ │
│ │ UIAbility │ │ UIAbility │ │ Extension │ │
│ │ (Page 1) │ │ (Page 2) │ │ Ability │ │
│ └──────────────┘ └──────────────┘ └───────────┘ │
│ │
└─────────────────────────────────────────────────────┘| Component | Purpose | Example |
|---|---|---|
| AbilityStage | Application entry, global lifecycle | Initialize app, load resources |
| UIAbility | UI page container | Main page, settings page |
| ExtensionAbility | Background services | Widget, notification, share |
| WindowStage | Window management | Multi-window, split screen |
AbilityStage
Implementation
// entry/src/main/ets/AbilityStage.ets
import { AbilityStage, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
export default class MyAbilityStage extends AbilityStage {
onCreate(): void {
// Called when application starts
hilog.info(0x0000, 'AbilityStage', 'onCreate');
// Initialize global resources
this.initializeApp();
}
onAcceptWant(want: Want): string {
// Handle incoming want
// Return ability name to launch
if (want.action === 'share') {
return 'ShareAbility';
}
return 'MainAbility';
}
private initializeApp(): void {
// Initialize services, database, etc.
}
}Configuration
// module.json5
{
"module": {
"name": "entry",
"type": "entry",
"srcEntry": "./ets/AbilityStage.ets",
"abilities": [
{
"name": "MainAbility",
"srcEntry": "./ets/abilities/MainAbility.ets",
"launchType": "singleton",
"exported": true
}
]
}
}UIAbility
Lifecycle
┌─────────────────────────────────────────────────────┐
│ UIAbility Lifecycle │
├─────────────────────────────────────────────────────┤
│ │
│ onCreate() ──► onWindowStageCreate() ──► onForeground()
│ │ │ │
│ │ │ ▼
│ │ │ (User Interaction)
│ │ │ │
│ │ │ ▼
│ onDestroy() ◄── onWindowStageDestroy() ◄── onBackground()
│ │
└─────────────────────────────────────────────────────┘Implementation
// entry/src/main/ets/abilities/MainAbility.ets
import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = 'MainAbility';
export default class MainAbility extends UIAbility {
private windowStage: window.WindowStage | null = null;
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(0x0000, TAG, 'onCreate');
// Handle launch parameters
const action = want.action;
const uri = want.uri;
if (action === 'deeplink') {
this.handleDeepLink(uri);
}
}
onWindowStageCreate(windowStage: window.WindowStage): void {
hilog.info(0x0000, TAG, 'onWindowStageCreate');
this.windowStage = windowStage;
// Load main page
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(0x0000, TAG, 'Failed to load content: %{public}s', JSON.stringify(err));
return;
}
hilog.info(0x0000, TAG, 'Content loaded successfully');
});
// Configure window
this.configureWindow(windowStage);
}
onForeground(): void {
hilog.info(0x0000, TAG, 'onForeground');
// App enters foreground
// Resume tasks, refresh data
}
onBackground(): void {
hilog.info(0x0000, TAG, 'onBackground');
// App enters background
// Save state, pause non-critical tasks
}
onWindowStageDestroy(): void {
hilog.info(0x0000, TAG, 'onWindowStageDestroy');
// Release UI resources
}
onDestroy(): void {
hilog.info(0x0000, TAG, 'onDestroy');
// Cleanup resources
}
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// Called when singleton ability receives new launch request
hilog.info(0x0000, TAG, 'onNewWant');
this.handleIntent(want);
}
private configureWindow(windowStage: window.WindowStage): void {
const win = windowStage.getMainWindowSync();
// Set status bar
win.setWindowLayoutFullScreen(true);
win.setWindowSystemBarEnable(['status', 'navigation']);
// Set colors
const sysBarProps: window.SystemBarProperties = {
statusBarColor: '#FFFFFF',
navigationBarColor: '#FFFFFF',
statusBarContentColor: '#000000'
};
win.setWindowSystemBarProperties(sysBarProps);
}
private handleDeepLink(uri: string | undefined): void {
if (!uri) return;
// Parse and handle deep link
}
private handleIntent(want: Want): void {
// Handle intent routing
}
}Launch Types
Singleton (Default)
// module.json5
{
"abilities": [{
"name": "MainAbility",
"launchType": "singleton"
}]
}- Only one instance exists
onNewWant()called for subsequent launches- Suitable for main entry, settings
Standard
{
"abilities": [{
"name": "DetailAbility",
"launchType": "standard"
}]
}- New instance for each launch
- Multiple instances can exist
- Suitable for detail pages, editors
Specified
{
"abilities": [{
"name": "DocumentAbility",
"launchType": "specified"
}]
}- Instance determined by key
- Same key reuses instance
- Suitable for document editing
// Launching specified ability
const want: Want = {
bundleName: 'com.example.app',
abilityName: 'DocumentAbility',
parameters: {
'instanceKey': 'document_123'
}
};Page Navigation
Router Navigation
import { router } from '@kit.ArkUI';
// Navigate to page
router.pushUrl({
url: 'pages/Detail',
params: {
id: '123',
title: 'Product Detail'
}
});
// Replace current page
router.replaceUrl({
url: 'pages/Login'
});
// Go back
router.back();
// Go back with result
router.back({
url: 'pages/List',
params: { refresh: true }
});
// Clear and navigate
router.clear();
router.pushUrl({ url: 'pages/Home' });Receiving Parameters
import { router } from '@kit.ArkUI';
@Entry
@Component
struct DetailPage {
@State id: string = '';
@State title: string = '';
aboutToAppear(): void {
const params = router.getParams() as Record<string, string>;
this.id = params?.id ?? '';
this.title = params?.title ?? '';
}
build() {
Column() {
Text(this.title)
// Page content
}
}
}Navigation Component
import { Navigation, NavPathStack } from '@kit.ArkUI';
@Entry
@Component
struct MainPage {
@Provide('navStack') navStack: NavPathStack = new NavPathStack();
build() {
Navigation(this.navStack) {
// Root content
HomeContent()
}
.navDestination(this.PageBuilder)
.mode(NavigationMode.Stack)
}
@Builder
PageBuilder(name: string, params: Object): void {
if (name === 'detail') {
DetailPage({ params: params as DetailParams })
} else if (name === 'settings') {
SettingsPage()
}
}
}
@Component
struct HomeContent {
@Consume('navStack') navStack: NavPathStack;
build() {
Column() {
Button('Go to Detail')
.onClick(() => {
this.navStack.pushPath({
name: 'detail',
param: { id: '123' }
});
})
}
}
}Context Usage
Getting Context
import { common, UIAbility } from '@kit.AbilityKit';
// In UIAbility
class MyAbility extends UIAbility {
onCreate(): void {
const context = this.context;
// Use context
}
}
// In Component (via getContext)
@Component
struct MyComponent {
private context = getContext(this) as common.UIAbilityContext;
aboutToAppear(): void {
// Access context
const filesDir = this.context.filesDir;
}
}Context Capabilities
import { common } from '@kit.AbilityKit';
@Component
struct ContextDemo {
private context = getContext(this) as common.UIAbilityContext;
// File paths
getFilePaths(): void {
const filesDir = this.context.filesDir; // App files
const cacheDir = this.context.cacheDir; // Cache
const tempDir = this.context.tempDir; // Temporary
const databaseDir = this.context.databaseDir; // Database
}
// Start another ability
async startAbility(): Promise<void> {
const want: Want = {
bundleName: 'com.example.target',
abilityName: 'TargetAbility'
};
await this.context.startAbility(want);
}
// Start ability for result
async startForResult(): Promise<void> {
const want: Want = {
bundleName: 'com.example.picker',
abilityName: 'ImagePickerAbility'
};
const result = await this.context.startAbilityForResult(want);
if (result.resultCode === 0) {
const imageUri = result.want?.uri;
}
}
// Terminate self
terminateSelf(): void {
this.context.terminateSelf();
}
// Terminate with result
terminateWithResult(): void {
const result: common.AbilityResult = {
resultCode: 0,
want: {
parameters: { selectedId: '123' }
}
};
this.context.terminateSelfWithResult(result);
}
}Extension Abilities
Widget Extension
// entry/src/main/ets/formability/FormAbility.ets
import { FormExtensionAbility, formBindingData, formInfo } from '@kit.FormKit';
export default class FormAbility extends FormExtensionAbility {
onAddForm(want: Want): formBindingData.FormBindingData {
const formData: Record<string, string> = {
'title': 'Widget Title',
'content': 'Widget Content'
};
return formBindingData.createFormBindingData(formData);
}
onUpdateForm(formId: string): void {
// Update widget data
const formData: Record<string, string> = {
'title': 'Updated Title'
};
const bindingData = formBindingData.createFormBindingData(formData);
formProvider.updateForm(formId, bindingData);
}
onRemoveForm(formId: string): void {
// Cleanup when widget removed
}
}Service Extension
// For background tasks
import { ServiceExtensionAbility, Want } from '@kit.AbilityKit';
export default class BackgroundService extends ServiceExtensionAbility {
onCreate(want: Want): void {
// Initialize service
}
onRequest(want: Want, startId: number): void {
// Handle service request
}
onDestroy(): void {
// Cleanup
}
}State Persistence
Preferences
import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
class PreferencesManager {
private prefs: preferences.Preferences | null = null;
private context: common.UIAbilityContext;
constructor(context: common.UIAbilityContext) {
this.context = context;
}
async init(): Promise<void> {
this.prefs = await preferences.getPreferences(this.context, 'app_prefs');
}
async set(key: string, value: preferences.ValueType): Promise<void> {
if (!this.prefs) return;
await this.prefs.put(key, value);
await this.prefs.flush();
}
async get<T extends preferences.ValueType>(key: string, defaultValue: T): Promise<T> {
if (!this.prefs) return defaultValue;
return await this.prefs.get(key, defaultValue) as T;
}
async remove(key: string): Promise<void> {
if (!this.prefs) return;
await this.prefs.delete(key);
await this.prefs.flush();
}
}Application State
// Using AppStorage for app-wide state
AppStorage.setOrCreate('isLoggedIn', false);
AppStorage.setOrCreate('userId', '');
// Access in components
@Component
struct ProfilePage {
@StorageLink('isLoggedIn') isLoggedIn: boolean = false;
@StorageLink('userId') userId: string = '';
build() {
Column() {
if (this.isLoggedIn) {
Text(`User: ${this.userId}`)
} else {
Text('Please login')
}
}
}
}
// Persist to disk
PersistentStorage.persistProp('isLoggedIn', false);
PersistentStorage.persistProp('userId', '');Best Practices
Lifecycle Management
// ✅ Good: Proper lifecycle handling
export default class MainAbility extends UIAbility {
onCreate(): void {
// Initialize only essential resources
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// Load UI, initialize view-related resources
}
onForeground(): void {
// Resume operations, refresh data
this.refreshData();
}
onBackground(): void {
// Pause operations, save state
this.saveState();
this.pauseNonCriticalTasks();
}
onDestroy(): void {
// Full cleanup
this.releaseResources();
}
}Memory Management
// ✅ Good: Release resources properly
@Component
struct ResourceAwarePage {
private subscription: Subscription | null = null;
aboutToAppear(): void {
this.subscription = eventBus.subscribe('event', this.handleEvent);
}
aboutToDisappear(): void {
// Always unsubscribe
this.subscription?.unsubscribe();
this.subscription = null;
}
handleEvent = (data: EventData): void => {
// Handle event
}
}Navigation Patterns
// ✅ Good: Use NavPathStack for complex navigation
@Entry
@Component
struct App {
@Provide('navStack') navStack: NavPathStack = new NavPathStack();
build() {
Navigation(this.navStack) {
// Content
}
.navDestination(this.routeBuilder)
}
@Builder
routeBuilder(name: string): void {
// Centralized route handling
}
}
// ❌ Bad: Direct router calls scattered everywhere
// Hard to track navigation flow, no type safetyHarmonyOS Project Template
Directory Structure
MyApp/
├── AppScope/
│ ├── app.json5 # Application configuration
│ └── resources/
│ └── base/
│ └── element/
│ └── string.json # App-level strings
├── entry/
│ ├── src/
│ │ └── main/
│ │ ├── ets/
│ │ │ ├── AbilityStage.ets # Application entry
│ │ │ ├── abilities/
│ │ │ │ └── MainAbility.ets # Main UIAbility
│ │ │ ├── pages/
│ │ │ │ ├── Index.ets # Entry page
│ │ │ │ ├── Home.ets
│ │ │ │ ├── Discover.ets
│ │ │ │ └── Profile.ets
│ │ │ ├── components/ # Reusable components
│ │ │ │ ├── common/
│ │ │ │ │ ├── Header.ets
│ │ │ │ │ ├── Footer.ets
│ │ │ │ │ └── Loading.ets
│ │ │ │ └── business/
│ │ │ │ ├── UserCard.ets
│ │ │ │ └── ProductCard.ets
│ │ │ ├── viewmodels/ # State management
│ │ │ │ ├── UserViewModel.ets
│ │ │ │ └── ProductViewModel.ets
│ │ │ ├── models/ # Data models
│ │ │ │ ├── User.ets
│ │ │ │ └── Product.ets
│ │ │ ├── services/ # Business logic
│ │ │ │ ├── UserService.ets
│ │ │ │ └── ProductService.ets
│ │ │ ├── network/ # Network layer
│ │ │ │ ├── HttpClient.ets
│ │ │ │ └── ApiService.ets
│ │ │ ├── utils/ # Utilities
│ │ │ │ ├── Logger.ets
│ │ │ │ └── Constants.ets
│ │ │ └── common/ # Shared types
│ │ │ └── Types.ets
│ │ └── resources/
│ │ ├── base/
│ │ │ ├── element/
│ │ │ │ ├── string.json
│ │ │ │ └── color.json
│ │ │ ├── media/
│ │ │ └── profile/
│ │ │ └── main_pages.json
│ │ ├── en_US/ # English resources
│ │ └── zh_CN/ # Chinese resources
│ └── module.json5 # Module configuration
├── oh_modules/ # Dependencies
├── build-profile.json5 # Build configuration
└── oh-package.json5 # Package configurationConfiguration Files
app.json5
{
"app": {
"bundleName": "com.example.myapp",
"vendor": "example",
"versionCode": 1000000,
"versionName": "1.0.0",
"icon": "$media:app_icon",
"label": "$string:app_name"
}
}module.json5
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "MainAbility",
"deviceTypes": ["phone", "tablet"],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "MainAbility",
"srcEntry": "./ets/abilities/MainAbility.ets",
"description": "$string:MainAbility_desc",
"icon": "$media:icon",
"label": "$string:MainAbility_label",
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["action.system.home"]
}
]
}
],
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
}
}main_pages.json
{
"src": [
"pages/Index",
"pages/Home",
"pages/Discover",
"pages/Profile"
]
}Core Files
AbilityStage.ets
import { AbilityStage, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = 'AbilityStage';
const DOMAIN = 0x0000;
export default class MyAbilityStage extends AbilityStage {
onCreate(): void {
hilog.info(DOMAIN, TAG, 'AbilityStage onCreate');
}
onAcceptWant(want: Want): string {
return 'MainAbility';
}
}MainAbility.ets
import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = 'MainAbility';
const DOMAIN = 0x0000;
export default class MainAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(DOMAIN, TAG, 'Ability onCreate');
}
onDestroy(): void {
hilog.info(DOMAIN, TAG, 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
hilog.info(DOMAIN, TAG, 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err, data) => {
if (err.code) {
hilog.error(DOMAIN, TAG, 'Failed to load content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
hilog.info(DOMAIN, TAG, 'Succeeded in loading content. Data: %{public}s', JSON.stringify(data) ?? '');
});
}
onWindowStageDestroy(): void {
hilog.info(DOMAIN, TAG, 'Ability onWindowStageDestroy');
}
onForeground(): void {
hilog.info(DOMAIN, TAG, 'Ability onForeground');
}
onBackground(): void {
hilog.info(DOMAIN, TAG, 'Ability onBackground');
}
}Index.ets (Entry Page with Tab Navigation)
import { router } from '@kit.ArkUI';
@Entry
@Component
struct Index {
@State currentIndex: number = 0;
private tabController: TabsController = new TabsController();
@Builder
TabBuilder(title: string, targetIndex: number, selectedImg: Resource, normalImg: Resource) {
Column() {
Image(this.currentIndex === targetIndex ? selectedImg : normalImg)
.width(24)
.height(24)
Text(title)
.fontSize(12)
.fontColor(this.currentIndex === targetIndex ? '#007AFF' : '#8E8E93')
.margin({ top: 4 })
}
.width('100%')
.height(56)
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.currentIndex = targetIndex;
this.tabController.changeIndex(targetIndex);
})
}
build() {
Tabs({ barPosition: BarPosition.End, controller: this.tabController }) {
TabContent() {
HomeTab()
}
.tabBar(this.TabBuilder('Home', 0, $r('app.media.home_selected'), $r('app.media.home')))
TabContent() {
DiscoverTab()
}
.tabBar(this.TabBuilder('Discover', 1, $r('app.media.discover_selected'), $r('app.media.discover')))
TabContent() {
ProfileTab()
}
.tabBar(this.TabBuilder('Profile', 2, $r('app.media.profile_selected'), $r('app.media.profile')))
}
.barMode(BarMode.Fixed)
.onChange((index: number) => {
this.currentIndex = index;
})
}
}
@Component
struct HomeTab {
build() {
Column() {
Text('Home')
.fontSize(24)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
@Component
struct DiscoverTab {
build() {
Column() {
Text('Discover')
.fontSize(24)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
@Component
struct ProfileTab {
build() {
Column() {
Text('Profile')
.fontSize(24)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}Model Template
User.ets
export interface User {
id: string;
name: string;
email: string;
avatar: string;
createdAt: number;
}
export class UserModel implements User {
id: string = '';
name: string = '';
email: string = '';
avatar: string = '';
createdAt: number = 0;
constructor(data?: Partial<User>) {
if (data) {
this.id = data.id ?? '';
this.name = data.name ?? '';
this.email = data.email ?? '';
this.avatar = data.avatar ?? '';
this.createdAt = data.createdAt ?? 0;
}
}
static fromJSON(json: Object): UserModel {
return new UserModel(json as Partial<User>);
}
}ViewModel Template
UserViewModel.ets
import { User, UserModel } from '../models/User';
import { UserService } from '../services/UserService';
@Observed
export class UserViewModel {
user: User | null = null;
isLoading: boolean = false;
errorMessage: string = '';
private userService: UserService = new UserService();
async loadUser(userId: string): Promise<void> {
this.isLoading = true;
this.errorMessage = '';
try {
this.user = await this.userService.getUser(userId);
} catch (error) {
this.errorMessage = (error as Error).message;
} finally {
this.isLoading = false;
}
}
async updateUser(updates: Partial<User>): Promise<void> {
if (!this.user) return;
this.isLoading = true;
try {
this.user = await this.userService.updateUser(this.user.id, updates);
} catch (error) {
this.errorMessage = (error as Error).message;
} finally {
this.isLoading = false;
}
}
}Service Template
UserService.ets
import { User, UserModel } from '../models/User';
import { ApiService } from '../network/ApiService';
export class UserService {
private api: ApiService = new ApiService();
async getUser(userId: string): Promise<User> {
const response = await this.api.get<User>(`/users/${userId}`);
return UserModel.fromJSON(response);
}
async updateUser(userId: string, updates: Partial<User>): Promise<User> {
const response = await this.api.put<User>(`/users/${userId}`, updates);
return UserModel.fromJSON(response);
}
async deleteUser(userId: string): Promise<void> {
await this.api.delete(`/users/${userId}`);
}
}Network Template
HttpClient.ets
import { http } from '@kit.NetworkKit';
export interface HttpResponse<T> {
code: number;
data: T;
message: string;
}
export class HttpClient {
private baseUrl: string;
private timeout: number = 30000;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async request<T>(
method: http.RequestMethod,
path: string,
data?: Object
): Promise<T> {
const httpRequest = http.createHttp();
try {
const response = await httpRequest.request(
`${this.baseUrl}${path}`,
{
method: method,
header: {
'Content-Type': 'application/json'
},
extraData: data ? JSON.stringify(data) : undefined,
connectTimeout: this.timeout,
readTimeout: this.timeout
}
);
if (response.responseCode >= 200 && response.responseCode < 300) {
const result = JSON.parse(response.result as string) as HttpResponse<T>;
return result.data;
} else {
throw new Error(`HTTP Error: ${response.responseCode}`);
}
} finally {
httpRequest.destroy();
}
}
async get<T>(path: string): Promise<T> {
return this.request<T>(http.RequestMethod.GET, path);
}
async post<T>(path: string, data: Object): Promise<T> {
return this.request<T>(http.RequestMethod.POST, path, data);
}
async put<T>(path: string, data: Object): Promise<T> {
return this.request<T>(http.RequestMethod.PUT, path, data);
}
async delete(path: string): Promise<void> {
await this.request<void>(http.RequestMethod.DELETE, path);
}
}ApiService.ets
import { HttpClient } from './HttpClient';
const BASE_URL = 'https://api.example.com/v1';
export class ApiService extends HttpClient {
constructor() {
super(BASE_URL);
}
}Component Template
UserCard.ets
import { User } from '../../models/User';
@Component
export struct UserCard {
@Prop user: User = {} as User;
onTap: () => void = () => {};
build() {
Row() {
Image(this.user.avatar || $r('app.media.default_avatar'))
.width(48)
.height(48)
.borderRadius(24)
Column() {
Text(this.user.name)
.fontSize(16)
.fontWeight(FontWeight.Medium)
Text(this.user.email)
.fontSize(14)
.fontColor('#8E8E93')
.margin({ top: 4 })
}
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
Blank()
Image($r('app.media.arrow_right'))
.width(16)
.height(16)
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
.onClick(() => this.onTap())
}
}Utility Templates
Logger.ets
import { hilog } from '@kit.PerformanceAnalysisKit';
const DOMAIN = 0x0000;
export class Logger {
private tag: string;
constructor(tag: string) {
this.tag = tag;
}
debug(message: string, ...args: Object[]): void {
hilog.debug(DOMAIN, this.tag, message, ...args);
}
info(message: string, ...args: Object[]): void {
hilog.info(DOMAIN, this.tag, message, ...args);
}
warn(message: string, ...args: Object[]): void {
hilog.warn(DOMAIN, this.tag, message, ...args);
}
error(message: string, ...args: Object[]): void {
hilog.error(DOMAIN, this.tag, message, ...args);
}
}Constants.ets
export class Constants {
// API
static readonly API_BASE_URL = 'https://api.example.com/v1';
static readonly API_TIMEOUT = 30000;
// Storage Keys
static readonly KEY_USER_TOKEN = 'user_token';
static readonly KEY_USER_ID = 'user_id';
static readonly KEY_THEME = 'app_theme';
// UI
static readonly ANIMATION_DURATION = 300;
static readonly PAGE_SIZE = 20;
// Colors
static readonly COLOR_PRIMARY = '#007AFF';
static readonly COLOR_SECONDARY = '#5856D6';
static readonly COLOR_SUCCESS = '#34C759';
static readonly COLOR_WARNING = '#FF9500';
static readonly COLOR_ERROR = '#FF3B30';
}Resource Templates
string.json
{
"string": [
{
"name": "app_name",
"value": "MyApp"
},
{
"name": "MainAbility_label",
"value": "MyApp"
},
{
"name": "MainAbility_desc",
"value": "Main application ability"
},
{
"name": "btn_submit",
"value": "Submit"
},
{
"name": "btn_cancel",
"value": "Cancel"
},
{
"name": "error_network",
"value": "Network error. Please try again."
}
]
}color.json
{
"color": [
{
"name": "start_window_background",
"value": "#FFFFFF"
},
{
"name": "primary",
"value": "#007AFF"
},
{
"name": "background",
"value": "#F2F2F7"
},
{
"name": "text_primary",
"value": "#1C1C1E"
},
{
"name": "text_secondary",
"value": "#8E8E93"
}
]
}Related skills
FAQ
What platforms does harmonyos-app target?
harmonyos-app targets HarmonyOS mobile development using ArkUI for screens, lifecycle management, permissions, and device APIs. The skill is for client-side feature work on Huawei's HarmonyOS stack, not iOS or Android.
What does harmonyos-app help implement?
harmonyos-app helps implement ArkUI screens, application lifecycle hooks, permission configurations, and device API calls during active HarmonyOS feature development. Output includes scaffolded project structure and client-side integration patterns.