feat: add comprehensive State Management V2 documentation

- Add complete V2 decorators guide (state-management-v2.md, 38KB)
  - @ComponentV2, @Local, @Param, @Event
  - @ObservedV2, @Trace, @Computed, @Monitor
  - @Provider/@Consumer for cross-level sync
  - V1 vs V2 comparison and migration guide
  - Best practices and troubleshooting

- Add 9 practical V2 code examples (state-management-v2-examples.ets, 13KB)
  - Basic components, nested objects, parent-child communication
  - Computed properties, monitors, providers/consumers
  - Real-world patterns: forms, todo lists, collections

- Update SKILL.md with V2 decorators reference table
- Source: Official OpenHarmony documentation (API 12+)
This commit is contained in:
cheliangzhao
2026-02-10 21:00:52 +08:00
parent 9d902b1074
commit 7f1ff2df6b
3 changed files with 2253 additions and 0 deletions

View File

@@ -34,6 +34,8 @@ struct HelloWorld {
## State Management Decorators ## State Management Decorators
### V1 (Traditional)
| Decorator | Usage | Description | | Decorator | Usage | Description |
|-----------|-------|-------------| |-----------|-------|-------------|
| `@State` | `@State count: number = 0` | Component internal state | | `@State` | `@State count: number = 0` | Component internal state |
@@ -42,6 +44,22 @@ struct HelloWorld {
| `@Provide/@Consume` | Cross-level | Ancestor → Descendant | | `@Provide/@Consume` | Cross-level | Ancestor → Descendant |
| `@Observed/@ObjectLink` | Nested objects | Deep object observation | | `@Observed/@ObjectLink` | Nested objects | Deep object observation |
### V2 (Recommended - API 12+)
| Decorator | Usage | Description |
|-----------|-------|-------------|
| `@ComponentV2` | `@ComponentV2 struct MyComp` | Enable V2 state management |
| `@Local` | `@Local count: number = 0` | Internal state (no external init) |
| `@Param` | `@Param title: string = ""` | Parent → Child (one-way, efficient) |
| `@Event` | `@Event onChange: () => void` | Child → Parent (callback) |
| `@ObservedV2` | `@ObservedV2 class Data` | Class observation |
| `@Trace` | `@Trace name: string` | Property-level tracking |
| `@Computed` | `@Computed get value()` | Cached computed properties |
| `@Monitor` | `@Monitor('prop') onFn()` | Watch changes with before/after |
| `@Provider/@Consumer` | Cross-level | Two-way sync across tree |
**See [references/state-management-v2.md](references/state-management-v2.md) for complete V2 guide.**
## Common Layouts ## Common Layouts
```typescript ```typescript
@@ -277,6 +295,7 @@ See [references/arkguard-obfuscation.md](references/arkguard-obfuscation.md) for
## Reference Files ## Reference Files
- **State Management V2**: [references/state-management-v2.md](references/state-management-v2.md) - Complete guide to V2 state management (@ComponentV2, @Local, @Param, @Event, @ObservedV2, @Trace, @Computed, @Monitor, @Provider, @Consumer)
- **Migration Guide**: [references/migration-guide.md](references/migration-guide.md) - Complete TypeScript to ArkTS migration rules and examples - **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 - **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 - **API Reference**: [references/api-reference.md](references/api-reference.md) - Common HarmonyOS APIs

View File

@@ -0,0 +1,523 @@
// State Management V2 - Quick Reference Examples
// ============================================
// 1. BASIC COMPONENT WITH @Local STATE
// ============================================
@Entry
@ComponentV2
struct CounterApp {
@Local count: number = 0;
@Local step: number = 1;
build() {
Column({ space: 10 }) {
Text(`Count: ${this.count}`)
.fontSize(30)
Button(`+${this.step}`)
.onClick(() => this.count += this.step)
Button(`-${this.step}`)
.onClick(() => this.count -= this.step)
}
}
}
// ============================================
// 2. NESTED OBJECTS WITH @ObservedV2/@Trace
// ============================================
@ObservedV2
class Address {
@Trace city: string;
@Trace street: string;
constructor(city: string, street: string) {
this.city = city;
this.street = street;
}
}
@ObservedV2
class Person {
@Trace name: string;
@Trace age: number;
@Trace address: Address;
constructor(name: string, age: number, address: Address) {
this.name = name;
this.age = age;
this.address = address;
}
}
@Entry
@ComponentV2
struct PersonProfile {
person: Person = new Person(
"Tom",
25,
new Address("Beijing", "Main St")
);
build() {
Column({ space: 10 }) {
Text(`${this.person.name}, ${this.person.age}`)
Text(`${this.person.address.city}`)
Button('Birthday').onClick(() => {
this.person.age++; // Observable
})
Button('Move').onClick(() => {
this.person.address.city = "Shanghai"; // Observable
})
}
}
}
// ============================================
// 3. PARENT-CHILD WITH @Param/@Event
// ============================================
@ComponentV2
struct Counter {
@Param count: number = 0;
@Event onIncrement: () => void = () => {};
@Event onDecrement: () => void = () => {};
build() {
Row({ space: 10 }) {
Button('-').onClick(() => this.onDecrement())
Text(`${this.count}`).fontSize(30)
Button('+').onClick(() => this.onIncrement())
}
}
}
@Entry
@ComponentV2
struct ParentApp {
@Local count: number = 0;
build() {
Column({ space: 20 }) {
Text('Parent Count: ' + this.count)
Counter({
count: this.count,
onIncrement: () => this.count++,
onDecrement: () => this.count--
})
}
}
}
// ============================================
// 4. COMPUTED PROPERTIES
// ============================================
@ObservedV2
class CartItem {
@Trace name: string;
@Trace price: number;
@Trace quantity: number;
constructor(name: string, price: number, quantity: number) {
this.name = name;
this.price = price;
this.quantity = quantity;
}
}
@Entry
@ComponentV2
struct ShoppingCart {
@Local items: CartItem[] = [
new CartItem("Apple", 5, 3),
new CartItem("Banana", 3, 5)
];
@Local taxRate: number = 0.1;
@Computed
get subtotal(): number {
console.log("Computing subtotal"); // Only logs when items change
return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
@Computed
get tax(): number {
return this.subtotal * this.taxRate;
}
@Computed
get total(): number {
return this.subtotal + this.tax;
}
build() {
Column({ space: 10 }) {
ForEach(this.items, (item: CartItem) => {
Row() {
Text(`${item.name}: $${item.price} × ${item.quantity}`)
Button('+').onClick(() => item.quantity++)
}
}, (item: CartItem, idx: number) => item.name + idx)
Divider()
Text(`Subtotal: $${this.subtotal.toFixed(2)}`)
Text(`Tax (${this.taxRate * 100}%): $${this.tax.toFixed(2)}`)
Text(`Total: $${this.total.toFixed(2)}`).fontWeight(FontWeight.Bold)
}
}
}
// ============================================
// 5. MONITOR CHANGES
// ============================================
@ObservedV2
class Product {
@Trace name: string = "Laptop";
@Trace price: number = 1000;
@Trace stock: number = 10;
@Monitor('price')
onPriceChange(monitor: IMonitor) {
const change = monitor.value();
console.log(`Price changed from ${change?.before} to ${change?.now}`);
if (change && change.now > change.before) {
console.log(`Price increased by ${change.now - change.before}`);
}
}
@Monitor('stock')
onStockChange(monitor: IMonitor) {
const change = monitor.value();
if (change && change.now < 5) {
console.warn(`Low stock alert: ${change.now} items`);
}
}
}
@Entry
@ComponentV2
struct ProductManager {
product: Product = new Product();
@Monitor('product.price', 'product.stock')
onProductChange(monitor: IMonitor) {
console.log('Product properties changed:', monitor.dirty);
}
build() {
Column({ space: 10 }) {
Text(`${this.product.name}`)
Text(`Price: $${this.product.price}`)
Text(`Stock: ${this.product.stock}`)
Button('Increase Price').onClick(() => {
this.product.price += 100;
})
Button('Decrease Stock').onClick(() => {
this.product.stock--;
})
}
}
}
// ============================================
// 6. PROVIDER/CONSUMER (CROSS-LEVEL)
// ============================================
@Entry
@ComponentV2
struct AppRoot {
@Provider('app-theme') theme: string = 'light';
@Provider('user-name') userName: string = 'Alice';
build() {
Column({ space: 20 }) {
Text(`App Theme: ${this.theme}`)
Button('Toggle Theme').onClick(() => {
this.theme = this.theme === 'light' ? 'dark' : 'light';
})
MiddleComponent()
}
}
}
@ComponentV2
struct MiddleComponent {
build() {
Column({ space: 10 }) {
Text('Middle Component')
DeepNestedComponent()
}
}
}
@ComponentV2
struct DeepNestedComponent {
@Consumer('app-theme') theme: string = 'default';
@Consumer('user-name') userName: string = 'Guest';
build() {
Column({ space: 10 }) {
Text(`User: ${this.userName}`)
Text(`Theme: ${this.theme}`)
.backgroundColor(this.theme === 'dark' ? Color.Black : Color.White)
.fontColor(this.theme === 'dark' ? Color.White : Color.Black)
Button('Change from Deep Component').onClick(() => {
this.theme = 'custom'; // Updates provider in AppRoot
})
}
}
}
// ============================================
// 7. FORM INPUT PATTERN
// ============================================
@ObservedV2
class FormData {
@Trace username: string = "";
@Trace email: string = "";
@Trace password: string = "";
}
@ComponentV2
struct FormField {
@Param label: string = "";
@Param value: string = "";
@Param type: InputType = InputType.Normal;
@Event onChange: (text: string) => void = () => {};
build() {
Column({ space: 5 }) {
Text(this.label).fontSize(14)
TextInput({ text: this.value })
.type(this.type)
.onChange((text: string) => this.onChange(text))
}
}
}
@Entry
@ComponentV2
struct RegistrationForm {
@Local formData: FormData = new FormData();
@Local isValid: boolean = false;
@Monitor('formData.username', 'formData.email', 'formData.password')
validateForm(monitor: IMonitor) {
this.isValid =
this.formData.username.length >= 3 &&
this.formData.email.includes('@') &&
this.formData.password.length >= 8;
}
build() {
Column({ space: 15 }) {
Text('Registration').fontSize(24).fontWeight(FontWeight.Bold)
FormField({
label: 'Username',
value: this.formData.username,
onChange: (text: string) => {
this.formData.username = text;
}
})
FormField({
label: 'Email',
value: this.formData.email,
type: InputType.Email,
onChange: (text: string) => {
this.formData.email = text;
}
})
FormField({
label: 'Password',
value: this.formData.password,
type: InputType.Password,
onChange: (text: string) => {
this.formData.password = text;
}
})
Button('Submit')
.enabled(this.isValid)
.backgroundColor(this.isValid ? Color.Blue : Color.Gray)
.onClick(() => {
console.log('Form submitted:', this.formData);
})
}
.padding(20)
}
}
// ============================================
// 8. TODO LIST EXAMPLE
// ============================================
@ObservedV2
class TodoItem {
@Trace id: string;
@Trace title: string;
@Trace completed: boolean;
constructor(title: string) {
this.id = Date.now().toString() + Math.random();
this.title = title;
this.completed = false;
}
}
@ObservedV2
class TodoStore {
@Trace items: TodoItem[] = [];
addItem(title: string): void {
this.items.push(new TodoItem(title));
}
removeItem(id: string): void {
const index = this.items.findIndex(item => item.id === id);
if (index !== -1) {
this.items.splice(index, 1);
}
}
toggleItem(id: string): void {
const item = this.items.find(item => item.id === id);
if (item) {
item.completed = !item.completed;
}
}
}
@ComponentV2
struct TodoItemView {
@Param item: TodoItem = new TodoItem("");
@Event onToggle: () => void = () => {};
@Event onDelete: () => void = () => {};
build() {
Row({ space: 10 }) {
Checkbox({ select: this.item.completed })
.onChange(() => this.onToggle())
Text(this.item.title)
.decoration({ type: this.item.completed ? TextDecorationType.LineThrough : TextDecorationType.None })
.opacity(this.item.completed ? 0.5 : 1)
.layoutWeight(1)
Button('Delete')
.onClick(() => this.onDelete())
}
.padding(10)
.width('100%')
}
}
@Entry
@ComponentV2
struct TodoApp {
@Local store: TodoStore = new TodoStore();
@Local inputText: string = "";
@Computed
get completedCount(): number {
return this.store.items.filter(item => item.completed).length;
}
@Computed
get totalCount(): number {
return this.store.items.length;
}
build() {
Column({ space: 15 }) {
Text('Todo List').fontSize(30).fontWeight(FontWeight.Bold)
Text(`${this.completedCount} / ${this.totalCount} completed`)
Row({ space: 10 }) {
TextInput({ text: this.inputText, placeholder: 'New task...' })
.layoutWeight(1)
.onChange((text: string) => {
this.inputText = text;
})
Button('Add')
.enabled(this.inputText.trim().length > 0)
.onClick(() => {
if (this.inputText.trim()) {
this.store.addItem(this.inputText.trim());
this.inputText = "";
}
})
}
List({ space: 5 }) {
ForEach(this.store.items, (item: TodoItem) => {
ListItem() {
TodoItemView({
item: item,
onToggle: () => this.store.toggleItem(item.id),
onDelete: () => this.store.removeItem(item.id)
})
}
}, (item: TodoItem) => item.id)
}
.layoutWeight(1)
}
.padding(20)
.height('100%')
}
}
// ============================================
// 9. COLLECTION TYPES (Array, Map, Set)
// ============================================
@ObservedV2
class CollectionDemo {
@Trace numbers: number[] = [1, 2, 3];
@Trace userMap: Map<string, string> = new Map([['id1', 'Alice'], ['id2', 'Bob']]);
@Trace tags: Set<string> = new Set(['typescript', 'arkts', 'harmonyos']);
}
@Entry
@ComponentV2
struct CollectionsExample {
demo: CollectionDemo = new CollectionDemo();
build() {
Column({ space: 15 }) {
// Array
Text('Array:').fontWeight(FontWeight.Bold)
ForEach(this.demo.numbers, (num: number, idx: number) => {
Text(`[${idx}] = ${num}`)
}, (num: number, idx: number) => num.toString() + idx)
Button('Array.push').onClick(() => {
this.demo.numbers.push(Math.floor(Math.random() * 100));
})
Divider()
// Map
Text('Map:').fontWeight(FontWeight.Bold)
ForEach(Array.from(this.demo.userMap.entries()), (entry: [string, string]) => {
Text(`${entry[0]}: ${entry[1]}`)
}, (entry: [string, string]) => entry[0])
Button('Map.set').onClick(() => {
const id = 'id' + Date.now();
this.demo.userMap.set(id, 'New User');
})
Divider()
// Set
Text('Set:').fontWeight(FontWeight.Bold)
ForEach(Array.from(this.demo.tags.values()), (tag: string) => {
Text(`• ${tag}`)
}, (tag: string) => tag)
Button('Set.add').onClick(() => {
this.demo.tags.add('tag' + Date.now());
})
}
.padding(20)
}
}

File diff suppressed because it is too large Load Diff