HarmonyOS/OpenHarmony application development-ArkTS language rendering control ForEach loop rendering

ForEach performs loop rendering based on array type data. Note: Starting from API version 9, this interface supports use in ArkTS cards.
1. Interface description

ForEach(
  arr: any[], 
  itemGenerator: (item: any, index?: number) => void,
  keyGenerator?: (item: any, index?: number) => string 
)

#2023Blind Box+Code#HarmonyOS/OpenHarmony application development-ArkTS language rendering control ForEach loop rendering-open source basic software community


2. Usage restrictions
ForEach must be used within the container component.
The generated child components should be child components that are allowed to be included in the ForEach parent container component.
Allows if/else conditional rendering to be included in subcomponent generator functions, and also allows ForEach to be included in if/else conditional rendering statements.
The calling order of the itemGenerator function is not necessarily the same as the data items in the array. During the development process, do not assume whether the itemGenerator and keyGenerator functions are executed and their execution order. For example, the following example may not run correctly

ForEach(anArray.map((item1, index1) => { return { i: index1 + 1, data: item1 }; }), 
  item => Text(`${item.i}. item.data.label`),
  item => item.data.id.toString())

3. Developer’s Suggestions
It is recommended that developers do not assume the execution order of item constructors. The order of execution may not be the order in which the items in the array are sorted.
Don't make assumptions about whether an array item is initially rendered. The initial rendering of ForEach builds all array items when @Component is first rendered. This behavior may be changed to lazy loading mode in subsequent framework versions.
Using the index parameter has a serious negative impact on UI update performance, so please try to avoid it.
If the index parameter is used in the item constructor, the parameter must also be used in the item index function. Otherwise, if the item index function does not use the index parameter, the framework will also take the index into consideration when ForEach generates the actual key value, and the index will be spliced ​​at the end by default.
4. Usage scenarios
1. Simple ForEach example
Create three Text and Divide components based on arr data.

@Entry
@Component
struct MyComponent {
  @State arr: number[] = [10, 20, 30];

  build() {
    Column({ space: 5 }) {
      Button('Reverse Array')
        .onClick(() => {
          this.arr.reverse();
        })
      ForEach(this.arr, (item: number) => {
        Text(`item value: ${item}`).fontSize(18)
        Divider().strokeWidth(2)
      }, (item: number) => item.toString())
    }
  }
}

2. Complex ForEach example

@Component
struct CounterView {
  label: string;
  @State count: number = 0;

  build() {
    Button(`${this.label}-${this.count} click +1`)
      .width(300).height(40)
      .backgroundColor('#a0ffa0')
      .onClick(() => {
        this.count++;
      })
  }
}

@Entry
@Component
struct MainView {
  @State arr: number[] = Array.from(Array(10).keys()); // [0.,.9]
  nextUnused: number = this.arr.length;

  build() {
    Column() {
      Button(`push new item`)
        .onClick(() => {
          this.arr.push(this.nextUnused++)
        })
        .width(300).height(40)
      Button(`pop last item`)
        .onClick(() => {
          this.arr.pop()
        })
        .width(300).height(40)
      Button(`prepend new item (unshift)`)
        .onClick(() => {
          this.arr.unshift(this.nextUnused++)
        })
        .width(300).height(40)
      Button(`remove first item (shift)`)
        .onClick(() => {
          this.arr.shift()
        })
        .width(300).height(40)
      Button(`insert at pos ${Math.floor(this.arr.length / 2)}`)
        .onClick(() => {
          this.arr.splice(Math.floor(this.arr.length / 2), 0, this.nextUnused++);
        })
        .width(300).height(40)
      Button(`remove at pos ${Math.floor(this.arr.length / 2)}`)
        .onClick(() => {
          this.arr.splice(Math.floor(this.arr.length / 2), 1);
        })
        .width(300).height(40)
      Button(`set at pos ${Math.floor(this.arr.length / 2)} to ${this.nextUnused}`)
        .onClick(() => {
          this.arr[Math.floor(this.arr.length / 2)] = this.nextUnused++;
        })
        .width(300).height(40)
      ForEach(this.arr,
        (item) => {
          CounterView({ label: item.toString() })
        },
        (item) => item.toString()
      )
    }
  }

MainView holds an array of numbers decorated with @State. Adding, deleting, and replacing array items are observable change events. When these events occur, the ForEach within MainView will be updated.
The item index function creates a unique and persistent key value for each array item. The ArkUI framework uses this key value to determine whether the item in the array has changed. As long as the key value is the same, the value of the array item is assumed to be unchanged, but its index position may will change. The premise of this mechanism is that different array items cannot have the same key value.
Using the calculated ID, the framework can distinguish between added, deleted, and retained array items:
(1) The framework will delete the UI component of the deleted array item.
(2) The framework only executes the item constructor for newly added array items.
(3) The framework does not execute item constructors for retained array items. If the item index in the array has changed, the framework will only move its UI component according to the new order, but will not update the UI component.
It is recommended to use the item index function, but this is optional. The generated IDs must be unique, meaning the same ID cannot be calculated for different items in the array. Even if two array items have the same value, their IDs must be different.
If the array item value changes, the ID must change.
Example: As mentioned before, the id generation function is optional. Here's ForEach without the item index function:
ForEach(this.arr,
(item) => { CounterView({ label: item.toString() }) } )



If no item ID function is provided, the framework attempts to intelligently detect array changes when updating the ForEach. However, it may remove child components and re-execute the item constructor for array items that are moved in the array (index is changed). In the example above, this would change the application's behavior with respect to the CounterView counter state. When a new CounterView instance is created, the value of counter will be initialized to 0.
3. ForEach example using @ObjectLink
When it is necessary to retain the state of repeated subcomponents, @ObjectLink can push the state to the parent component in the component tree.

let NextID: number = 0;

@Observed
class MyCounter {
  public id: number;
  public c: number;

  constructor(c: number) {
    this.id = NextID++;
    this.c = c;
  }
}

@Component
struct CounterView {
  @ObjectLink counter: MyCounter;
  label: string = 'CounterView';

  build() {
    Button(`CounterView [${this.label}] this.counter.c=${this.counter.c} +1`)
      .width(200).height(50)
      .onClick(() => {
        this.counter.c += 1;
      })
  }
}

@Entry
@Component
struct MainView {
  @State firstIndex: number = 0;
  @State counters: Array<MyCounter> = [new MyCounter(0), new MyCounter(0), new MyCounter(0),
    new MyCounter(0), new MyCounter(0)];

  build() {
    Column() {
      ForEach(this.counters.slice(this.firstIndex, this.firstIndex + 3),
        (item) => {
          CounterView({ label: `Counter item #${item.id}`, counter: item })
        },
        (item) => item.id.toString()
      )
      Button(`Counters: shift up`)
        .width(200).height(50)
        .onClick(() => {
          this.firstIndex = Math.min(this.firstIndex + 1, this.counters.length - 3);
        })
      Button(`counters: shift down`)
        .width(200).height(50)
        .onClick(() => {
          this.firstIndex = Math.max(0, this.firstIndex - 1);
        })
    }
  }
}

When the value of firstIndex is incremented, the ForEach inside the Mainview will update and remove the CounterView subcomponent associated with the item ID firstIndex-1. For the array item with ID firstindex + 3, a new CounterView subcomponent instance will be created. Since the state variable counter value of the CounterView subcomponent is maintained by the parent component Mainview, rebuilding the CounterView subcomponent instance will not rebuild the state variable counter value.
Note that violating the above array item ID rules is the most common application development error, especially in Array scenarios, because it is easy to add repeated numbers during execution.
4. Nested use of ForEach
allows nesting ForEach in another ForEach in the same component, but it is more recommended to split the component into two and each constructor contains only one ForEach. The following is a counterexample for ForEach nesting.

class Month {
  year: number;
  month: number;
  days: number[];

  constructor(year: number, month: number, days: number[]) {
    this.year = year;
    this.month = month;
    this.days = days;
  }
}
@Component
struct CalendarExample {
  // 模拟6个月
  @State calendar : Month[] = [
    new Month(2020, 1, [...Array(31).keys()]),
    new Month(2020, 2, [...Array(28).keys()]),
    new Month(2020, 3, [...Array(31).keys()]),
    new Month(2020, 4, [...Array(30).keys()]),
    new Month(2020, 5, [...Array(31).keys()]),
    new Month(2020, 6, [...Array(30).keys()])
  ]
  build() {
    Column() {
      Button() {
        Text('next month')
      }.onClick(() => {
        this.calendar.shift()
        this.calendar.push(new Month(year: 2020, month: 7, days: [...Array(31).keys()]))
      })
      ForEach(this.calendar,
        (item: Month) => {
          ForEach(item.days,
            (day : number) => {
              // 构建日期块
            },
            (day : number) => day.toString()
          )// 内部ForEach
        },
        (item: Month) => (item.year * 12 + item.month).toString() // 字段与年和月一起使用,作为月份的唯一ID。
      )// 外部ForEach
    }
  }
}


There are two problems with the above example:
(1) The code is poorly readable.
(2) For the above array structure form of year and month data, since the framework cannot observe changes to the Month data structure in the array (such as day array changes), the inner ForEach cannot refresh the date display.
It is recommended to split the Calendar into Year, Month and Day sub-components when designing the application. Define a "Day" model class to hold information about day and decorate this class with @Observed. The DayView component uses ObjectLink to decorate variables to bind day data. Do the same for MonthView and Month model classes.
5. Examples of using the optional index parameter in ForEach
You can use the optional index parameter in the constructor and ID generation function.

@Entry
@Component
struct ForEachWithIndex {
  @State arr: number[] = [4, 3, 1, 5];

  build() {
    Column() {
      ForEach(this.arr,
        (it, indx) => {
          Text(`Item: ${indx} - ${it}`)
        },
        (it, indx) => {
          return `${indx} - ${it}`
        }
      )
    }
  }
}

The ID generation function must be constructed correctly. When the index parameter is used in the item constructor, the ID generation function must also use the index parameter to generate a unique ID and the ID of the given source array item. When an array item's index position in the array changes, its ID changes.
This example also illustrates that the index parameter can cause significant performance degradation. Even if an item is moved within the source array without modification, the UI that relies on that array item still needs to be re-rendered because the index changes. For example, when using index sorting, the array only needs to move the unmodified child UI nodes of the ForEach to the correct location, which is a lightweight operation for the framework. When using indexes, all child UI nodes need to be rebuilt, which is a much heavier operation.

 

Guess you like

Origin blog.csdn.net/weixin_69135651/article/details/132358565