Tools: Understanding ArkTS ArcList: The Go-To Component for Watch UIs

Tools: Understanding ArkTS ArcList: The Go-To Component for Watch UIs

Source: Dev.to

πŸ“˜ Introduction ## πŸ“‹ What is ArcList? ## ⌚ Why Use ArcList for Wearables? ## πŸ’‘ Basic Syntax ## πŸ› οΈ Tips for Effective Usage ## βœ… Use Case: Static Settings Check-List ## πŸ”„ Use Case: Dynamic Settings Check-List ## 🧩 Updating index.ets for Dynamic Content ## 🎯 Result: Dynamic List Output ## ✨ Conclusion ## πŸ“š References ## Written by Bilal Basboz Read the original article:Understanding ArkTS ArcList: The Go-To Component for Watch UIs Smartwatch apps are rapidly evolving, demanding fluid, visually intuitive UIs adapted for small, circular screens. HarmonyOS offers developers an elegant way to build such interfaces through the ArkTS framework and its ArcUI components. One of the most efficient components designed specifically for this purpose is ArcList. It stands out as a key element in wearable UI design, simplifying list rendering in circular layouts while improving user interaction. In this article, we’ll explore what ArcList is, how it works, and why it’s essential for wearable UI development in ArkTS. ArcList is a circular list container provided by ArkTS’s ArkUI framework. It is optimized for wearable devices, especially those with round displays. Unlike traditional vertical or horizontal lists, ArcList arranges its child items along an arc, providing a natural and intuitive scroll experience on smartwatches. Smartwatches require unique interaction patterns due to limited screen space and curved displays. ArcList offers several benefits: Basic usage of ArcList with static list-items Static usage of ArcList with List items To use LazyForEach, a data source class is required to define how the list handles operations such as add, remove, and move. In this example, we’ll create a simple data source class to demonstrate its basic usage: Created NumsDataSource to be used as a LazyForEach's DataSource To maintain the same static content list but render it dynamically, you can update the index.ets file using LazyForEach as shown below: After updating the list to use LazyForEach, the visual result and behavior remain unchanged compared to the static list. When you run the app, the list appears and functions the same, but now supports dynamic operations such as adding or removing items. ArcList is a must-know component for any HarmonyOS wearable developer. It not only simplifies list handling on circular screens but also provides a smooth, user-friendly experience tailored for the wrist. Whether you’re building a to-do app, media selector, or quick settings interface, ArcList should be your go-to choice. The OpenCms demo, Brough to you by Alkacon Software The OpenCms demo, Brough to you by Alkacon Software Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK: import { ArcList, ArcListItem, ComponentContent, ArcListAttribute, ArcListItemAttribute, LengthMetrics } from '@kit.ArkUI' @Builder function buildText() { Column() { Text('Header') .fontSize('60px') .fontWeight(FontWeight.Bold) .fontColor(Color.White) }.margin(0) } @Entry @Component struct Index { private watchSize: string = '466px' // Default watch size: 466*466 private listSize: string = '414px' // Item width context: UIContext = this.getUIContext() header: ComponentContent<Object> = new ComponentContent(this.context, wrapBuilder(buildText)); @Builder ListCardItem() { ArcListItem() { Button({ type: ButtonType.Capsule }) { Column({ space: 2 }) { Text('Title') .fontSize('30px') .fontColor(Color.White) Text('Subtitle') .fontSize('20px') .fontColor(Color.White) } .width('100%') .padding('8px') } .width(this.listSize) .height('100px') .focusable(true) .focusOnTouch(true) .backgroundColor('#0B798B') .onClick(() => { }) }.align(Alignment.Center) } build() { ArcList({ initialIndex: 0, header: this.header }) { this.ListCardItem() this.ListCardItem() this.ListCardItem() this.ListCardItem() this.ListCardItem() this.ListCardItem() } .height('100%') .width('100%') .backgroundColor('#004C5D') .space(LengthMetrics.px(10)) .borderRadius(this.watchSize) .focusable(true) .focusOnTouch(true) .defaultFocus(true) } } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: import { ArcList, ArcListItem, ComponentContent, ArcListAttribute, ArcListItemAttribute, LengthMetrics } from '@kit.ArkUI' @Builder function buildText() { Column() { Text('Header') .fontSize('60px') .fontWeight(FontWeight.Bold) .fontColor(Color.White) }.margin(0) } @Entry @Component struct Index { private watchSize: string = '466px' // Default watch size: 466*466 private listSize: string = '414px' // Item width context: UIContext = this.getUIContext() header: ComponentContent<Object> = new ComponentContent(this.context, wrapBuilder(buildText)); @Builder ListCardItem() { ArcListItem() { Button({ type: ButtonType.Capsule }) { Column({ space: 2 }) { Text('Title') .fontSize('30px') .fontColor(Color.White) Text('Subtitle') .fontSize('20px') .fontColor(Color.White) } .width('100%') .padding('8px') } .width(this.listSize) .height('100px') .focusable(true) .focusOnTouch(true) .backgroundColor('#0B798B') .onClick(() => { }) }.align(Alignment.Center) } build() { ArcList({ initialIndex: 0, header: this.header }) { this.ListCardItem() this.ListCardItem() this.ListCardItem() this.ListCardItem() this.ListCardItem() this.ListCardItem() } .height('100%') .width('100%') .backgroundColor('#004C5D') .space(LengthMetrics.px(10)) .borderRadius(this.watchSize) .focusable(true) .focusOnTouch(true) .defaultFocus(true) } } COMMAND_BLOCK: import { ArcList, ArcListItem, ComponentContent, ArcListAttribute, ArcListItemAttribute, LengthMetrics } from '@kit.ArkUI' @Builder function buildText() { Column() { Text('Header') .fontSize('60px') .fontWeight(FontWeight.Bold) .fontColor(Color.White) }.margin(0) } @Entry @Component struct Index { private watchSize: string = '466px' // Default watch size: 466*466 private listSize: string = '414px' // Item width context: UIContext = this.getUIContext() header: ComponentContent<Object> = new ComponentContent(this.context, wrapBuilder(buildText)); @Builder ListCardItem() { ArcListItem() { Button({ type: ButtonType.Capsule }) { Column({ space: 2 }) { Text('Title') .fontSize('30px') .fontColor(Color.White) Text('Subtitle') .fontSize('20px') .fontColor(Color.White) } .width('100%') .padding('8px') } .width(this.listSize) .height('100px') .focusable(true) .focusOnTouch(true) .backgroundColor('#0B798B') .onClick(() => { }) }.align(Alignment.Center) } build() { ArcList({ initialIndex: 0, header: this.header }) { this.ListCardItem() this.ListCardItem() this.ListCardItem() this.ListCardItem() this.ListCardItem() this.ListCardItem() } .height('100%') .width('100%') .backgroundColor('#004C5D') .space(LengthMetrics.px(10)) .borderRadius(this.watchSize) .focusable(true) .focusOnTouch(true) .defaultFocus(true) } } COMMAND_BLOCK: import { ArcList, ArcListItem, ComponentContent, ArcListAttribute, ArcListItemAttribute, LengthMetrics } from '@kit.ArkUI' @Builder function buildText() { Column() { Text('Settings') .fontSize('60px') .fontWeight(FontWeight.Bold) .fontColor(Color.White) }.margin(0) } @Entry @Component struct Index { @State private numItems: number[] = [0, 1, 2, 3, 4, 6, 7, 8, 9]; @State private numItemsFlags: boolean[] = [false, false, false, false, false, false, false, false, false, false]; private watchSize: string = '466px' // Default watch size: 466*466 private listSize: string = '414px' // Item width context: UIContext = this.getUIContext() header: ComponentContent<Object> = new ComponentContent(this.context, wrapBuilder(buildText)); @Builder ListCardItem(index: number, num: number) { ArcListItem() { Button({ type: ButtonType.Capsule }) { Row() { Checkbox({ name: `check_${index}` }) .focusable(false) .select(this.numItemsFlags[index]) Text(`Setting ${num}`) .fontSize('30px') .fontColor(Color.White) } .width('100%') .padding('8px') } .width(this.listSize) .height('100px') .focusable(true) .focusOnTouch(true) .backgroundColor('#0B798B') .onClick(() => { this.numItemsFlags[index] = !this.numItemsFlags[index]; }) }.align(Alignment.Center) } build() { ArcList({ initialIndex: 0, header: this.header }) { ForEach(this.numItems, (item: number, index: number) => { this.ListCardItem(index, item) }, (item: string, index: number) => `item_${item}_${index}`) } .height('100%') .width('100%') .backgroundColor('#004C5D') .space(LengthMetrics.px(10)) .borderRadius(this.watchSize) .focusable(true) .focusOnTouch(true) .defaultFocus(true) } } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: import { ArcList, ArcListItem, ComponentContent, ArcListAttribute, ArcListItemAttribute, LengthMetrics } from '@kit.ArkUI' @Builder function buildText() { Column() { Text('Settings') .fontSize('60px') .fontWeight(FontWeight.Bold) .fontColor(Color.White) }.margin(0) } @Entry @Component struct Index { @State private numItems: number[] = [0, 1, 2, 3, 4, 6, 7, 8, 9]; @State private numItemsFlags: boolean[] = [false, false, false, false, false, false, false, false, false, false]; private watchSize: string = '466px' // Default watch size: 466*466 private listSize: string = '414px' // Item width context: UIContext = this.getUIContext() header: ComponentContent<Object> = new ComponentContent(this.context, wrapBuilder(buildText)); @Builder ListCardItem(index: number, num: number) { ArcListItem() { Button({ type: ButtonType.Capsule }) { Row() { Checkbox({ name: `check_${index}` }) .focusable(false) .select(this.numItemsFlags[index]) Text(`Setting ${num}`) .fontSize('30px') .fontColor(Color.White) } .width('100%') .padding('8px') } .width(this.listSize) .height('100px') .focusable(true) .focusOnTouch(true) .backgroundColor('#0B798B') .onClick(() => { this.numItemsFlags[index] = !this.numItemsFlags[index]; }) }.align(Alignment.Center) } build() { ArcList({ initialIndex: 0, header: this.header }) { ForEach(this.numItems, (item: number, index: number) => { this.ListCardItem(index, item) }, (item: string, index: number) => `item_${item}_${index}`) } .height('100%') .width('100%') .backgroundColor('#004C5D') .space(LengthMetrics.px(10)) .borderRadius(this.watchSize) .focusable(true) .focusOnTouch(true) .defaultFocus(true) } } COMMAND_BLOCK: import { ArcList, ArcListItem, ComponentContent, ArcListAttribute, ArcListItemAttribute, LengthMetrics } from '@kit.ArkUI' @Builder function buildText() { Column() { Text('Settings') .fontSize('60px') .fontWeight(FontWeight.Bold) .fontColor(Color.White) }.margin(0) } @Entry @Component struct Index { @State private numItems: number[] = [0, 1, 2, 3, 4, 6, 7, 8, 9]; @State private numItemsFlags: boolean[] = [false, false, false, false, false, false, false, false, false, false]; private watchSize: string = '466px' // Default watch size: 466*466 private listSize: string = '414px' // Item width context: UIContext = this.getUIContext() header: ComponentContent<Object> = new ComponentContent(this.context, wrapBuilder(buildText)); @Builder ListCardItem(index: number, num: number) { ArcListItem() { Button({ type: ButtonType.Capsule }) { Row() { Checkbox({ name: `check_${index}` }) .focusable(false) .select(this.numItemsFlags[index]) Text(`Setting ${num}`) .fontSize('30px') .fontColor(Color.White) } .width('100%') .padding('8px') } .width(this.listSize) .height('100px') .focusable(true) .focusOnTouch(true) .backgroundColor('#0B798B') .onClick(() => { this.numItemsFlags[index] = !this.numItemsFlags[index]; }) }.align(Alignment.Center) } build() { ArcList({ initialIndex: 0, header: this.header }) { ForEach(this.numItems, (item: number, index: number) => { this.ListCardItem(index, item) }, (item: string, index: number) => `item_${item}_${index}`) } .height('100%') .width('100%') .backgroundColor('#004C5D') .space(LengthMetrics.px(10)) .borderRadius(this.watchSize) .focusable(true) .focusOnTouch(true) .defaultFocus(true) } } CODE_BLOCK: export class NumsDataSource implements IDataSource { private dataArray: number[] = []; private listeners: DataChangeListener[] = []; constructor(element: number[]) { for (let index = 0; index < element.length; index++) { this.dataArray.push(element[index]); } } public totalCount(): number { return this.dataArray.length; } public getData(index: number): number { return this.dataArray[index]; } registerDataChangeListener(listener: DataChangeListener): void { if (this.listeners.indexOf(listener) < 0) { this.listeners.push(listener); } } unregisterDataChangeListener(listener: DataChangeListener): void { const pos = this.listeners.indexOf(listener); if (pos >= 0) { this.listeners.splice(pos, 1); } } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: export class NumsDataSource implements IDataSource { private dataArray: number[] = []; private listeners: DataChangeListener[] = []; constructor(element: number[]) { for (let index = 0; index < element.length; index++) { this.dataArray.push(element[index]); } } public totalCount(): number { return this.dataArray.length; } public getData(index: number): number { return this.dataArray[index]; } registerDataChangeListener(listener: DataChangeListener): void { if (this.listeners.indexOf(listener) < 0) { this.listeners.push(listener); } } unregisterDataChangeListener(listener: DataChangeListener): void { const pos = this.listeners.indexOf(listener); if (pos >= 0) { this.listeners.splice(pos, 1); } } } CODE_BLOCK: export class NumsDataSource implements IDataSource { private dataArray: number[] = []; private listeners: DataChangeListener[] = []; constructor(element: number[]) { for (let index = 0; index < element.length; index++) { this.dataArray.push(element[index]); } } public totalCount(): number { return this.dataArray.length; } public getData(index: number): number { return this.dataArray[index]; } registerDataChangeListener(listener: DataChangeListener): void { if (this.listeners.indexOf(listener) < 0) { this.listeners.push(listener); } } unregisterDataChangeListener(listener: DataChangeListener): void { const pos = this.listeners.indexOf(listener); if (pos >= 0) { this.listeners.splice(pos, 1); } } } COMMAND_BLOCK: import { ArcList, ArcListItem, ComponentContent, ArcListAttribute, ArcListItemAttribute, LengthMetrics } from '@kit.ArkUI' import { NumsDataSource } from './test3'; @Builder function buildText() { Column() { Text('Settings') .fontSize('60px') .fontWeight(FontWeight.Bold) .fontColor(Color.White) }.margin(0) } @Entry @Component struct Index { private numItems: number[] = [0, 1, 2, 3, 4, 6, 7, 8, 9]; @State private numItemsFlags: boolean[] = [false, false, false, false, false, false, false, false, false, false]; private watchSize: string = '466px' // Default watch size: 466*466 private listSize: string = '414px' // Item width context: UIContext = this.getUIContext() header: ComponentContent<Object> = new ComponentContent(this.context, wrapBuilder(buildText)); private numsDataSource: NumsDataSource = new NumsDataSource(this.numItems); @Builder ListCardItem(index: number, num: number) { ArcListItem() { Button({ type: ButtonType.Capsule }) { Row() { Checkbox({ name: `check_${index}` }) .focusable(false) .select(this.numItemsFlags[index]) Text(`Setting ${num}`) .fontSize('30px') .fontColor(Color.White) } .width('100%') .padding('8px') } .width(this.listSize) .height('100px') .focusable(true) .focusOnTouch(true) .backgroundColor('#0B798B') .onClick(() => { this.numItemsFlags[index] = !this.numItemsFlags[index]; }) }.align(Alignment.Center) } build() { ArcList({ initialIndex: 0, header: this.header }) { LazyForEach(this.numsDataSource, (item: number, index: number) => { this.ListCardItem(index, item) }, (item: string, index: number) => `item_${item}_${index}`) } .height('100%') .width('100%') .backgroundColor('#004C5D') .space(LengthMetrics.px(10)) .borderRadius(this.watchSize) .focusable(true) .focusOnTouch(true) .defaultFocus(true) } } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: import { ArcList, ArcListItem, ComponentContent, ArcListAttribute, ArcListItemAttribute, LengthMetrics } from '@kit.ArkUI' import { NumsDataSource } from './test3'; @Builder function buildText() { Column() { Text('Settings') .fontSize('60px') .fontWeight(FontWeight.Bold) .fontColor(Color.White) }.margin(0) } @Entry @Component struct Index { private numItems: number[] = [0, 1, 2, 3, 4, 6, 7, 8, 9]; @State private numItemsFlags: boolean[] = [false, false, false, false, false, false, false, false, false, false]; private watchSize: string = '466px' // Default watch size: 466*466 private listSize: string = '414px' // Item width context: UIContext = this.getUIContext() header: ComponentContent<Object> = new ComponentContent(this.context, wrapBuilder(buildText)); private numsDataSource: NumsDataSource = new NumsDataSource(this.numItems); @Builder ListCardItem(index: number, num: number) { ArcListItem() { Button({ type: ButtonType.Capsule }) { Row() { Checkbox({ name: `check_${index}` }) .focusable(false) .select(this.numItemsFlags[index]) Text(`Setting ${num}`) .fontSize('30px') .fontColor(Color.White) } .width('100%') .padding('8px') } .width(this.listSize) .height('100px') .focusable(true) .focusOnTouch(true) .backgroundColor('#0B798B') .onClick(() => { this.numItemsFlags[index] = !this.numItemsFlags[index]; }) }.align(Alignment.Center) } build() { ArcList({ initialIndex: 0, header: this.header }) { LazyForEach(this.numsDataSource, (item: number, index: number) => { this.ListCardItem(index, item) }, (item: string, index: number) => `item_${item}_${index}`) } .height('100%') .width('100%') .backgroundColor('#004C5D') .space(LengthMetrics.px(10)) .borderRadius(this.watchSize) .focusable(true) .focusOnTouch(true) .defaultFocus(true) } } COMMAND_BLOCK: import { ArcList, ArcListItem, ComponentContent, ArcListAttribute, ArcListItemAttribute, LengthMetrics } from '@kit.ArkUI' import { NumsDataSource } from './test3'; @Builder function buildText() { Column() { Text('Settings') .fontSize('60px') .fontWeight(FontWeight.Bold) .fontColor(Color.White) }.margin(0) } @Entry @Component struct Index { private numItems: number[] = [0, 1, 2, 3, 4, 6, 7, 8, 9]; @State private numItemsFlags: boolean[] = [false, false, false, false, false, false, false, false, false, false]; private watchSize: string = '466px' // Default watch size: 466*466 private listSize: string = '414px' // Item width context: UIContext = this.getUIContext() header: ComponentContent<Object> = new ComponentContent(this.context, wrapBuilder(buildText)); private numsDataSource: NumsDataSource = new NumsDataSource(this.numItems); @Builder ListCardItem(index: number, num: number) { ArcListItem() { Button({ type: ButtonType.Capsule }) { Row() { Checkbox({ name: `check_${index}` }) .focusable(false) .select(this.numItemsFlags[index]) Text(`Setting ${num}`) .fontSize('30px') .fontColor(Color.White) } .width('100%') .padding('8px') } .width(this.listSize) .height('100px') .focusable(true) .focusOnTouch(true) .backgroundColor('#0B798B') .onClick(() => { this.numItemsFlags[index] = !this.numItemsFlags[index]; }) }.align(Alignment.Center) } build() { ArcList({ initialIndex: 0, header: this.header }) { LazyForEach(this.numsDataSource, (item: number, index: number) => { this.ListCardItem(index, item) }, (item: string, index: number) => `item_${item}_${index}`) } .height('100%') .width('100%') .backgroundColor('#004C5D') .space(LengthMetrics.px(10)) .borderRadius(this.watchSize) .focusable(true) .focusOnTouch(true) .defaultFocus(true) } } - Optimized Layout: Arranges items along a visible arc. - Focus & Highlight: Automatically centers and highlights the focused item. - Smooth Interaction: Supports rotational input and smooth scroll gestures. - Flexible Item Management: Works with both static and dynamic content. - Use ArcList directly in the build() function of your component, or place it inside a Stack or any layout container that supports alignment. - Use ArcListItem as a direct child of ArcList, or within ForEach or LazyForEach for dynamic lists. - If your list items are intended to be interactive, wrap them with the Button component and set the type to ButtonType.Capsule for a visually appropriate style. - Style the currently focused item differently (e.g., larger size, highlight color) to improve clarity and navigation feedback.