Files
arkts-skills/arkts-development/references/component-patterns.md
2026-01-22 12:51:22 +08:00

9.5 KiB

ArkUI Component Patterns

Advanced component patterns and best practices for ArkTS development.

Table of Contents

  1. Component Structure
  2. State Management Patterns
  3. Parent-Child Communication
  4. List Optimization
  5. Custom Components
  6. Conditional Rendering

Component Structure

Basic Component

@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)

@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

@Component
struct Counter {
  @State count: number = 0;
  
  build() {
    Column() {
      Text(`Count: ${this.count}`)
      Button('Increment')
        .onClick(() => { this.count++; })
    }
  }
}

@Prop - One-Way Binding (Parent → Child)

// 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++; })
    }
  }
}
// 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

// 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)
  }
}
// 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

// 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

// 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

// 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

// 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

@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

@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

@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

@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