From ab3e0b5590f5c6149a75aa6db59bdd227cc1e431 Mon Sep 17 00:00:00 2001 From: CHE LIANG ZHAO Date: Thu, 22 Jan 2026 12:51:22 +0800 Subject: [PATCH] feat: arkts-development skills --- arkts-development/SKILL.md | 154 ++++++ .../assets/component-template.ets | 57 ++ .../assets/list-page-template.ets | 129 +++++ arkts-development/references/api-reference.md | 503 +++++++++++++++++ .../references/component-patterns.md | 521 ++++++++++++++++++ .../references/migration-guide.md | 407 ++++++++++++++ 6 files changed, 1771 insertions(+) create mode 100644 arkts-development/SKILL.md create mode 100644 arkts-development/assets/component-template.ets create mode 100644 arkts-development/assets/list-page-template.ets create mode 100644 arkts-development/references/api-reference.md create mode 100644 arkts-development/references/component-patterns.md create mode 100644 arkts-development/references/migration-guide.md diff --git a/arkts-development/SKILL.md b/arkts-development/SKILL.md new file mode 100644 index 0000000..fe7719e --- /dev/null +++ b/arkts-development/SKILL.md @@ -0,0 +1,154 @@ +--- +name: arkts-development +description: HarmonyOS ArkTS application development with ArkUI declarative UI framework. Use when building HarmonyOS/OpenHarmony apps, creating ArkUI components, implementing state management with decorators (@State, @Prop, @Link), migrating from TypeScript to ArkTS, or working with HarmonyOS-specific APIs (router, http, preferences). Covers component lifecycle, layout patterns, and ArkTS language constraints. +--- + +# ArkTS Development + +Build HarmonyOS applications using ArkTS and the ArkUI declarative UI framework. + +## Quick Start + +Create a basic component: + +```typescript +@Entry +@Component +struct HelloWorld { + @State message: string = 'Hello, ArkTS!'; + + build() { + Column() { + Text(this.message) + .fontSize(30) + .fontWeight(FontWeight.Bold) + Button('Click Me') + .onClick(() => { this.message = 'Button Clicked!'; }) + } + .width('100%') + .height('100%') + .justifyContent(FlexAlign.Center) + } +} +``` + +## State Management Decorators + +| Decorator | Usage | Description | +|-----------|-------|-------------| +| `@State` | `@State count: number = 0` | Component internal state | +| `@Prop` | `@Prop title: string` | Parent → Child (one-way) | +| `@Link` | `@Link value: number` | Parent ↔ Child (two-way, use `$varName`) | +| `@Provide/@Consume` | Cross-level | Ancestor → Descendant | +| `@Observed/@ObjectLink` | Nested objects | Deep object observation | + +## Common Layouts + +```typescript +// Vertical +Column({ space: 10 }) { Text('A'); Text('B'); } + .alignItems(HorizontalAlign.Center) + +// Horizontal +Row({ space: 10 }) { Text('A'); Text('B'); } + .justifyContent(FlexAlign.SpaceBetween) + +// Stack (overlay) +Stack({ alignContent: Alignment.Center }) { + Image($r('app.media.bg')) + Text('Overlay') +} + +// List with ForEach +List({ space: 10 }) { + ForEach(this.items, (item: string) => { + ListItem() { Text(item) } + }, (item: string) => item) +} +``` + +## Component Lifecycle + +```typescript +@Entry +@Component +struct Page { + aboutToAppear() { /* Init data */ } + onPageShow() { /* Page visible */ } + onPageHide() { /* Page hidden */ } + aboutToDisappear() { /* Cleanup */ } + build() { Column() { Text('Page') } } +} +``` + +## Navigation + +```typescript +import { router } from '@kit.ArkUI'; + +// Push +router.pushUrl({ url: 'pages/Detail', params: { id: 123 } }); + +// Replace +router.replaceUrl({ url: 'pages/New' }); + +// Back +router.back(); + +// Get params +const params = router.getParams() as Record; +``` + +## Network Request + +```typescript +import { http } from '@kit.NetworkKit'; + +const req = http.createHttp(); +const res = await req.request('https://api.example.com/data', { + method: http.RequestMethod.GET, + header: { 'Content-Type': 'application/json' } +}); +if (res.responseCode === 200) { + const data = JSON.parse(res.result as string); +} +req.destroy(); +``` + +## Local Storage + +```typescript +import { preferences } from '@kit.ArkData'; + +const prefs = await preferences.getPreferences(this.context, 'store'); +await prefs.put('key', 'value'); +await prefs.flush(); +const val = await prefs.get('key', 'default'); +``` + +## ArkTS Language Constraints + +ArkTS enforces stricter rules than TypeScript for performance and safety: + +| Prohibited | Use Instead | +|------------|-------------| +| `any`, `unknown` | Explicit types, interfaces | +| `var` | `let`, `const` | +| Dynamic property access `obj['key']` | Fixed object structure | +| `for...in`, `delete`, `with` | `for...of`, array methods | +| `#privateField` | `private` keyword | +| Structural typing | Explicit `implements`/`extends` | + +See [references/migration-guide.md](references/migration-guide.md) for complete TypeScript → ArkTS migration details. + +## Reference Files + +- **Migration Guide**: [references/migration-guide.md](references/migration-guide.md) - Complete TypeScript to ArkTS migration rules and examples +- **Component Patterns**: [references/component-patterns.md](references/component-patterns.md) - Advanced component patterns and best practices +- **API Reference**: [references/api-reference.md](references/api-reference.md) - Common HarmonyOS APIs + +## Development Environment + +- **IDE**: DevEco Studio +- **SDK**: HarmonyOS SDK +- **Simulator**: Built-in DevEco Studio emulator diff --git a/arkts-development/assets/component-template.ets b/arkts-development/assets/component-template.ets new file mode 100644 index 0000000..9f9ff43 --- /dev/null +++ b/arkts-development/assets/component-template.ets @@ -0,0 +1,57 @@ +// ArkTS Basic Component Template +// Replace {{ComponentName}} with your component name + +@Entry +@Component +struct {{ComponentName}} { + // State management + @State isLoading: boolean = false; + @State errorMessage: string = ''; + + // Lifecycle + aboutToAppear(): void { + this.loadData(); + } + + aboutToDisappear(): void { + // Cleanup resources + } + + // Methods + async loadData(): Promise { + this.isLoading = true; + try { + // Load data here + } catch (error) { + this.errorMessage = 'Failed to load data'; + } finally { + this.isLoading = false; + } + } + + // Build + build() { + Column() { + if (this.isLoading) { + LoadingProgress() + .width(50) + .height(50) + } else if (this.errorMessage) { + Text(this.errorMessage) + .fontSize(16) + .fontColor(Color.Red) + Button('Retry') + .onClick(() => { this.loadData(); }) + } else { + // Main content here + Text('{{ComponentName}}') + .fontSize(24) + .fontWeight(FontWeight.Bold) + } + } + .width('100%') + .height('100%') + .justifyContent(FlexAlign.Center) + .alignItems(HorizontalAlign.Center) + } +} diff --git a/arkts-development/assets/list-page-template.ets b/arkts-development/assets/list-page-template.ets new file mode 100644 index 0000000..a647630 --- /dev/null +++ b/arkts-development/assets/list-page-template.ets @@ -0,0 +1,129 @@ +// ArkTS List Page Template +// Replace {{PageName}}, {{ItemType}}, {{itemName}} with your values + +import { router } from '@kit.ArkUI'; + +interface {{ItemType}} { + id: string; + title: string; + description: string; +} + +@Entry +@Component +struct {{PageName}} { + @State items: {{ItemType}}[] = []; + @State isLoading: boolean = false; + @State isRefreshing: boolean = false; + + aboutToAppear(): void { + this.loadItems(); + } + + async loadItems(): Promise { + this.isLoading = true; + try { + // TODO: Fetch items from API + this.items = [ + { id: '1', title: 'Item 1', description: 'Description 1' }, + { id: '2', title: 'Item 2', description: 'Description 2' }, + ]; + } finally { + this.isLoading = false; + } + } + + async refreshItems(): Promise { + this.isRefreshing = true; + await this.loadItems(); + this.isRefreshing = false; + } + + navigateToDetail(item: {{ItemType}}): void { + router.pushUrl({ + url: 'pages/{{PageName}}Detail', + params: { id: item.id } + }); + } + + @Builder + ItemCard(item: {{ItemType}}) { + Column() { + Text(item.title) + .fontSize(18) + .fontWeight(FontWeight.Medium) + .fontColor('#333333') + + Text(item.description) + .fontSize(14) + .fontColor('#666666') + .margin({ top: 4 }) + } + .alignItems(HorizontalAlign.Start) + .padding(16) + .width('100%') + .backgroundColor(Color.White) + .borderRadius(8) + .onClick(() => { this.navigateToDetail(item); }) + } + + build() { + Column() { + // Header + Row() { + Text('{{PageName}}') + .fontSize(24) + .fontWeight(FontWeight.Bold) + + Blank() + + Button('Add') + .onClick(() => { + router.pushUrl({ url: 'pages/{{PageName}}Create' }); + }) + } + .width('100%') + .padding(16) + + // Content + if (this.isLoading && this.items.length === 0) { + Column() { + LoadingProgress().width(50).height(50) + Text('Loading...').margin({ top: 16 }) + } + .width('100%') + .layoutWeight(1) + .justifyContent(FlexAlign.Center) + } else if (this.items.length === 0) { + Column() { + Text('No items found') + .fontSize(16) + .fontColor('#999999') + Button('Refresh') + .margin({ top: 16 }) + .onClick(() => { this.loadItems(); }) + } + .width('100%') + .layoutWeight(1) + .justifyContent(FlexAlign.Center) + } else { + Refresh({ refreshing: $$this.isRefreshing }) { + List({ space: 12 }) { + ForEach(this.items, (item: {{ItemType}}) => { + ListItem() { + this.ItemCard(item) + } + }, (item: {{ItemType}}) => item.id) + } + .width('100%') + .padding({ left: 16, right: 16 }) + } + .onRefreshing(() => { this.refreshItems(); }) + .layoutWeight(1) + } + } + .width('100%') + .height('100%') + .backgroundColor('#f5f5f5') + } +} diff --git a/arkts-development/references/api-reference.md b/arkts-development/references/api-reference.md new file mode 100644 index 0000000..dd7747a --- /dev/null +++ b/arkts-development/references/api-reference.md @@ -0,0 +1,503 @@ +# HarmonyOS API Reference + +Common HarmonyOS APIs for ArkTS development. + +## Table of Contents + +1. [Router Navigation](#router-navigation) +2. [HTTP Networking](#http-networking) +3. [Preferences Storage](#preferences-storage) +4. [File Operations](#file-operations) +5. [Device Info](#device-info) +6. [Prompt & Dialog](#prompt--dialog) +7. [Media](#media) + +--- + +## Router Navigation + +```typescript +import { router } from '@kit.ArkUI'; +``` + +### Push Page + +```typescript +router.pushUrl({ + url: 'pages/DetailPage', + params: { + id: 123, + title: 'Detail' + } +}); +``` + +### Push with Mode + +```typescript +// Standard mode (default) - adds to stack +router.pushUrl({ + url: 'pages/Page', +}, router.RouterMode.Standard); + +// Single mode - reuses if exists +router.pushUrl({ + url: 'pages/Page', +}, router.RouterMode.Single); +``` + +### Replace Page + +```typescript +router.replaceUrl({ + url: 'pages/NewPage' +}); +``` + +### Back Navigation + +```typescript +// Back to previous +router.back(); + +// Back to specific page +router.back({ + url: 'pages/HomePage' +}); + +// Back with result +router.back({ + url: 'pages/HomePage', + params: { result: 'success' } +}); +``` + +### Get Parameters + +```typescript +// In target page +aboutToAppear(): void { + const params = router.getParams() as Record; + if (params) { + const id = params['id'] as number; + const title = params['title'] as string; + } +} +``` + +### Get Router State + +```typescript +const state = router.getState(); +console.log('Current page:', state.name); +console.log('Page path:', state.path); +console.log('Stack index:', state.index); +``` + +### Clear Router Stack + +```typescript +router.clear(); +``` + +--- + +## HTTP Networking + +```typescript +import { http } from '@kit.NetworkKit'; +``` + +### GET Request + +```typescript +async function getData(): Promise { + const httpRequest = http.createHttp(); + + try { + const response = await httpRequest.request( + 'https://api.example.com/data', + { + method: http.RequestMethod.GET, + header: { + 'Content-Type': 'application/json' + }, + connectTimeout: 60000, + readTimeout: 60000 + } + ); + + if (response.responseCode === 200) { + const data = JSON.parse(response.result as string); + console.log('Data:', JSON.stringify(data)); + } + } catch (error) { + console.error('Request failed:', error); + } finally { + httpRequest.destroy(); + } +} +``` + +### POST Request + +```typescript +async function postData(body: object): Promise { + const httpRequest = http.createHttp(); + + try { + const response = await httpRequest.request( + 'https://api.example.com/submit', + { + method: http.RequestMethod.POST, + header: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer token' + }, + extraData: JSON.stringify(body) + } + ); + + console.log('Response code:', response.responseCode); + } finally { + httpRequest.destroy(); + } +} +``` + +### Request Options + +```typescript +interface HttpRequestOptions { + method: http.RequestMethod; // GET, POST, PUT, DELETE, etc. + header?: Object; // Request headers + extraData?: string | Object; // Request body + connectTimeout?: number; // Connection timeout (ms) + readTimeout?: number; // Read timeout (ms) + expectDataType?: http.HttpDataType; +} +``` + +--- + +## Preferences Storage + +```typescript +import { preferences } from '@kit.ArkData'; +``` + +### Get Preferences Instance + +```typescript +// In component with context +const dataPreferences = await preferences.getPreferences( + this.context, + 'myPreferencesStore' +); +``` + +### Write Data + +```typescript +await dataPreferences.put('username', 'John'); +await dataPreferences.put('age', 25); +await dataPreferences.put('isVip', true); +await dataPreferences.put('scores', [90, 85, 92]); +await dataPreferences.flush(); // Persist to disk +``` + +### Read Data + +```typescript +// With default values +const username = await dataPreferences.get('username', '') as string; +const age = await dataPreferences.get('age', 0) as number; +const isVip = await dataPreferences.get('isVip', false) as boolean; +``` + +### Check Key Exists + +```typescript +const hasKey = await dataPreferences.has('username'); +``` + +### Delete Data + +```typescript +await dataPreferences.delete('username'); +await dataPreferences.flush(); +``` + +### Clear All + +```typescript +await dataPreferences.clear(); +await dataPreferences.flush(); +``` + +### Delete Preferences File + +```typescript +await preferences.deletePreferences(this.context, 'myPreferencesStore'); +``` + +--- + +## File Operations + +```typescript +import { fileIo as fs } from '@kit.CoreFileKit'; +``` + +### Get Application Paths + +```typescript +// In AbilityStage or UIAbility +const filesDir = this.context.filesDir; // /data/app/.../files +const cacheDir = this.context.cacheDir; // /data/app/.../cache +const tempDir = this.context.tempDir; // /data/app/.../temp +``` + +### Write File + +```typescript +const filePath = `${this.context.filesDir}/data.txt`; +const file = fs.openSync(filePath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY); +fs.writeSync(file.fd, 'Hello, HarmonyOS!'); +fs.closeSync(file); +``` + +### Read File + +```typescript +const filePath = `${this.context.filesDir}/data.txt`; +const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY); +const buffer = new ArrayBuffer(4096); +const readLen = fs.readSync(file.fd, buffer); +const content = String.fromCharCode(...new Uint8Array(buffer.slice(0, readLen))); +fs.closeSync(file); +``` + +### Check File Exists + +```typescript +const exists = fs.accessSync(filePath); +``` + +### Delete File + +```typescript +fs.unlinkSync(filePath); +``` + +### List Directory + +```typescript +const files = fs.listFileSync(this.context.filesDir); +files.forEach((file: string) => { + console.log('File:', file); +}); +``` + +--- + +## Device Info + +```typescript +import { deviceInfo } from '@kit.BasicServicesKit'; +``` + +### Get Device Information + +```typescript +const brand = deviceInfo.brand; // e.g., "HUAWEI" +const model = deviceInfo.productModel; // e.g., "Mate 60" +const osVersion = deviceInfo.osFullName; // e.g., "HarmonyOS 5.0" +const sdkVersion = deviceInfo.sdkApiVersion; // e.g., 12 +const deviceType = deviceInfo.deviceType; // e.g., "phone", "tablet" +``` + +--- + +## Prompt & Dialog + +```typescript +import { promptAction } from '@kit.ArkUI'; +``` + +### Toast + +```typescript +promptAction.showToast({ + message: 'Operation successful', + duration: 2000, + bottom: 80 +}); +``` + +### Alert Dialog + +```typescript +promptAction.showDialog({ + title: 'Confirm', + message: 'Are you sure you want to delete?', + buttons: [ + { text: 'Cancel', color: '#999999' }, + { text: 'Delete', color: '#FF0000' } + ] +}).then((result) => { + if (result.index === 1) { + // Delete confirmed + } +}); +``` + +### Action Sheet + +```typescript +promptAction.showActionMenu({ + title: 'Select Option', + buttons: [ + { text: 'Camera', color: '#000000' }, + { text: 'Gallery', color: '#000000' }, + { text: 'Cancel', color: '#999999' } + ] +}).then((result) => { + switch (result.index) { + case 0: // Camera + break; + case 1: // Gallery + break; + } +}); +``` + +### Custom Dialog + +```typescript +@CustomDialog +struct ConfirmDialog { + controller: CustomDialogController; + title: string = ''; + onConfirm: () => void = () => {}; + + build() { + Column() { + Text(this.title).fontSize(20).margin({ bottom: 20 }) + Row({ space: 20 }) { + Button('Cancel') + .onClick(() => { this.controller.close(); }) + Button('Confirm') + .onClick(() => { + this.onConfirm(); + this.controller.close(); + }) + } + } + .padding(20) + } +} + +// Usage in component +@Entry +@Component +struct DialogExample { + dialogController: CustomDialogController = new CustomDialogController({ + builder: ConfirmDialog({ + title: 'Delete Item?', + onConfirm: () => { this.handleDelete(); } + }), + autoCancel: true + }); + + handleDelete(): void { + // Delete logic + } + + build() { + Button('Show Dialog') + .onClick(() => { this.dialogController.open(); }) + } +} +``` + +--- + +## Media + +### Image Picker + +```typescript +import { photoAccessHelper } from '@kit.MediaLibraryKit'; +import { picker } from '@kit.CoreFileKit'; + +async function pickImage(): Promise { + const photoPicker = new picker.PhotoViewPicker(); + + try { + const result = await photoPicker.select({ + MIMEType: picker.PhotoViewMIMETypes.IMAGE_TYPE, + maxSelectNumber: 1 + }); + + if (result.photoUris.length > 0) { + return result.photoUris[0]; + } + } catch (error) { + console.error('Pick image failed:', error); + } + + return null; +} +``` + +### Camera Capture + +```typescript +import { camera } from '@kit.CameraKit'; + +// Request camera permission first +// Then use camera APIs for capture +``` + +### Audio Playback + +```typescript +import { media } from '@kit.MediaKit'; + +async function playAudio(uri: string): Promise { + const player = await media.createAVPlayer(); + + player.on('stateChange', (state: string) => { + if (state === 'prepared') { + player.play(); + } + }); + + player.url = uri; +} +``` + +--- + +## Common Import Patterns + +```typescript +// UI Kit +import { router, promptAction } from '@kit.ArkUI'; + +// Network Kit +import { http } from '@kit.NetworkKit'; + +// Data Kit +import { preferences } from '@kit.ArkData'; + +// File Kit +import { fileIo as fs, picker } from '@kit.CoreFileKit'; + +// Basic Services +import { deviceInfo } from '@kit.BasicServicesKit'; + +// Media Kit +import { media } from '@kit.MediaKit'; +``` diff --git a/arkts-development/references/component-patterns.md b/arkts-development/references/component-patterns.md new file mode 100644 index 0000000..864e6c8 --- /dev/null +++ b/arkts-development/references/component-patterns.md @@ -0,0 +1,521 @@ +# ArkUI Component Patterns + +Advanced component patterns and best practices for ArkTS development. + +## Table of Contents + +1. [Component Structure](#component-structure) +2. [State Management Patterns](#state-management-patterns) +3. [Parent-Child Communication](#parent-child-communication) +4. [List Optimization](#list-optimization) +5. [Custom Components](#custom-components) +6. [Conditional Rendering](#conditional-rendering) + +--- + +## Component Structure + +### Basic Component + +```typescript +@Component +struct MyComponent { + // Private properties + private readonly TAG: string = 'MyComponent'; + + // State properties + @State isLoading: boolean = false; + + // Props from parent + @Prop title: string = ''; + + // Lifecycle + aboutToAppear(): void { + console.log(this.TAG, 'aboutToAppear'); + } + + // Build method (required) + build() { + Column() { + Text(this.title) + } + } +} +``` + +### Entry Component (Page) + +```typescript +@Entry +@Component +struct HomePage { + @State currentTab: number = 0; + + onPageShow(): void { + // Called when page becomes visible + } + + onPageHide(): void { + // Called when page becomes hidden + } + + onBackPress(): boolean { + // Return true to prevent default back behavior + return false; + } + + build() { + Navigation() { + // Page content + } + .title('Home') + } +} +``` + +--- + +## State Management Patterns + +### @State - Component Internal State + +```typescript +@Component +struct Counter { + @State count: number = 0; + + build() { + Column() { + Text(`Count: ${this.count}`) + Button('Increment') + .onClick(() => { this.count++; }) + } + } +} +``` + +### @Prop - One-Way Binding (Parent → Child) + +```typescript +// Child component +@Component +struct DisplayCard { + @Prop title: string = ''; + @Prop value: number = 0; + + build() { + Column() { + Text(this.title).fontSize(16) + Text(`${this.value}`).fontSize(24) + } + } +} + +// Parent component +@Entry +@Component +struct Dashboard { + @State temperature: number = 25; + + build() { + Column() { + DisplayCard({ title: 'Temperature', value: this.temperature }) + Button('Update') + .onClick(() => { this.temperature++; }) + } + } +} +``` + +### @Link - Two-Way Binding + +```typescript +// Child component +@Component +struct EditableInput { + @Link inputValue: string; + + build() { + TextInput({ text: this.inputValue }) + .onChange((value: string) => { + this.inputValue = value; + }) + } +} + +// Parent component +@Entry +@Component +struct FormPage { + @State username: string = ''; + + build() { + Column() { + Text(`Username: ${this.username}`) + EditableInput({ inputValue: $username }) // Note: $ prefix + } + } +} +``` + +### @Provide/@Consume - Cross-Level State + +```typescript +// Ancestor component +@Entry +@Component +struct App { + @Provide('theme') theme: string = 'light'; + + build() { + Column() { + SettingsPage() + Button('Toggle Theme') + .onClick(() => { + this.theme = this.theme === 'light' ? 'dark' : 'light'; + }) + } + } +} + +// Descendant component (any depth) +@Component +struct ThemedCard { + @Consume('theme') theme: string; + + build() { + Column() { + Text('Card Content') + .fontColor(this.theme === 'light' ? Color.Black : Color.White) + } + .backgroundColor(this.theme === 'light' ? Color.White : Color.Black) + } +} +``` + +### @Observed/@ObjectLink - Nested Object Observation + +```typescript +// Observable class +@Observed +class Task { + id: number; + title: string; + completed: boolean; + + constructor(id: number, title: string) { + this.id = id; + this.title = title; + this.completed = false; + } +} + +// Child component with object link +@Component +struct TaskItem { + @ObjectLink task: Task; + + build() { + Row() { + Checkbox() + .select(this.task.completed) + .onChange((value: boolean) => { + this.task.completed = value; + }) + Text(this.task.title) + .decoration({ + type: this.task.completed ? TextDecorationType.LineThrough : TextDecorationType.None + }) + } + } +} + +// Parent component +@Entry +@Component +struct TaskList { + @State tasks: Task[] = [ + new Task(1, 'Buy groceries'), + new Task(2, 'Read book') + ]; + + build() { + List() { + ForEach(this.tasks, (task: Task) => { + ListItem() { + TaskItem({ task: task }) + } + }, (task: Task) => task.id.toString()) + } + } +} +``` + +### @StorageLink/@StorageProp - AppStorage Binding + +```typescript +// Initialize in EntryAbility +AppStorage.setOrCreate('userToken', ''); +AppStorage.setOrCreate('isLoggedIn', false); + +// Component with storage binding +@Entry +@Component +struct ProfilePage { + @StorageLink('userToken') token: string = ''; // Two-way + @StorageProp('isLoggedIn') isLoggedIn: boolean = false; // One-way + + build() { + Column() { + if (this.isLoggedIn) { + Text('Welcome!') + Button('Logout') + .onClick(() => { + this.token = ''; + AppStorage.set('isLoggedIn', false); + }) + } else { + Text('Please login') + } + } + } +} +``` + +--- + +## Parent-Child Communication + +### Events via Callback + +```typescript +// Child component +@Component +struct SearchBar { + private onSearch: (query: string) => void = () => {}; + @State query: string = ''; + + build() { + Row() { + TextInput({ placeholder: 'Search...' }) + .onChange((value: string) => { this.query = value; }) + Button('Search') + .onClick(() => { this.onSearch(this.query); }) + } + } +} + +// Parent component +@Entry +@Component +struct SearchPage { + @State results: string[] = []; + + handleSearch(query: string): void { + // Perform search + this.results = [`Result for: ${query}`]; + } + + build() { + Column() { + SearchBar({ onSearch: (q: string) => this.handleSearch(q) }) + ForEach(this.results, (item: string) => { + Text(item) + }) + } + } +} +``` + +--- + +## List Optimization + +### LazyForEach for Large Lists + +```typescript +// Data source implementing IDataSource +class MyDataSource implements IDataSource { + private data: string[] = []; + private listeners: DataChangeListener[] = []; + + constructor(data: string[]) { + this.data = data; + } + + totalCount(): number { + return this.data.length; + } + + getData(index: number): string { + return this.data[index]; + } + + registerDataChangeListener(listener: DataChangeListener): void { + this.listeners.push(listener); + } + + unregisterDataChangeListener(listener: DataChangeListener): void { + const idx = this.listeners.indexOf(listener); + if (idx >= 0) { + this.listeners.splice(idx, 1); + } + } +} + +// Component with LazyForEach +@Entry +@Component +struct LargeList { + private dataSource: MyDataSource = new MyDataSource( + Array.from({ length: 10000 }, (_, i) => `Item ${i}`) + ); + + build() { + List() { + LazyForEach(this.dataSource, (item: string, index: number) => { + ListItem() { + Text(item).fontSize(16).padding(10) + } + }, (item: string) => item) + } + .cachedCount(5) // Number of items to cache + } +} +``` + +### ForEach Key Function + +```typescript +// Always provide a unique key function +ForEach(this.items, (item: Item) => { + ListItem() { ItemCard({ item: item }) } +}, (item: Item) => item.id.toString()) // Unique key +``` + +--- + +## Custom Components + +### Builder Pattern + +```typescript +@Entry +@Component +struct BuilderExample { + @Builder + CardBuilder(title: string, content: string) { + Column() { + Text(title).fontSize(20).fontWeight(FontWeight.Bold) + Text(content).fontSize(14) + } + .padding(16) + .backgroundColor(Color.White) + .borderRadius(8) + } + + build() { + Column({ space: 16 }) { + this.CardBuilder('Card 1', 'Content 1') + this.CardBuilder('Card 2', 'Content 2') + } + .padding(16) + } +} +``` + +### BuilderParam for Slot Pattern + +```typescript +@Component +struct Card { + @BuilderParam content: () => void = this.defaultContent; + + @Builder + defaultContent() { + Text('Default Content') + } + + build() { + Column() { + this.content() + } + .padding(16) + .backgroundColor(Color.White) + .borderRadius(8) + } +} + +// Usage +@Entry +@Component +struct SlotExample { + build() { + Column() { + Card() { + Column() { + Text('Custom Title') + Image($r('app.media.icon')) + } + } + } + } +} +``` + +--- + +## Conditional Rendering + +### if/else + +```typescript +@Component +struct ConditionalExample { + @State isLoggedIn: boolean = false; + + build() { + Column() { + if (this.isLoggedIn) { + Text('Welcome back!') + Button('Logout') + } else { + Text('Please login') + Button('Login') + .onClick(() => { this.isLoggedIn = true; }) + } + } + } +} +``` + +### Visibility Control + +```typescript +@Component +struct VisibilityExample { + @State showDetails: boolean = false; + + build() { + Column() { + Text('Summary') + Text('Detailed information...') + .visibility(this.showDetails ? Visibility.Visible : Visibility.None) + Button(this.showDetails ? 'Hide' : 'Show') + .onClick(() => { this.showDetails = !this.showDetails; }) + } + } +} +``` + +--- + +## Best Practices + +1. **Minimize @State scope** - Keep state as close to where it's used as possible +2. **Use @Prop for read-only data** - Prevents accidental modifications +3. **Prefer @Link for form inputs** - Enables two-way binding +4. **Use LazyForEach for lists > 100 items** - Improves performance +5. **Always provide key functions** - Enables efficient list updates +6. **Use @Builder for reusable UI blocks** - Reduces duplication +7. **Clean up in aboutToDisappear** - Cancel timers, unsubscribe events diff --git a/arkts-development/references/migration-guide.md b/arkts-development/references/migration-guide.md new file mode 100644 index 0000000..41351e6 --- /dev/null +++ b/arkts-development/references/migration-guide.md @@ -0,0 +1,407 @@ +# TypeScript to ArkTS Migration Guide + +Complete guide for migrating TypeScript code to ArkTS, covering all language constraints and adaptation rules. + +## Table of Contents + +1. [Overview](#overview) +2. [Constraint Categories](#constraint-categories) +3. [Prohibited Features](#prohibited-features) +4. [Migration Examples](#migration-examples) +5. [Migration Checklist](#migration-checklist) + +--- + +## Overview + +ArkTS is based on TypeScript but enforces stricter rules for: +- **Performance**: Static analysis enables AOT compilation +- **Type Safety**: Eliminates runtime type errors +- **Predictability**: Fixed object structures at compile time + +Constraints are categorized as: +- **Error**: Must fix, blocks compilation +- **Warning**: Should fix, may become errors in future + +--- + +## Constraint Categories + +### 1. Type System Constraints + +#### Prohibited: `any` and `unknown` + +```typescript +// ❌ TypeScript +let value: any = getData(); +let result: unknown = parse(input); + +// ✅ ArkTS +interface Data { id: number; name: string; } +let value: Data = getData(); +let result: Data | null = parse(input); +``` + +#### Prohibited: Type assertions to `any` + +```typescript +// ❌ TypeScript +(obj as any).dynamicProp = value; + +// ✅ ArkTS - Define complete interface +interface MyObject { + existingProp: string; + dynamicProp?: number; +} +let obj: MyObject = { existingProp: 'test' }; +obj.dynamicProp = value; +``` + +### 2. Variable Declaration + +#### Prohibited: `var` + +```typescript +// ❌ TypeScript +var count = 0; +var name = "hello"; + +// ✅ ArkTS +let count = 0; +const name = "hello"; +``` + +### 3. Object Structure Constraints + +#### Prohibited: Runtime property modification + +```typescript +class Point { + x: number = 0; + y: number = 0; +} + +let p = new Point(); + +// ❌ All prohibited +p['z'] = 99; // Dynamic property +delete p.x; // Property deletion +Object.assign(p, {z: 1}); // Runtime extension + +// ✅ Define all properties upfront +class Point3D { + x: number = 0; + y: number = 0; + z: number = 0; +} +``` + +#### Prohibited: Structural typing (duck typing) + +```typescript +interface Named { name: string; } + +// ❌ TypeScript allows structural matching +let obj = { name: "Alice", age: 25 }; +let named: Named = obj; // Works in TS, fails in ArkTS + +// ✅ ArkTS requires explicit implementation +class Person implements Named { + name: string = ""; + age: number = 0; +} +let named: Named = new Person(); +``` + +### 4. Private Fields + +#### Prohibited: `#` private fields + +```typescript +// ❌ TypeScript +class MyClass { + #secret: string = ""; + #getValue(): string { return this.#secret; } +} + +// ✅ ArkTS +class MyClass { + private secret: string = ""; + private getValue(): string { return this.secret; } +} +``` + +### 5. Symbol Properties + +#### Prohibited: Symbol as property key + +```typescript +// ❌ TypeScript +const sym = Symbol('key'); +let obj = { [sym]: 'value' }; + +// ✅ ArkTS +let obj = { key: 'value' }; +``` + +### 6. Prohibited Statements + +#### `for...in` + +```typescript +// ❌ TypeScript +for (let key in obj) { + console.log(obj[key]); +} + +// ✅ ArkTS - Use Object.keys with forEach +Object.keys(obj).forEach((key: string) => { + // Access via typed interface +}); + +// ✅ ArkTS - Use for...of for arrays +let arr: string[] = ['a', 'b', 'c']; +for (let item of arr) { + console.log(item); +} +``` + +#### `delete` + +```typescript +// ❌ TypeScript +delete obj.property; + +// ✅ ArkTS - Use optional properties +interface Config { + name: string; + value?: number; // Optional, can be undefined +} +let config: Config = { name: 'test', value: undefined }; +``` + +#### `with` + +```typescript +// ❌ TypeScript +with (obj) { + console.log(property); +} + +// ✅ ArkTS - Use explicit references +console.log(obj.property); +``` + +#### `in` operator for type checking + +```typescript +// ❌ TypeScript +if ('name' in person) { + console.log(person.name); +} + +// ✅ ArkTS - Use instanceof +if (person instanceof Person) { + console.log(person.name); +} + +// ✅ ArkTS - Use discriminated unions +interface Person { type: 'person'; name: string; } +interface Animal { type: 'animal'; species: string; } +type Entity = Person | Animal; + +function getName(e: Entity): string { + if (e.type === 'person') { + return e.name; + } + return e.species; +} +``` + +### 7. Interface Constraints + +#### Prohibited: Call signatures and construct signatures + +```typescript +// ❌ TypeScript +interface Callable { + (x: number): number; + new (s: string): Object; +} + +// ✅ ArkTS - Use classes +class Calculator { + calculate(x: number): number { + return x * 2; + } +} + +class Factory { + create(s: string): Object { + return { value: s }; + } +} +``` + +### 8. Other Restrictions + +| Feature | Status | Alternative | +|---------|--------|-------------| +| Comma expressions | Prohibited (except in `for`) | Separate statements | +| Computed property names | Limited | String literal keys | +| Spread on non-arrays | Limited | Explicit copying | +| `eval()` | Prohibited | Avoid | +| `Function()` constructor | Prohibited | Arrow functions | +| Prototype modification | Prohibited | Class inheritance | + +--- + +## Migration Examples + +### Example 1: Dynamic Configuration Object + +```typescript +// ❌ TypeScript +let config: any = {}; +config.apiUrl = 'https://api.example.com'; +config.timeout = 5000; +config.retry = true; + +// ✅ ArkTS +interface AppConfig { + apiUrl: string; + timeout: number; + retry: boolean; +} + +let config: AppConfig = { + apiUrl: 'https://api.example.com', + timeout: 5000, + retry: true +}; +``` + +### Example 2: Object Iteration + +```typescript +// ❌ TypeScript +interface User { name: string; age: number; } +let user: User = { name: 'John', age: 30 }; + +for (let key in user) { + console.log(`${key}: ${user[key]}`); +} + +// ✅ ArkTS +interface User { + name: string; + age: number; +} + +let user: User = { name: 'John', age: 30 }; +console.log(`name: ${user.name}`); +console.log(`age: ${user.age}`); + +// Or use explicit property list +const props: (keyof User)[] = ['name', 'age']; +for (let prop of props) { + // Handle each known property +} +``` + +### Example 3: Optional Property Handling + +```typescript +// ❌ TypeScript +let obj: any = { a: 1 }; +if (obj.b) { + delete obj.b; +} +obj.c = 3; + +// ✅ ArkTS +interface MyObj { + a: number; + b?: number; + c?: number; +} + +let obj: MyObj = { a: 1 }; +if (obj.b !== undefined) { + obj.b = undefined; // Set to undefined instead of delete +} +obj.c = 3; +``` + +### Example 4: Type Guards + +```typescript +// ❌ TypeScript +function process(input: unknown) { + if (typeof input === 'string') { + return input.toUpperCase(); + } + if ('length' in input) { + return (input as any[]).length; + } +} + +// ✅ ArkTS +function processString(input: string): string { + return input.toUpperCase(); +} + +function processArray(input: string[]): number { + return input.length; +} + +// Use union types with type narrowing +type Input = string | string[]; + +function process(input: Input): string | number { + if (typeof input === 'string') { + return input.toUpperCase(); + } + return input.length; +} +``` + +--- + +## Migration Checklist + +### Phase 1: Enable Strict Mode +- [ ] Enable `strict: true` in tsconfig.json +- [ ] Enable `noImplicitAny: true` +- [ ] Enable `strictNullChecks: true` +- [ ] Fix all resulting errors + +### Phase 2: Remove Prohibited Keywords +- [ ] Replace all `var` with `let`/`const` +- [ ] Remove all `any` type annotations +- [ ] Remove all `unknown` type annotations +- [ ] Replace `#` private fields with `private` + +### Phase 3: Fix Object Patterns +- [ ] Replace dynamic property access with typed interfaces +- [ ] Remove `delete` statements +- [ ] Remove `for...in` loops +- [ ] Remove `with` statements +- [ ] Replace `in` operator type checks + +### Phase 4: Update Interfaces +- [ ] Remove call signatures from interfaces +- [ ] Remove construct signatures from interfaces +- [ ] Replace structural typing with explicit implements + +### Phase 5: Validate +- [ ] Build with ArkTS compiler +- [ ] Fix remaining errors +- [ ] Test all functionality + +--- + +## Resources + +- [Official Migration Guide](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/typescript-to-arkts-migration-guide) +- [ArkTS Language Reference](https://developer.huawei.com/consumer/cn/arkts/)