鸿蒙开发中状态混乱、UI不刷新、数据不同步?本文用15分钟带你彻底搞懂@State、@Prop、@Link、@Provide/Consume四大核心装饰器,附完整培训班管理系统实战代码和踩坑记录,让你的鸿蒙App数据流转从此丝滑!


一、学员信息状态管理

1.1 @State装饰器详解

@State装饰器用于管理组件内部状态,当状态变化时,UI会自动刷新。

// components/student/StudentManager.ets
@Component
struct StudentManager {
  @State studentList: Student[] = [];
  @State selectedStudent: Student | null = null;
  @State isLoading: boolean = false;
  @State searchKeyword: string = '';

  async aboutToAppear() {
    await this.loadStudents();
  }

  async loadStudents() {
    this.isLoading = true;
    try {
      this.studentList = await StudentService.getAllStudents();
    } catch (error) {
      console.error('加载学员列表失败:', error);
    } finally {
      this.isLoading = false;
    }
  }

  async searchStudents(keyword: string) {
    this.searchKeyword = keyword;
    if (keyword.length === 0) {
      await this.loadStudents();
      return;
    }

    this.isLoading = true;
    try {
      this.studentList = await StudentService.searchStudents(keyword);
    } catch (error) {
      console.error('搜索学员失败:', error);
    } finally {
      this.isLoading = false;
    }
  }

  build() {
    Column() {
      // 搜索栏
      SearchBar({
        searchText: $searchKeyword,
        searchCallback: (keyword: string) => {
          this.searchStudents(keyword);
        }
      })

      // 学员列表
      if (this.isLoading) {
        LoadingView()
      } else {
        List({ space: 12 }) {
          ForEach(this.studentList, (student: Student) => {
            ListItem() {
              StudentCard({
                student: student,
                isSelected: this.selectedStudent?.id === student.id,
                clickCallback: (selected: Student) => {
                  this.selectedStudent = selected;
                }
              })
            }
          }, (student: Student) => student.id)
        }
        .layoutWeight(1)
      }
    }
    .width('100%')
    .height('100%')
  }
}

1.2 @ObjectLink装饰器详解

@ObjectLink装饰器用于监听对象内部属性的变化,适用于复杂对象的状态管理。

// model/StudentObserved.ets
@Observed
class StudentObserved implements Student {
  id: string = '';
  name: string = '';
  phone: string = '';
  email: string = '';
  gender: number = 0;
  birthday: string = '';
  address: string = '';
  avatar: string = '';
  age: number = 0;
  courses: string[] = [];
  status: number = 0;
  createTime: string = '';
  updateTime: string = '';

  constructor(student?: Student) {
    if (student) {
      Object.assign(this, student);
    }
  }

  updateName(name: string) {
    this.name = name;
    this.updateTime = Utils.formatDate(new Date());
  }

  updatePhone(phone: string) {
    this.phone = phone;
    this.updateTime = Utils.formatDate(new Date());
  }

  addCourse(courseId: string) {
    if (!this.courses.includes(courseId)) {
      this.courses.push(courseId);
      this.updateTime = Utils.formatDate(new Date());
    }
  }

  removeCourse(courseId: string) {
    const index = this.courses.indexOf(courseId);
    if (index > -1) {
      this.courses.splice(index, 1);
      this.updateTime = Utils.formatDate(new Date());
    }
  }
}
// components/student/StudentDetailCard.ets
@Component
export struct StudentDetailCard {
  @ObjectLink student: StudentObserved;
  editCallback: () => void = () => {};

  build() {
    Column() {
      // 头像区域
      Stack() {
        Image(this.student.avatar || $r('app.media.ic_default_avatar'))
          .width(80)
          .height(80)
          .borderRadius(40)

        // 编辑按钮
        Image($r('app.media.ic_edit'))
          .width(24)
          .height(24)
          .fillColor('#FFFFFF')
          .backgroundColor('#007DFF')
          .borderRadius(12)
          .position({ x: 56, y: 56 })
          .onClick(() => {
            this.editCallback();
          })
      }
      .width(80)
      .height(80)

      // 姓名
      Text(this.student.name)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .margin({ top: 12 })

      // 状态标签
      Text(this.student.status === 0 ? '在读' : '已毕业')
        .fontSize(12)
        .fontColor('#FFFFFF')
        .backgroundColor(this.student.status === 0 ? '#52C41A' : '#999999')
        .borderRadius(4)
        .padding({ left: 8, right: 8, top: 2, bottom: 2 })
        .margin({ top: 8 })

      // 信息列表
      Column() {
        this.InfoItem('手机号', this.student.phone)
        this.InfoItem('邮箱', this.student.email)
        this.InfoItem('性别', this.student.gender === 1 ? '男' : '女')
        this.InfoItem('年龄', `${this.student.age}岁`)
        this.InfoItem('地址', this.student.address)
        this.InfoItem('已报课程', `${this.student.courses.length}门`)
      }
      .width('100%')
      .margin({ top: 16 })
      .padding(16)
      .backgroundColor('#F8F8F8')
      .borderRadius(8)
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
  }

  @Builder
  InfoItem(label: string, value: string) {
    Row() {
      Text(label)
        .fontSize(14)
        .fontColor('#666666')
        .width(80)

      Text(value)
        .fontSize(14)
        .fontColor('#333333')
        .layoutWeight(1)
    }
    .width('100%')
    .height(40)
    .borderWidth({ bottom: 1 })
    .borderColor('#F0F0F0')
  }
}

二、课程数据状态流转

2.1 @Prop装饰器详解

@Prop装饰器用于父子组件之间的单向数据传递,父组件数据变化会同步到子组件。

// components/course/CourseList.ets
@Component
export struct CourseList {
  @State courseList: Course[] = [];
  @State selectedFilter: string = 'all';

  build() {
    Column() {
      // 筛选标签
      this.FilterTabs()

      // 课程列表
      List({ space: 12 }) {
        ForEach(this.courseList, (course: Course) => {
          ListItem() {
            CourseCard({
              course: course,
              isEnrolled: this.isCourseEnrolled(course.id),
              enrollCallback: (courseId: string) => {
                this.handleEnroll(courseId);
              }
            })
          }
        }, (course: Course) => course.id)
      }
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  FilterTabs() {
    Row() {
      this.FilterTab('all', '全部')
      this.FilterTab('not_started', '未开始')
      this.FilterTab('in_progress', '进行中')
      this.FilterTab('ended', '已结束')
    }
    .width('100%')
    .height(48)
    .backgroundColor('#FFFFFF')
    .justifyContent(FlexAlign.SpaceAround)
  }

  @Builder
  FilterTab(filter: string, label: string) {
    Column() {
      Text(label)
        .fontSize(14)
        .fontColor(this.selectedFilter === filter ? '#007DFF' : '#666666')
        .fontWeight(this.selectedFilter === filter ? FontWeight.Bold : FontWeight.Normal)

      // 选中指示器
      if (this.selectedFilter === filter) {
        Divider()
          .width(24)
          .height(2)
          .color('#007DFF')
          .margin({ top: 4 })
      }
    }
    .onClick(() => {
      this.selectedFilter = filter;
      this.filterCourses();
    })
  }

  filterCourses() {
    // 根据筛选条件过滤课程
  }

  isCourseEnrolled(courseId: string): boolean {
    // 检查是否已报名
    return false;
  }

  handleEnroll(courseId: string) {
    // 处理报名逻辑
  }
}
// components/course/CourseCard.ets
@Component
export struct CourseCard {
  @Prop course: Course;
  @Prop isEnrolled: boolean = false;
  enrollCallback: (courseId: string) => () => void = () => () => {};

  build() {
    Column() {
      // 课程封面
      Image(this.course.coverImage || $r('app.media.ic_default_course'))
        .width('100%')
        .height(120)
        .objectFit(ImageFit.Cover)
        .borderRadius({ topLeft: 8, topRight: 8 })

      // 课程信息
      Column() {
        // 课程名称
        Text(this.course.name)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

        // 课程描述
        Text(this.course.description)
          .fontSize(12)
          .fontColor('#666666')
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .margin({ top: 4 })

        // 课程信息行
        Row() {
          // 授课老师
          Row() {
            Image($r('app.media.ic_teacher'))
              .width(16)
              .height(16)
              .fillColor('#999999')

            Text(this.course.teacher)
              .fontSize(12)
              .fontColor('#999999')
              .margin({ left: 4 })
          }

          // 报名人数
          Row() {
            Image($r('app.media.ic_users'))
              .width(16)
              .height(16)
              .fillColor('#999999')

            Text(`${this.course.currentStudents}/${this.course.maxStudents}`)
              .fontSize(12)
              .fontColor('#999999')
              .margin({ left: 4 })
          }
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .margin({ top: 8 })

        // 价格和状态
        Row() {
          Text(`¥${this.course.price}`)
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FF6B6B')

          if (this.isEnrolled) {
            Text('已报名')
              .fontSize(12)
              .fontColor('#52C41A')
              .backgroundColor('#F6FFED')
              .borderRadius(4)
              .padding({ left: 8, right: 8, top: 2, bottom: 2 })
          } else {
            Button('报名')
              .width(64)
              .height(28)
              .fontSize(12)
              .backgroundColor('#007DFF')
              .borderRadius(14)
              .onClick(() => {
                this.enrollCallback(this.course.id);
              })
          }
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)
        .margin({ top: 8 })
      }
      .padding(12)
    }
    .width('100%')
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
    .shadow({
      radius: 4,
      color: 'rgba(0, 0, 0, 0.1)',
      offsetX: 0,
      offsetY: 2
    })
  }
}

三、报名流程状态控制

3.1 @Link装饰器详解

@Link装饰器用于父子组件之间的双向数据绑定,子组件修改会同步到父组件。

// components/enrollment/EnrollmentForm.ets
@Component
export struct EnrollmentForm {
  @Link selectedStudentId: string;
  @Link selectedCourseId: string;
  @Link remark: string;
  @Link isSubmitting: boolean;

  private studentList: Student[] = [];
  private courseList: Course[] = [];

  async aboutToAppear() {
    await this.loadData();
  }

  async loadData() {
    this.studentList = await StudentService.getAllStudents();
    this.courseList = await CourseService.getAllCourses();
  }

  build() {
    Column() {
      // 选择学员
      this.SelectorSection('选择学员', this.studentList, this.selectedStudentId, (id: string) => {
        this.selectedStudentId = id;
      })

      // 选择课程
      this.SelectorSection('选择课程', this.courseList, this.selectedCourseId, (id: string) => {
        this.selectedCourseId = id;
      })

      // 备注输入
      Column() {
        Text('备注信息')
          .fontSize(14)
          .fontColor('#666666')
          .alignSelf(ItemAlign.Start)

        TextInput({ placeholder: '请输入备注信息(选填)' })
          .width('100%')
          .height(80)
          .backgroundColor('#F8F8F8')
          .borderRadius(8)
          .margin({ top: 8 })
          .onChange((value: string) => {
            this.remark = value;
          })
      }
      .width('100%')
      .margin({ top: 16 })
    }
    .width('100%')
    .padding(16)
  }

  @Builder
  SelectorSection<T extends { id: string; name: string }>(
    title: string,
    list: T[],
    selectedId: string,
    onSelect: (id: string) => void
  ) {
    Column() {
      Text(title)
        .fontSize(14)
        .fontColor('#666666')
        .alignSelf(ItemAlign.Start)

      List() {
        ForEach(list, (item: T) => {
          ListItem() {
            Row() {
              Text(item.name)
                .fontSize(14)
                .fontColor(selectedId === item.id ? '#007DFF' : '#333333')
                .fontWeight(selectedId === item.id ? FontWeight.Bold : FontWeight.Normal)

              if (selectedId === item.id) {
                Image($r('app.media.ic_check'))
                  .width(16)
                  .height(16)
                  .fillColor('#007DFF')
              }
            }
            .width('100%')
            .height(48)
            .padding({ left: 12, right: 12 })
            .backgroundColor(selectedId === item.id ? '#E6F7FF' : '#FFFFFF')
            .borderRadius(8)
            .margin({ top: 4 })
            .onClick(() => {
              onSelect(item.id);
            })
          }
        }, (item: T) => item.id)
      }
      .width('100%')
      .height(200)
      .margin({ top: 8 })
    }
    .width('100%')
    .margin({ top: 16 })
  }
}
// pages/EnrollmentPage.ets
@Entry
@Component
struct EnrollmentPage {
  @State selectedStudentId: string = '';
  @State selectedCourseId: string = '';
  @State remark: string = '';
  @State isSubmitting: boolean = false;

  build() {
    Column() {
      // 标题栏
      TitleBar({ title: '学员报名', showBack: true })

      // 表单
      EnrollmentForm({
        selectedStudentId: $selectedStudentId,
        selectedCourseId: $selectedCourseId,
        remark: $remark,
        isSubmitting: $isSubmitting
      })

      // 提交按钮
      Button(this.isSubmitting ? '提交中...' : '确认报名')
        .width('90%')
        .height(48)
        .backgroundColor(this.canSubmit() ? '#007DFF' : '#CCCCCC')
        .borderRadius(24)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 24, top: 'auto' })
        .enabled(this.canSubmit() && !this.isSubmitting)
        .onClick(() => {
          this.submitEnrollment();
        })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  canSubmit(): boolean {
    return this.selectedStudentId.length > 0 && this.selectedCourseId.length > 0;
  }

  async submitEnrollment() {
    if (!this.canSubmit()) return;

    this.isSubmitting = true;

    try {
      const enrollment: Enrollment = {
        id: Utils.generateId(),
        studentId: this.selectedStudentId,
        courseId: this.selectedCourseId,
        enrollTime: Utils.formatDate(new Date()),
        status: 0,
        remark: this.remark,
        createTime: Utils.formatDate(new Date()),
        updateTime: Utils.formatDate(new Date())
      };

      await EnrollmentService.createEnrollment(enrollment);
      Utils.showToast('报名成功');
      router.back();
    } catch (error) {
      Utils.showToast('报名失败,请重试');
    } finally {
      this.isSubmitting = false;
    }
  }
}

四、@State、@Prop、@Link实战应用

4.1 状态管理最佳实践

// viewmodel/StudentViewModel.ets
export class StudentViewModel {
  @State studentList: Student[] = [];
  @State selectedStudent: Student | null = null;
  @State isLoading: boolean = false;
  @State errorMessage: string = '';

  private studentService: StudentService;

  constructor() {
    this.studentService = new StudentService();
  }

  async loadStudents(): Promise<void> {
    this.isLoading = true;
    this.errorMessage = '';

    try {
      this.studentList = await this.studentService.getAllStudents();
    } catch (error) {
      this.errorMessage = '加载学员列表失败,请重试';
      console.error('加载学员列表失败:', error);
    } finally {
      this.isLoading = false;
    }
  }

  async searchStudents(keyword: string): Promise<void> {
    if (keyword.length === 0) {
      await this.loadStudents();
      return;
    }

    this.isLoading = true;
    this.errorMessage = '';

    try {
      this.studentList = await this.studentService.searchStudents(keyword);
    } catch (error) {
      this.errorMessage = '搜索失败,请重试';
      console.error('搜索学员失败:', error);
    } finally {
      this.isLoading = false;
    }
  }

  async deleteStudent(studentId: string): Promise<boolean> {
    try {
      await this.studentService.deleteStudent(studentId);
      this.studentList = this.studentList.filter(s => s.id !== studentId);
      if (this.selectedStudent?.id === studentId) {
        this.selectedStudent = null;
      }
      Utils.showToast('删除成功');
      return true;
    } catch (error) {
      Utils.showToast('删除失败,请重试');
      return false;
    }
  }

  selectStudent(student: Student): void {
    this.selectedStudent = student;
  }

  clearSelection(): void {
    this.selectedStudent = null;
  }
}

4.2 状态管理流程图

┌─────────────────────────────────────────────────────────────┐
│                     状态管理流程                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐              │
│  │ @State   │───▶│ @Prop    │───▶│ @Link    │              │
│  │ 组件内部 │    │ 父传子   │    │ 双向绑定 │              │
│  └──────────┘    └──────────┘    └──────────┘              │
│       │               │               │                     │
│       ▼               ▼               ▼                     │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐              │
│  │ UI刷新   │    │ UI刷新   │    │ UI刷新   │              │
│  └──────────┘    └──────────┘    └──────────┘              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

五、跨组件数据共享最佳实践

5.1 @Provide/@Consume装饰器详解

@Provide/@Consume装饰器用于跨组件层级的数据共享,无需逐层传递。

// context/StudentContext.ets
@Observed
export class StudentContext {
  studentList: Student[] = [];
  selectedStudent: Student | null = null;
  isLoading: boolean = false;

  updateStudentList(list: Student[]) {
    this.studentList = list;
  }

  selectStudent(student: Student) {
    this.selectedStudent = student;
  }

  clearSelection() {
    this.selectedStudent = null;
  }
}
// pages/StudentManagement.ets
import { StudentContext } from '../context/StudentContext';

@Entry
@Component
struct StudentManagement {
  @Provide('studentContext') context: StudentContext = new StudentContext();

  build() {
    Column() {
      // 标题栏
      TitleBar({ title: '学员管理' })

      // 内容区域
      Row() {
        // 左侧列表
        StudentListPanel()
          .width('40%')
          .height('100%')

        // 右侧详情
        StudentDetailPanel()
          .layoutWeight(1)
          .height('100%')
      }
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
}
// components/student/StudentListPanel.ets
@Component
export struct StudentListPanel {
  @Consume('studentContext') context: StudentContext;

  build() {
    Column() {
      // 搜索栏
      SearchBar({ searchText: '', searchCallback: (keyword: string) => {
        this.searchStudents(keyword);
      }})

      // 学员列表
      if (this.context.isLoading) {
        LoadingView()
      } else {
        List({ space: 8 }) {
          ForEach(this.context.studentList, (student: Student) => {
            ListItem() {
              StudentCard({
                student: student,
                isSelected: this.context.selectedStudent?.id === student.id,
                clickCallback: (selected: Student) => {
                  this.context.selectStudent(selected);
                }
              })
            }
          }, (student: Student) => student.id)
        }
        .layoutWeight(1)
      }
    }
    .width('100%')
    .height('100%')
  }

  async searchStudents(keyword: string) {
    // 搜索逻辑
  }
}
// components/student/StudentDetailPanel.ets
@Component
export struct StudentDetailPanel {
  @Consume('studentContext') context: StudentContext;

  build() {
    Column() {
      if (this.context.selectedStudent) {
        // 显示学员详情
        StudentDetailCard({
          student: this.context.selectedStudent,
          editCallback: () => {
            this.editStudent();
          }
        })
      } else {
        // 空状态
        EmptyView({ message: '请选择学员查看详情' })
      }
    }
    .width('100%')
    .height('100%')
    .padding(16)
  }

  editStudent() {
    // 编辑学员逻辑
  }
}

5.2 状态管理架构图

┌─────────────────────────────────────────────────────────────┐
│                    状态管理架构                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                  @Provide                            │   │
│  │                  (根组件)                             │   │
│  └─────────────────────────────────────────────────────┘   │
│                           │                                 │
│           ┌───────────────┼───────────────┐                │
│           ▼               ▼               ▼                │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐       │
│  │ @Consume     │ │ @Consume     │ │ @Consume     │       │
│  │ (列表组件)   │ │ (详情组件)   │ │ (编辑组件)   │       │
│  └──────────────┘ └──────────────┘ └──────────────┘       │
│           │               │               │                │
│           ▼               ▼               ▼                │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐       │
│  │ @State       │ │ @State       │ │ @State       │       │
│  │ (局部状态)   │ │ (局部状态)   │ │ (局部状态)   │       │
│  └──────────────┘ └──────────────┘ └──────────────┘       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

六、踩坑记录

6.1 @State状态不更新

问题:@State装饰的数组,修改元素后UI不刷新。

原因:直接修改数组元素不会触发UI更新。

解决方案

// 错误方式
this.list[0].name = '新名称';

// 正确方式
this.list = [...this.list];
this.list[0].name = '新名称';

6.2 @Prop单向数据流

问题:子组件修改@Prop属性后,父组件数据未同步。

原因:@Prop是单向数据流,子组件修改不会影响父组件。

解决方案

// 使用@Link实现双向绑定
@Link selectedId: string;

// 或者使用回调函数
selectCallback: (id: string) => void = () => {};

6.3 @Provide/@Consume作用域

问题:@Provide/@Consume在不同页面之间不生效。

原因:@Provide/@Consume只在同一组件树内有效。

解决方案

// 使用AppStorage实现全局状态
AppStorage.SetOrCreate('globalState', new GlobalState());

// 在组件中使用
@StorageLink('globalState') globalState: GlobalState;

七、总结与预告

本文要点回顾

  1. @State装饰器:管理组件内部状态,触发UI刷新

  2. @ObjectLink装饰器:监听对象内部属性变化

  3. @Prop装饰器:父子组件单向数据传递

  4. @Link装饰器:父子组件双向数据绑定

  5. @Provide/@Consume装饰器:跨组件层级数据共享

  6. 最佳实践:状态管理架构设计和常见问题解决

下期预告

下期我们将深入讲解数据持久化篇,包括:

  • 学员信息本地存储

  • 课程数据管理

  • 网络请求与API对接

  • 数据缓存策略

互动引导

如果本文对你有帮助,请点赞、收藏、关注!有任何问题欢迎在评论区留言,我会及时回复。


系列文章导航

  • 第1篇:项目架构篇

  • 第2篇:UI界面篇

  • 第3篇:状态管理篇(本文)

  • 第4篇:数据持久化篇

  • 第5篇:性能优化篇

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐