DEV Community

ZHZL-m
ZHZL-m

Posted on

【Journey of HarmonyOS Next】Developing with ArkTS (2) - > UI Development II

Image description

1 -> Declarative UI development guide

1.1 -> Development Instructions

The general development journey for a declarative UI is shown in the following table.

Image description

1.2 -> Create a page

First, select the layout structure to create a page based on the expected effect of the page, and add basic built-in components to the page. The following example uses Flex to display the Text component on the page horizontally and vertically.

// test.ets
@Entry
@Component
struct MyComponent {
  build() {
    Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
      Text('One Piece')
    }
    .width('100%')
    .height('100%')
  }
}
Enter fullscreen mode Exit fullscreen mode

1.3 -> Modify the component style

When you add a built-in component to a page, the default style is displayed if you do not set the attribute method. You can change the UI display of a component by changing the attribute style of the component or the generic attribute style supported by the component.

You can change the display content of the Text component to "Tomato" by modifying the construction parameters of the Text portlet.

Modify the fontSize property of the Text component to change the font size of the component to 26, and use the fontWeight property to change the font weight to 500.

// test2.ets
@Entry
@Component
struct MyComponent {
  build() {
    Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
      Text('Tomato')
        .fontSize(26)
        .fontWeight(500)
    }
    .width('100%')
    .height('100%')
  }
}
Enter fullscreen mode Exit fullscreen mode

Image description

1.4 -> Update page content

After you create a basic page, you can update the page content based on the status of the component. The following example shows a simple way to update a page.

illustrate

Before updating the status of a component, initialize the component's member variables. The member variables of a custom component can be initialized either locally or by constructing parameters when the component is constructed, depending on the decorator used for the variable.

// test3.ets
@Entry
@Component
struct ParentComp {
  @State isCountDown: boolean = true

  build() {
    Column() {
      Text(this.isCountDown ? 'Count Down' : 'Stopwatch').fontSize(20).margin(20)
      if (this.isCountDown) {
        // 图片资源放在media目录下
        Image($r("app.media.countdown")).width(120).height(120)
        TimerComponent({ counter: 10, changePerSec: -1, showInColor: Color.Red })
      } else {
        // 图片资源放在media目录下
        Image($r("app.media.stopwatch")).width(120).height(120)
        TimerComponent({ counter: 0, changePerSec: +1, showInColor: Color.Black })
      }
      Button(this.isCountDown ? 'Switch to Stopwatch' : 'Switch to Count Down')
        .onClick(() => {
          this.isCountDown = !this.isCountDown
        })
    }.width('100%')
  }
}

// 自定义计时器/倒计时组件
@Component
struct TimerComponent {
  @State counter: number = 0
  private changePerSec: number = -1
  private showInColor: Color = Color.Black
  private timerId: number = -1

  build() {
    Text(`${this.counter}sec`)
      .fontColor(this.showInColor)
      .fontSize(20)
      .margin(20)
  }

  aboutToAppear() {
    this.timerId = setInterval(() => {
      this.counter += this.changePerSec
    }, 1000)
  }

  aboutToDisappear() {
    if (this.timerId > 0) {
      clearTimeout(this.timerId)
      this.timerId = -1
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Image description

Initial Creation and Rendering:

Create a parent component, ParentComp;

Initialize the state variable isCountDown of ParentComp locally;

Execute the build function of ParentComp;

Create a Column component;

a. Create a Text component, set its text display content, and add an instance of the Text component to the Column. b. Determine the if condition and create the element under the true condition;
i. Create a TimerComponent using the given constructor;

ii. Create an Image component and set its image source address.

c. Create a built-in component of the Button and set the corresponding contents.

Status Update:

When a user clicks a button:

The value of the isCountDown state variable of ParentComp is changed to false;

Execute the build function of ParentComp;

The Column component is reused and reinitialized;

Column's subcomponents reuse in-memory objects and reinitialize them;

a. The Text component is reused, reinitialized with the new text content;

b. Determine the if condition, using the element under the false condition;

i. Create a component under false conditions;

ii. Destroy the components under the original true condition;

c. Reuse the Button component and use the new image source address.

2 -> Create a simple view

2.1 -> Build the Stack layout

  1. Create a food name.

Delete the code of the build method of the project template, create a Stack component, and put the Text component in the curly braces of the Stack component to make it a child component of the Stack component. A stack component is a stacked component that can contain one or more subcomponents, and the latter component overrides the previous subcomponent.

@Entry
@Component
struct MyComponent {
  build() {
    Stack() {
      Text('Tomato')
        .fontSize(26)
        .fontWeight(500)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Image description

  1. Picture display of food.

Create an Image component, specify the URL of the Image component, and the Image component and the Text component are required construction parameter components. In order for the Text component to be displayed above the Image component, the Image component must be declared first. Image resources are placed in the rawfile folder under resources, and the form $rawfile('filename') is used when referencing resources under rawfile, and filename is the relative path of the file in the rawfile directory. Currently, $rawfile only supports image resources in the Image control.

@Entry
@Component
struct MyComponent {
  build() {
    Stack() {
      Image($rawfile('Tomato.png'))
      Text('Tomato')
        .fontSize(26)
        .fontWeight(500)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Image description

  1. Access images through resources.

In addition to specifying the image path, you can also use the reference media resource symbol $r to reference the resource, which needs to follow the rules of the resource qualifier in the resources folder. Right-click the resources folder, click New>Resource Directory, and select Resource Type as Media.

Put the Tomato.png in the media folder. You can refer to the application resource in the form of $r('app.type.name'), i.e., $r('app.media.Tomato').

@Entry
@Component
struct MyComponent {
  build() {
    Stack() {
        Image($r('app.media.Tomato'))
            .objectFit(ImageFit.Contain)
            .height(357)
        Text('Tomato')
            .fontSize(26)
            .fontWeight(500)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Set the width and height of the image, and set the objectFit property of the image to ImageFit.Contain, that is, the image is displayed completely within the boundary while maintaining the aspect ratio of the image.

If the Image fills the entire screen, here's why:

The image does not have a width or height set.

The default objectFit property of Image is ImageFit.Cover, i.e. zoom in or out while maintaining the aspect ratio so that it fills the entire display boundary.

@Entry
@Component
struct MyComponent {
  build() {
    Stack() {
        Image($r('app.media.Tomato'))
            .objectFit(ImageFit.Contain)
            .height(357)
        Text('Tomato')
            .fontSize(26)
            .fontWeight(500)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Image description

  1. Set up a food picture and name layout.

Set the alignment of the stack to the bottom start alignment, and the stack is centered alignment by default. Set the alignContent parameter to Alignment.BottomStart for the stack construction parameter. Alignment, like FontWeight, is a built-in enumeration type provided by the framework.

@Entry
@Component
struct MyComponent {
  build() {
    Stack({ alignContent: Alignment.BottomStart }) {
        Image($r('app.media.Tomato'))
            .objectFit(ImageFit.Contain)
            .height(357)
         Text('Tomato')
            .fontSize(26)
            .fontWeight(500)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Image description

  1. Adjust the margin of the Text component so that it is a certain distance from the left and bottom.

margin is a shorthand property that allows you to specify the margins of the four edges uniformly or separately. Here's how to set it up:

1If the parameter is set to Length, the margins of the four edges are specified, for example, margin(20), that is, the margins of the top, right, bottom, and left edges are all 20.
2The parameters are {top?: Length, right?: Length, bottom?: Length, left?: Length}, which specify the margins for the four edges. For example, margin({ left: 26, bottom: 17.4 }) means the left margin is 26 and the bottom margin is 17.4.

@Entry
@Component
struct MyComponent {
  build() {
    Stack({ alignContent: Alignment.BottomStart }) {
        Image($r('app.media.Tomato'))
            .objectFit(ImageFit.Contain)
            .height(357)
        Text('Tomato')
            .fontSize(26)
            .fontWeight(500)
            .margin({left: 26, bottom: 17.4})
    }   
  }
}
Enter fullscreen mode Exit fullscreen mode

Image description

  1. Adjust the structure between components and semantic component names.

Create a page entry component as FoodDetail, create a Column in FoodDetail, and set alignItems(HorizontalAlign.Center) in the horizontal direction. The name of the MyComponent component has been changed to FoodImageDisplay, which is a subcomponent of FoodDetail.

A column is a container component with subcomponents arranged vertically, and the layout is linear in nature, so you can only set the alignment in the direction of the cross axis.

@Component
struct FoodImageDisplay {
  build() {
    Stack({ alignContent: Alignment.BottomStart }) {
      Image($r('app.media.Tomato'))
        .objectFit(ImageFit.Contain)
      Text('Tomato')
        .fontSize(26)
        .fontWeight(500)
        .margin({ left: 26, bottom: 17.4 })
    }
    .height(357)   
  }
}

@Entry
@Component
struct FoodDetail {
  build() {
    Column() {
      FoodImageDisplay()
    }
    .alignItems(HorizontalAlign.Center)
  }
}
Enter fullscreen mode Exit fullscreen mode

2.2 -> Build a Flex layout

You can use Flex Layout to build food ingredient lists, and the advantage of Flex Layout in this scenario is that you can avoid redundant width and height calculations, and set the size of different cells by proportion, which is more flexible.

  1. Create a ContentTable component to make it a child component of the page entry component FoodDetail.
@Component
struct FoodImageDisplay {
  build() {
    Stack({ alignContent: Alignment.BottomStart }) {
      Image($r('app.media.Tomato'))
        .objectFit(ImageFit.Contain)
        .height(357)
      Text('Tomato')
        .fontSize(26)
        .fontWeight(500)
        .margin({ left: 26, bottom: 17.4 })
    }   
  }
}

@Component
struct ContentTable {
  build() {}
}

@Entry
@Component
struct FoodDetail {
  build() {
    Column() {
      FoodImageDisplay()
      ContentTable()
    }
    .alignItems(HorizontalAlign.Center)
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Create a Flex component to showcase both types of Tomato ingredients.

One is Calories, which contain calories; One is Nutrition, which contains protein, fat, carbohydrates and vitamin C.

Create the heat category first. Create a Flex component with a height of 280 and a top, right, and left padding of 30, containing three Text subcomponents representing the category name (Calories), the content name (Calories), and the content value (17kcal). Flex components are arranged horizontally by default.

The FoodImageDisplay code has been omitted and only extended for ContentTable.

@Component
struct ContentTable {
  build() {
    Flex() {
      Text('Calories')
        .fontSize(17.4)
        .fontWeight(FontWeight.Bold)
      Text('Calories')
        .fontSize(17.4)
      Text('17kcal')
        .fontSize(17.4)
    }
    .height(280)
    .padding({ top: 30, right: 30, left: 30 })
  }
}

@Entry
@Component
struct FoodDetail {
  build() {
    Column() {
      FoodImageDisplay()
      ContentTable()
    }
    .alignItems(HorizontalAlign.Center)
  }
}
Enter fullscreen mode Exit fullscreen mode

Image description

  1. Adjust the layout and set the proportion of each part.

The proportion of the classification name (layoutWeight) is 1, and the total proportion of the ingredient name and ingredient content (layoutWeight) is 2. The ingredient name and ingredient content are located in the same Flex, and the ingredient name occupies all the remaining space flexGrow(1).

@Component
struct FoodImageDisplay {
  build() {
    Stack({ alignContent: Alignment.BottomStart }) {
      Image($r('app.media.Tomato'))
        .objectFit(ImageFit.Contain)
        .height(357)
      Text('Tomato')
        .fontSize(26)
        .fontWeight(500)
        .margin({ left: 26, bottom: 17.4 })
    }  
  }
}

@Component
struct ContentTable {
  build() {
    Flex() {
      Text('Calories')
        .fontSize(17.4)
        .fontWeight(FontWeight.Bold)
        .layoutWeight(1)
      Flex() {
        Text('Calories')
          .fontSize(17.4)
          .flexGrow(1)
        Text('17kcal')
          .fontSize(17.4)
      }
      .layoutWeight(2)
    }
    .height(280)
    .padding({ top: 30, right: 30, left: 30 })
  }
}

@Entry
@Component
struct FoodDetail {
  build() {
    Column() {
      FoodImageDisplay()
      ContentTable()
    }
    .alignItems(HorizontalAlign.Center)
  }
}
Enter fullscreen mode Exit fullscreen mode

Image description

  1. Create nutrient classification modeled after calorie classification.

The nutrition component contains four components: Protein, Fat, Carbohydrates and Vitamin C.

Set the outer Flex to FlexDirection.Column vertically, arrange FlexAlign.SpaceInstead equidistant in the main axis direction (vertical direction), and align ItemAlign.Start in the direction of the cross axis (horizontal axis).

@Component
struct ContentTable {
  build() {
    Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) {
      Flex() {
        Text('Calories')
          .fontSize(17.4)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
        Flex() {
          Text('Calories')
            .fontSize(17.4)
            .flexGrow(1)
          Text('17kcal')
            .fontSize(17.4)
        }
        .layoutWeight(2)
      }
      Flex() {
        Text('Nutrition')
          .fontSize(17.4)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
        Flex() {
          Text('Protein')
            .fontSize(17.4)
            .flexGrow(1)
          Text('0.9g')
            .fontSize(17.4)
        }
        .layoutWeight(2)
      }
      Flex() {
        Text(' ')
          .fontSize(17.4)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
        Flex() {
          Text('Fat')
            .fontSize(17.4)
            .flexGrow(1)
          Text('0.2g')
            .fontSize(17.4)
        }
        .layoutWeight(2)
      }
      Flex() {
        Text(' ')
          .fontSize(17.4)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
        Flex() {
          Text('Carbohydrates')
            .fontSize(17.4)
            .flexGrow(1)
          Text('3.9g')
            .fontSize(17.4)
        }
        .layoutWeight(2)
      }
      Flex() {
        Text(' ')
          .fontSize(17.4)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
        Flex() {
          Text('vitaminC')
            .fontSize(17.4)
            .flexGrow(1)
          Text('17.8mg')
            .fontSize(17.4)
        }
        .layoutWeight(2)
      }
    }
    .height(280)
    .padding({ top: 30, right: 30, left: 30 })
  }
}

@Entry
@Component
struct FoodDetail {
    build() {
        Column() {
            FoodImageDisplay()
            ContentTable()
        }
        .alignItems(HorizontalAlign.Center)
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Use custom constructors @builder simplify your code. It can be found that the constituent units in each ingredient list are actually the same UI structure.

Image description

Each constituent unit is currently declared, resulting in duplication and redundancy of code. You can use @ Builder to build custom methods that abstract away the same UI structure declarations. Both the @ Builder deco-grooming method and the Component's build method are designed to declare some UI rendering structure, following the same ArkTS syntax. You can define one or more methods @ Builder decorating, but there must be only one build method for a Component.

Declare the @ Builder modified IngredientItem method in the ContentTable, which is used to declare the category name, ingredient name, and ingredient content UI description.

@Component
struct ContentTable {
  @Builder IngredientItem(title:string, name: string, value: string) {
    Flex() {
      Text(title)
        .fontSize(17.4)
        .fontWeight(FontWeight.Bold)
        .layoutWeight(1)
      Flex({ alignItems: ItemAlign.Center }) {
        Text(name)
          .fontSize(17.4)
          .flexGrow(1)
        Text(value)
          .fontSize(17.4)
      }
      .layoutWeight(2)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

If you call the IngredientItem API in the build method of the ContentTable, you need to use this to call the method in the scope of the Component, so as to distinguish the global method call.

@Component
struct ContentTable {
  ......
  build() {
    Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) {
      this.IngredientItem('Calories', 'Calories', '17kcal')
      this.IngredientItem('Nutrition', 'Protein', '0.9g')
      this.IngredientItem('', 'Fat', '0.2g')
      this.IngredientItem('', 'Carbohydrates', '3.9g')
      this.IngredientItem('', 'VitaminC', '17.8mg')
    }
    .height(280)
    .padding({ top: 30, right: 30, left: 30 })
  }
}
Enter fullscreen mode Exit fullscreen mode

The overall code of the ContentTable component is as follows:

@Component
struct ContentTable {
  @Builder IngredientItem(title:string, name: string, value: string) {
    Flex() {
      Text(title)
        .fontSize(17.4)
        .fontWeight(FontWeight.Bold)
        .layoutWeight(1)
      Flex() {
        Text(name)
          .fontSize(17.4)
          .flexGrow(1)
        Text(value)
          .fontSize(17.4)
      }
      .layoutWeight(2)
    }
  }

  build() {
    Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) {
      this.IngredientItem('Calories', 'Calories', '17kcal')
      this.IngredientItem('Nutrition', 'Protein', '0.9g')
      this.IngredientItem('', 'Fat', '0.2g')
      this.IngredientItem('', 'Carbohydrates', '3.9g')
      this.IngredientItem('', 'VitaminC', '17.8mg')
    }
    .height(280)
    .padding({ top: 30, right: 30, left: 30 })
  }
}

@Entry
@Component
struct FoodDetail {
    build() {
        Column() {
            FoodImageDisplay()
            ContentTable()
        }
        .alignItems(HorizontalAlign.Center)
    }
}
Enter fullscreen mode Exit fullscreen mode

Image description

2.3 -> Build food data models

In the created view, you can go through each piece of information about each food, such as the name of the food, calories, protein, fat, carbs, and vitamin C. Such a form of coding is certainly impractical in actual development, so it is necessary to create a food data model to store and manage the data in a unified manner.

Image description

  1. Create a model folder and create FoodData.ets in the model directory.

Image description

  1. Define the storage model of food data, FoodData, and enumerate variables, including food id, name, category, image, calories, protein, fat, carbohydrates, and vitamin C attributes.

The ArkTS language is an extension of the TS language and also supports the TS syntax.

enum Category  {
  Fruit,
  Vegetable,
  Nut,
  Seafood,
  Dessert
}

let NextId = 0;
class FoodData {
  id: string;
  name: string;
  image: Resource;
  category: Category;
  calories: number;
  protein: number;
  fat: number;
  carbohydrates: number;
  vitaminC: number;

  constructor(name: string, image: Resource, category: Category, calories: number, protein: number, fat: number, carbohydrates: number, vitaminC: number) {
    this.id = `${ NextId++ }`;
    this.name = name;
    this.image = image;
    this.category = category;
    this.calories = calories;
    this.protein = protein;
    this.fat = fat;
    this.carbohydrates = carbohydrates;
    this.vitaminC = vitaminC;
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Save food image resources. In the resources >base> media directory, the food image resource is stored, and the image name is the name of the food.

  2. Create food resource data. Create FoodDataModels.ets in the model folder and declare the food composition group FoodComposition in that page.

In actual development, you can customize more data resources, and when there are many food resources, it is recommended to use LazyForEach to lazily load data.

const FoodComposition: any[] = [
  { 'name': 'Tomato', 'image': $r('app.media.Tomato'), 'category': Category.Vegetable, 'calories': 17, 'protein': 0.9, 'fat': 0.2, 'carbohydrates': 3.9, 'vitaminC': 17.8 },
  { 'name': 'Walnut', 'image': $r('app.media.Walnut'), 'category': Category.Nut, 'calories': 654 , 'protein': 15, 'fat': 65, 'carbohydrates': 14, 'vitaminC': 1.3 }
  ]
Enter fullscreen mode Exit fullscreen mode
  1. Create the initializeOnStartUp method to initialize the array of FoodData. The FoodData and Category defined in FoodData.ets are used in FoodDataModels.ets, so the FoodData class in FoodData.ets needs to be exported, and FoodData and Category should be imported in FoodDataModels.ets.
// FoodData.ets
export enum Category {
 ......
}
export class FoodData {
  ......
}
// FoodDataModels.ets
import { Category, FoodData } from './FoodData'

export function initializeOnStartup(): Array<FoodData> {
  let FoodDataArray: Array<FoodData> = []
  FoodComposition.forEach(item => {
    FoodDataArray.push(new FoodData(item.name, item.image, item.category, item.calories, item.protein, item.fat, item.carbohydrates, item.vitaminC ));
  })
  return FoodDataArray;
}
Enter fullscreen mode Exit fullscreen mode

Now that you've prepared the data resources for the Healthy Eating app, you'll be able to load the data to create a food list page.

2.4 -> Build a food list layout

Use the List component and ForEach to render in a loop to build a food list layout.

  1. Create a new page in the pages directory, FoodCategoryList.ets. Right-click on the pages folder, select "New > Page", and change the page name to "FoodCategoryList".

  2. Create a new FoodList component as the page entry component, and FoodListItem as its child component. The List component is a list component, which is suitable for displaying duplicate data, and its subcomponent is ListItem, which is suitable for displaying units in the list.

@Component
struct FoodListItem {
  build() {}
}

@Entry
@Component
struct FoodList {
  build() {
    List() {
      ListItem() {
        FoodListItem()
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Introduce the FoodData class and the initializeOnStartup method.
import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'
Enter fullscreen mode Exit fullscreen mode
  1. FoodList and FoodListItem component value transfer.

Create a foodItems variable of type FoodData[] in the FoodList component and call the initializeOnStartup method to assign a value to it. Create a member variable of type FoodData in the FoodListItem component. Pass the foodItems[0] of the first element of the parent component's foodItems array as a parameter to the FoodListItem.

import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'

@Component
struct FoodListItem {
  private foodItem: FoodData
  build() {}
}

@Entry
@Component
struct FoodList {
  private foodItems: FoodData[] = initializeOnStartup()
  build() {
    List() {
      ListItem() {
        FoodListItem({ foodItem: this.foodItems[0] })
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Declare the UI layout of the subcomponent FoodListItem. Create a Flex component that contains a thumbnail of the food image, the name of the food, and the calories corresponding to the food.
import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'

@Component
struct FoodListItem {
  private foodItem: FoodData
  build() {
    Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
      Image(this.foodItem.image)
        .objectFit(ImageFit.Contain)
        .height(40)
        .width(40)       
        .margin({ right: 16 })
      Text(this.foodItem.name)
        .fontSize(14)
        .flexGrow(1)
      Text(this.foodItem.calories + ' kcal')
        .fontSize(14)
    }
    .height(64)
    .margin({ right: 24, left:32 })
  }
}

@Entry
@Component
struct FoodList {
  private foodItems: FoodData[] = initializeOnStartup()
  build() {
    List() {
      ListItem() {
        FoodListItem({ foodItem: this.foodItems[0] })
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Image description

  1. Create two FoodListItems. Create two FoodListItems in the List component, passing the first element of the foodItems array, this.foodItems[0], and the second element: this.foodItems[1], respectively.
import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'

@Component
struct FoodListItem {
    private foodItem: FoodData
    build() {
        Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
            Image(this.foodItem.image)
                .objectFit(ImageFit.Contain)
                .height(40)
                .width(40)              
                .margin({ right: 16 })
            Text(this.foodItem.name)
                .fontSize(14)
                .flexGrow(1)
            Text(this.foodItem.calories + ' kcal')
                .fontSize(14)
        }
        .height(64)
        .margin({ right: 24, left:32 })
    }
}

@Entry
@Component
struct FoodList {
  private foodItems: FoodData[] = initializeOnStartup()
  build() {
    List() {
      ListItem() {
        FoodListItem({ foodItem: this.foodItems[0] })
      }
      ListItem() {
        FoodListItem({ foodItem: this.foodItems[1] })
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Image description

  1. It certainly doesn't make sense to create each FoodListItem individually. This requires the introduction of a ForEach loop rendering, which is syntax as follows.
ForEach(
    arr: any[], // Array to be iterated
    itemGenerator: (item: any) => void, // child component generator
    keyGenerator?: (item: any) => string // (optional) Unique key generator, which is recommended.
)
Enter fullscreen mode Exit fullscreen mode

The ForEach group has three parameters, the first parameter is the array that needs to be traversed, the second parameter is the lambda function that generates the child component, and the third parameter is the key-value generator. For performance reasons, even if the third parameter is optional, it is highly recommended for developers. keyGenerator enables development frameworks to better recognize array changes without having to rebuild all nodes due to item changes.

Looping through the foodItems array creates a ListItem component, with each item in the foodItems being passed as a parameter to the FoodListItem component.


ForEach(this.foodItems, item => {
    ListItem() {
        FoodListItem({ foodItem: item })
    }
}, item => item.id.toString())
Enter fullscreen mode Exit fullscreen mode

The overall code is as follows.

import { FoodData } from '../model/FoodData'
import { initializeOnStartup } from '../model/FoodDataModels'

@Component
struct FoodListItem {
  private foodItem: FoodData
  build() {
    Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
      Image(this.foodItem.image)
        .objectFit(ImageFit.Contain)
        .height(40)
        .width(40)     
        .margin({ right: 16 })
      Text(this.foodItem.name)
        .fontSize(14)
        .flexGrow(1)
      Text(this.foodItem.calories + ' kcal')
        .fontSize(14)
    }
    .height(64)
    .margin({ right: 24, left:32 })
  }
}

@Entry
@Component
struct FoodList {
  private foodItems: FoodData[] = initializeOnStartup()
  build() {
    List() {
      ForEach(this.foodItems, item => {
        ListItem() {
          FoodListItem({ foodItem: item })
        }
      }, item => item.id.toString())
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Add a FoodList title.
@Entry
@Component
struct FoodList {
  private foodItems: FoodData[] = initializeOnStartup()
  build() {
    Column() {
      Flex({justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center}) {
        Text('Food List')
          .fontSize(20)
          .margin({ left:20 })
      }
      .height('7%')
      .backgroundColor('#FFf1f3f5')
      List() {
        ForEach(this.foodItems, item => {
          ListItem() {
            FoodListItem({ foodItem: item })
          }
        }, item => item.id.toString())
      }
      .height('93%')
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Image description

2.5 -> Build a food classification grid layout

The Healthy Eating app provides users with two ways to display food on the homepage: list display and grid display. The developer will implement the grid layout of different food categories through tabs.

  1. Introduce the Category enumeration type to the FoodCategoryList page.
import { Category, FoodData } from '../model/FoodData'
Enter fullscreen mode Exit fullscreen mode
  1. Create a FoodCategoryList and a FoodCategory component, where FoodCategoryList is used as a new page entry component, and the initializeOnStartup method is called in the entry component.
@Component
struct FoodList {
  private foodItems: FoodData[]
  build() {
    ......
  }
}

@Component
struct FoodCategory {
  private foodItems: FoodData[]
  build() {
    ......
  }
}

@Entry
@Component
struct FoodCategoryList {
  private foodItems: FoodData[] = initializeOnStartup()
  build() {
    ......
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Create a showList member variable in the FoodCategoryList component to control the rendering switch between the List layout and the Grid layout. You need to use the conditional rendering statement if... else...。
@Entry
@Component
struct FoodCategoryList {
  private foodItems: FoodData[] = initializeOnStartup()
  private showList: boolean = false

  build() {
    Stack() {
      if (this.showList) {
        FoodList({ foodItems: this.foodItems })
      } else {
        FoodCategory({ foodItems: this.foodItems })
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Create an icon in the upper right corner of the page to switch the List/Grid layout. Set the Stack alignment to TopEnd alignment, create an Image component, and set its click event, i.e., showList is negated.
@Entry
@Component
struct FoodCategoryList {
  private foodItems: FoodData[] = initializeOnStartup()
  private showList: boolean = false

  build() {
    Stack({ alignContent: Alignment.TopEnd }) {
      if (this.showList) {
        FoodList({ foodItems: this.foodItems })
      } else {
        FoodCategory({ foodItems: this.foodItems })
      }
      Image($r('app.media.Switch'))
        .height(24)
        .width(24)
        .margin({ top: 15, right: 10 })
        .onClick(() => {
        this.showList = !this.showList
      })
    }.height('100%')
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Add @State decorators. After clicking the switch tab in the upper right corner, the page does not change, this is because showList is not stateful data, and its change will not trigger a refresh of the page. @State decorator needs to be added to it to make it state data, and its change will cause the component it is in to be re-rendered.
@Entry
@Component
struct FoodCategoryList {
  private foodItems: FoodData[] = initializeOnStartup()
  @State private showList: boolean = false

  build() {
    Stack({ alignContent: Alignment.TopEnd }) {
      if (this.showList) {
        FoodList({ foodItems: this.foodItems })
      } else {
        FoodCategory({ foodItems: this.foodItems })
      }
      Image($r('app.media.Switch'))
        .height(24)
        .width(24)
        .margin({ top: 15, right: 10 })
        .onClick(() => {
        this.showList = !this.showList
      })
    }.height('100%')
  }
}
Enter fullscreen mode Exit fullscreen mode

Click the toggle icon and the FoodList component will appear, click again and the FoodList component will disappear.

  1. Create a tab that shows all the foods.

Create a Tabs component and its sub-component TabContent in the FoodCategory component, and set tabBar to All. Set the width of the TabBars to 280 and the layout mode to Scrollable, that is, they can slide after exceeding the total length. Tabs is a container component that allows you to switch between content views through tabs, and each tab corresponds to a content view TabContent.

@Component
struct FoodCategory {
  private foodItems: FoodData[]
  build() {
    Stack() {
      Tabs() {
        TabContent() {}.tabBar('All')
      }
      .barWidth(280)
      .barMode(BarMode.Scrollable)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Image description

  1. Create a FoodGrid component as a child component of TabContent.
@Component
struct FoodGrid {
  private foodItems: FoodData[]
  build() {}
}

@Component
struct FoodCategory {
  private foodItems: FoodData[]
  build() {
    Stack() {
      Tabs() {
        TabContent() {
          FoodGrid({ foodItems: this.foodItems })
        }.tabBar('All')
      }
      .barWidth(280)
      .barMode(BarMode.Scrollable)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Implement a 2*6 grid layout (a total of 12 food data resources).

Create the Grid component and set the number of columnsTemplate('1fr 1fr'), the number of rows('1fr 1fr 1fr'), rowsGap and columnsGap to 8. Create a Scroll component so that it can be sliding.

@Component
struct FoodGrid {
  private foodItems: FoodData[]
  build() {
    Scroll() {
      Grid() {
        ForEach(this.foodItems, (item: FoodData) => {
          GridItem() {}
        }, (item: FoodData) => item.id.toString())
      }
      .rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr')
      .columnsTemplate('1fr 1fr')
      .columnsGap(8)
      .rowsGap(8)
    }
    .scrollBar(BarState.Off)
    .padding({left: 16, right: 16})
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Create a FoodGridItem component that displays the food image, name, and calories, and implements its UI layout, which is a child component of the GridItem. Each FoodGridItem is 184 in height and 8 in row spacing, setting the total Grid height to (184 + 8) * 6 - 8 = 1144.
@Component
struct FoodGridItem {
  private foodItem: FoodData
  build() {
    Column() {
      Row() {
        Image(this.foodItem.image)
          .objectFit(ImageFit.Contain)
          .height(152)
          .width('100%')
      }
      Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
        Text(this.foodItem.name)
          .fontSize(14)
          .flexGrow(1)
          .padding({ left: 8 })
        Text(this.foodItem.calories + 'kcal')
          .fontSize(14)
          .margin({ right: 6 })
      }
      .height(32)
      .width('100%')
      .backgroundColor('#FFe5e5e5')
    }
    .height(184)
    .width('100%')
  }
}

@Component
struct FoodGrid {
  private foodItems: FoodData[]
  build() {
    Scroll() {
      Grid() {
        ForEach(this.foodItems, (item: FoodData) => {
          GridItem() {
            FoodGridItem({foodItem: item})
          }
        }, (item: FoodData) => item.id.toString())
      }
      .rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr')
      .columnsTemplate('1fr 1fr')
      .columnsGap(8)
      .rowsGap(8)
      .height(1144)
    }
    .scrollBar(BarState.Off)
    .padding({ left: 16, right: 16 })
  }
}
Enter fullscreen mode Exit fullscreen mode

Image description

  1. Create tabs that show the Vegetables (Category.Vegetable), Fruits (Category.Fruit), Nuts (Category.Nut), Seafood (Category.SeaFood) and Dessert (Category.Dessert) categories.
@Component
struct FoodCategory {
  private foodItems: FoodData[]
  build() {
    Stack() {
      Tabs() {
        TabContent() {
          FoodGrid({ foodItems: this.foodItems })
        }.tabBar('All')

        TabContent() {
          FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Vegetable)) })
        }.tabBar('Vegetable')

        TabContent() {
          FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Fruit)) })
        }.tabBar('Fruit')

        TabContent() {
          FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Nut)) })
        }.tabBar('Nut')

        TabContent() {
          FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Seafood)) })
        }.tabBar('Seafood')

        TabContent() {
          FoodGrid({ foodItems: this.foodItems.filter(item => (item.category === Category.Dessert)) })
        }.tabBar('Dessert')
      }
      .barWidth(280)
      .barMode(BarMode.Scrollable)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Set the number of rows and height of the Grid for different food classifications. Because the amount of food in different categories is different, it is not possible to use the '1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr' constant to set it in 6 rows.

Create gridRowTemplate and HeightValue member variables, and set the number and height of Grid rows through the member variables.

@Component
struct FoodGrid {
  private foodItems: FoodData[]
  private gridRowTemplate : string = ''
  private heightValue: number
  build() {
    Scroll() {
      Grid() {
        ForEach(this.foodItems, (item: FoodData) => {
          GridItem() {
            FoodGridItem({foodItem: item})
          }
        }, (item: FoodData) => item.id.toString())
      }
      .rowsTemplate(this.gridRowTemplate)
      .columnsTemplate('1fr 1fr')
      .columnsGap(8)
      .rowsGap(8)
      .height(this.heightValue)
    }
    .scrollBar(BarState.Off)
    .padding({left: 16, right: 16})
  }
}
Enter fullscreen mode Exit fullscreen mode

Call the aboutToAppear operation to calculate the number of rows (gridRowTemplate) and height (heightValue).

aboutToAppear() {
  var rows = Math.round(this.foodItems.length / 2);
  this.gridRowTemplate = '1fr '.repeat(rows);
  this.heightValue = rows * 192 - 8;
}
Enter fullscreen mode Exit fullscreen mode

The custom component provides two lifecycle callback APIs: aboutToAppear and aboutToDisappear. The timing of the execution of aboutToAppear is after the custom component is created and before the custom component build method is executed. aboutToDisappear is executed at the time of deinitialization of the custom component.

Image description

@Component
struct FoodGrid {
  private foodItems: FoodData[]
  private gridRowTemplate : string = ''
  private heightValue: number

  aboutToAppear() {
    var rows = Math.round(this.foodItems.length / 2);
    this.gridRowTemplate = '1fr '.repeat(rows);
    this.heightValue = rows * 192 - 8;
  }

  build() {
    Scroll() {
      Grid() {
        ForEach(this.foodItems, (item: FoodData) => {
          GridItem() {
            FoodGridItem({foodItem: item})
          }
        }, (item: FoodData) => item.id.toString())
      }
      .rowsTemplate(this.gridRowTemplate)
      .columnsTemplate('1fr 1fr')
      .columnsGap(8)
      .rowsGap(8)
      .height(this.heightValue)
    }
    .scrollBar(BarState.Off)
    .padding({left: 16, right: 16})
  }
}
Enter fullscreen mode Exit fullscreen mode

Image description

2.6 -> Page redirection and data transfer

2.6.1 -> Page redirect

The declarative UI paradigm provides two mechanisms for jumping between pages:

The routing container component Navigator wraps the routing capability of the page, and after specifying the page target, all the child components wrapped by the container have routing capabilities.

Routing Router API interfaces: By introducing router interfaces on a page, you can call various interfaces of router to implement various operations on page routing.

Let's learn these two jump mechanisms separately to implement the links to the food classification list page and the food detail page.

  1. Click on the FoodListItem to go to the FoodDetail page. Create a Navigator component inside the FoodListItem, so that all of its sub-components have routing capabilities, and the target page is 'pages/FoodDetail'.
@Component
struct FoodListItem {
  private foodItem: FoodData
  build() {
    Navigator({ target: 'pages/FoodDetail' }) {
      Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
        Image(this.foodItem.image)
          .objectFit(ImageFit.Contain)
          .height(40)
          .width(40)         
          .margin({ right: 16 })
        Text(this.foodItem.name)
          .fontSize(14)
          .flexGrow(1)
        Text(this.foodItem.calories + ' kcal')
          .fontSize(14)
      }
      .height(64)
    }
    .margin({ right: 24, left:32 })
  }
}
Enter fullscreen mode Exit fullscreen mode

Image description

  1. Click on the FoodGridItem to go to the FoodDetail page. Call the push API of the router module of the page routing module to push the FoodDetail page to the routing stack to implement page redirection. To use the router routing API, you need to import the router first.
import router from '@ohos.router'

@Component
struct FoodGridItem {
  private foodItem: FoodData
  build() {
    Column() {
      ......
    }
    .height(184)
    .width('100%')
    .onClick(() => {
      router.push({ url: 'pages/FoodDetail' })
    })
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Add an icon to the FoodDetail page that goes back to the food list page. Save the fallback icon Back.png in the resources > base > media folders. Create a custom component PageTitle, which contains the back-up icon and the text of the Food Detail, call the router.back() operation of the route, and pop up the top page of the route stack, that is, return to the previous page.
// FoodDetail.ets
import router from '@ohos.router'

@Component
struct PageTitle {
    build() {
        Flex({ alignItems: ItemAlign.Start }) {
            Image($r('app.media.Back'))
                .width(21.8)
                .height(19.6)
            Text('Food Detail')
                .fontSize(21.8)
                .margin({left: 17.4})
        }
        .height(61)
        .padding({ top: 13, bottom: 15, left: 28.3 })
        .onClick(() => {
            router.back()
        })
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Create a Stack component in the FoodDetail component, including the subcomponents FoodImageDisplay and PageTitle, and set the alignment to the top left alignment TopStart.
@Entry
@Component
struct FoodDetail {
  build() {
    Column() {
      Stack( { alignContent: Alignment.TopStart }) {
        FoodImageDisplay()
        PageTitle()
      }
      ContentTable()
    }
    .alignItems(HorizontalAlign.Center)
  }
}
Enter fullscreen mode Exit fullscreen mode

2.6.2 -> Data transfer between pages

The jump and fallback of the FoodCategoryList page and the FoodDetail page have been completed, but when you click on the different FoodListItem/FoodGridItem, the FoodDetail page that jumps to Tomato is the detailed description of Tomato, because there is no data transfer between the two pages, and you need to use the parameter route.

  1. Set the params attribute in the Navigator of the FoodListItem component, and the params attribute accepts the key-value of the object.
// FoodList.ets
@Component
struct FoodListItem {
  private foodItem: FoodData
  build() {
    Navigator({ target: 'pages/FoodDetail' }) {
      ......
    }
    .params({ foodData: this.foodItem })
  }
}
Enter fullscreen mode Exit fullscreen mode

The router API called by FoodGridItem also has the ability to carry parameter jumps, which is similar to that of Navigator.

router.push({
  url: 'pages/FoodDetail',
  params: { foodData: this.foodItem }
})
Enter fullscreen mode Exit fullscreen mode
  1. Introduce the FoodData class in the FoodDetail page and add a foodItem member variable within the FoodDetail component.
// FoodDetail.ets
import { FoodData } from '../model/FoodData'

@Entry
@Component
struct FoodDetail {
  private foodItem: FoodData
  build() {
    ......
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Obtain the value corresponding to the foodData. Call router.getParams().foodData to get the data corresponding to the foodData carried when you are redirected to the FoodCategoryList page.
@Entry
@Component
struct FoodDetail {
  private foodItem: FoodData = router.getParams()['foodId']

  build() {
    ......
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Refactor the components of the FoodDetail page. When building a view, the food information on the FoodDetail page is a directly declared constant, and now it is re-assigned with the passed FoodData data. The overall FoodDetail.ets code is as follows.
@Component
struct PageTitle {
    build() {
        Flex({ alignItems: ItemAlign.Start }) {
            Image($r('app.media.Back'))
                .width(21.8)
                .height(19.6)
            Text('Food Detail')
                .fontSize(21.8)
                .margin({left: 17.4})
        }
        .height(61)

        .padding({ top: 13, bottom: 15, left: 28.3 })
        .onClick(() => {
            router.back()
        })
    }
}

@Component
struct FoodImageDisplay {
  private foodItem: FoodData
  build() {
    Stack({ alignContent: Alignment.BottomStart }) {
      Image(this.foodItem.image)
        .objectFit(ImageFit.Contain)
      Text(this.foodItem.name)
        .fontSize(26)
        .fontWeight(500)
        .margin({ left: 26, bottom: 17.4 })
    }
    .height(357)

  }
}

@Component
struct ContentTable {
  private foodItem: FoodData

  @Builder IngredientItem(title:string, name: string, value: string) {
    Flex() {
      Text(title)
        .fontSize(17.4)
        .fontWeight(FontWeight.Bold)
        .layoutWeight(1)
      Flex() {
        Text(name)
          .fontSize(17.4)
          .flexGrow(1)
        Text(value)
          .fontSize(17.4)
      }
      .layoutWeight(2)
    }
  }

  build() {
    Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Start }) {
      this.IngredientItem('Calories', 'Calories', this.foodItem.calories + 'kcal')
      this.IngredientItem('Nutrition', 'Protein', this.foodItem.protein + 'g')
      this.IngredientItem('', 'Fat', this.foodItem.fat + 'g')
      this.IngredientItem('', 'Carbohydrates', this.foodItem.carbohydrates + 'g')
      this.IngredientItem('', 'VitaminC', this.foodItem.vitaminC + 'mg')
    }
    .height(280)
    .padding({ top: 30, right: 30, left: 30 })
  }
}

@Entry
@Component
struct FoodDetail {
  private foodItem: FoodData = router.getParams().foodData

  build() {
    Column() {
      Stack( { alignContent: Alignment.TopStart }) {
        FoodImageDisplay({ foodItem: this.foodItem })
        PageTitle()
      }
      ContentTable({ foodItem: this.foodItem })
    }
    .alignItems(HorizontalAlign.Center)
  }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)