HarmonyOS Application Development Core Principles ArkTS First — Use ArkTS with strict type safety, no any or 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
No 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 configuration
Layer 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
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()) } }
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
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
// 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
// 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
class HttpClient { private baseUrl: string = 'https://api.example.com';
async get
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
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
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
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
async get(key: string): Promise
// 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
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
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
anytypes 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 reference/arkts.md — ArkTS language guide and restrictions reference/arkui.md — ArkUI components and styling reference/stage-model.md — Stage model architecture reference/distributed.md — Distributed capabilities guide templates/project-structure.md — Project template