522 lines
9.5 KiB
Markdown
522 lines
9.5 KiB
Markdown
# 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
|