最佳实践 - 鸿蒙应用架构设计进阶:基于「百得知识库」ArkTS 的声明式 UI 与响应式状态管理实现路径

百得知识库页面展示

img

开屏页(Advertising):展示广告图片,包含倒计时功能,倒计时结束后自动跳转至主页面,提供用户短暂的品牌展示和加载时间

首页(Index.ets):应用主框架,实现底部Tab导航首页、学习、消息、我的,通过状态管理控制当前显示页面,并负责整体布局和背景设置

学习页(Learn):整合学习工具打卡、目标、学习平台和面试大全等模块入口,以卡片形式展示各类学习功能,提供清晰的学习路径导航

消息页(Message):展示消息统计总数量/未读数量,分类显示系统消息和通知公告,支持下拉刷新,点击可查看详情,提供未读消息计数提示

我的页(Mine):显示用户个人信息头像、昵称,提供编辑个人信息入口,整合功能列表反馈、设置等,处理登录/未登录状态的差异化展示和退出登录逻辑

登录页(LoginPage):实现用户身份认证,支持账号密码输入验证,含用户协议和隐私政策勾选及查看功能

开屏页:广告展示与倒计时跳转机制

img

倒计时控制机制通过状态变量 countDownSeconds(默认3秒)实现,每秒递减一次。

当倒计时结束时,系统会清除定时器以防内存泄漏,从 PreferencesUtil 恢复用户 token 和信息,并通过 router.replaceUrl 跳转到首页,同时在 onPageHide 中清理相关资源

// 展示开屏页面时间,单位秒
@State countDownSeconds: number = CommonConstant.ADVERTISING_TIME;
// 结束时间,单位秒
endTime: number = CommonConstant.ADVERTISING_END_TIME;

/**
 * 生命周期函数
 */
onPageShow() {
  this.endTime = setInterval(async () => {
    if (this.countDownSeconds === CommonConstant.ADVERTISING_END_TIME) {
      // 清除定时器
      clearInterval(this.endTime);
      // 获取对应的token等数据
      const token = PreferencesUtil.getData(CommonConstant.PREFERENCES_NAME, CommonConstant.TOKEN_NAME, '')
      const userInfo = PreferencesUtil.getData(CommonConstant.PREFERENCES_NAME, CommonConstant.USER_INFO, '')
      if (token && userInfo) {
        AppStorage.setOrCreate(CommonConstant.TOKEN_NAME, token)
        AppStorage.setOrCreate(CommonConstant.USER_INFO, JSON.parse(userInfo))
      }
      // 跳转到首页
      router.replaceUrl({ url: RouterConstant.PAGE_INDEX })
    } else {
      this.countDownSeconds--;
    }
  }, 1000);
}

/**
 * 生命周期函数:隐藏页面
 */
onPageHide() {
  // 清除定时任务
  clearInterval(this.endTime);
}

学习模块核心功能

img

打卡定位

img

  • 判断当前位置是否存在且当天未打卡

  • 如果未打卡,则调用 learnClockApi.learnClock 提交打卡信息

  • 显示成功提示后跳转到学习工具页

  • 若当天已打卡,则提示用户无需重复打卡

// 核心打卡功能实现
async clock() {
  if (this.location !== '') {
    if (!this.isClock) {
      // 调用API进行打卡
      await learnClockApi.learnClock({ location: this.location, content: '学习打卡' })
      showToast('打卡成功,已连续打卡200天')
      router.replaceUrl({ url: RouterConstant.VIEWS_LEARN_TOOL })
    } else {
      showToast('今日已打过卡,不需要重复打卡')
    }
  }
}

目标进度统计与数据获取

img

  • 通过 targetInfoApi 获取用户目标进度和分页数据

  • 根据总数判断是否还有更多内容

  • 若 isFlushed 为真,则刷新数据,否则追加记录

  • 更新页面显示状态并重置加载标识,防止重复操作

  • 若 isUpdate 为真,则弹出已更新提示

async getUserTargetData(isFlushed: boolean, isUpdate: boolean) {
  // 获取用户整体目标完成进度统计
  this.countData = await targetInfoApi.getTargetInfoCount()
  // 分页查询用户整体目标
  const pageResult = await targetInfoApi.pageListTargetInfo({ page: this.page, pageSize: this.pageSize })
  this.total = pageResult.total
  // 判断总数据
  if (this.total > this.page * this.pageSize) {
    this.textShow = false
  } else {
    this.textShow = true
  }
  // 判断是否是刷新还是下拉
  isFlushed ? this.targetInfoVos = pageResult.records : this.targetInfoVos.push(...pageResult.records)
  // 展示页面数据
  this.isShow = true
  // 节流,防止用户重复下拉
  this.isLoad = false
  this.isRefreshing = false
  // 是否刷新
  if (isUpdate) {
    showToast("已更新")
  }
}

目标添加滑动功能

img

  • 通过 IBestDialog 弹出对话框添加新目标

  • 验证输入后调用 targetInfoApi.addTargetInfo 保存并刷新列表

  • 列表尾端每个目标提供“完成”和“删除”按钮

  • 完成目标通过 targetInfoApi.completeTargetInfo 更新状态并提示

  • 删除目标通过 IBestDialogUtil 弹出确认框后调用 targetInfoApi.deleteTargetInfo 删除

// 添加目标对话框
IBestDialog({
  visible: $dialogVisible,
  title: "添加目标",
  showCancelButton: true,
  defaultBuilder: (): void => this.formInputContain(),
  beforeClose: async (action) => {
    if (action === 'cancel') {
      return true
    }
    const value = this.inputTargetValue.trim();
    this.formInputError = !value;

    if (this.formInputError) {
      showToast('请输入目标内容');
      return false; // 阻止关闭
    }

    // 添加新目标
    await targetInfoApi.addTargetInfo({ content: value });
    showToast('添加目标成功');

    // 刷新列表
    this.page = 1;
    await this.aboutToAppear();

    return true; // 允许关闭对话框
  }
})

// 列表尾端操作
@Builder
itemEnd(item: TargetInfoVo) {
  Row() {
    // 完成目标按钮
    Button({ type: ButtonType.Circle }) {
      Image($r('app.media.icon_finish'))
        .width(40)
        .aspectRatio(1)
    }
    .onClick(async () => {
      if (item.status === '1') {
        showToast('当前目标已完成,无需重复点击');
        return;
      }
      await targetInfoApi.completeTargetInfo({ id: item.id });
      showToast('目标已完成');
      router.replaceUrl({ url: RouterConstant.VIEWS_LEARN_TARGET });
    }).margin({ right: 10 })

    // 删除目标按钮
    Button({ type: ButtonType.Circle }) {
      Image($r('app.media.icon_delete'))
        .width(40)
        .aspectRatio(1)
    }
    .margin({ right: 10 })
    .onClick(async () => {
      IBestDialogUtil.open({
        title: "提示",
        message: "是否确认删除当前目标?",
        showCancelButton: true,
        onConfirm: async () => {
          await targetInfoApi.deleteTargetInfo({ id: item.id });
          showToast('删除目标成功');
          router.replaceUrl({ url: RouterConstant.VIEWS_LEARN_TARGET });
        }
      })
    })
  }.justifyContent(FlexAlign.SpaceBetween)
}

用户登录与隐私设置

img

img

  • 用户登录成功后,将 token 和用户信息同时保存到内存和设备存储中,实现数据持久化

  • 通过 AppStorage 存入内存,方便应用运行时快速访问

  • 通过 PreferencesUtil 存入设备存储,保证应用重启后仍可读取

  • 调用 userApi.getUserInfo 获取用户信息并同步保存

// 登录成功后数据持久化存储
if (token) {
  // 内存存储
  AppStorage.setOrCreate(CommonConstant.TOKEN_NAME, token)
  // 设备存储
  PreferencesUtil.savaData(CommonConstant.PREFERENCES_NAME, CommonConstant.TOKEN_NAME, token)
  // 获取并存储用户信息
  const userInfo = await userApi.getUserInfo();
  AppStorage.setOrCreate(CommonConstant.USER_INFO, userInfo)
  PreferencesUtil.savaData(CommonConstant.PREFERENCES_NAME, CommonConstant.USER_INFO, JSON.stringify(userInfo))
}

首页内容展示系统

img

分类切换机制

img

  • 通过两个 Column 分别显示“最新”和“最热门”标题

  • 点击标题时切换下划线显示状态以标识选中项

  • 重置分页并触发对应数据加载最新文章或热门文章

Row({ space: 10 }) {
  Column() {
    Text($r('app.string.article_best_new'))
      .textStyles()
    // 选中标题会出现下划线
    if (this.newDividerShow) {
      Divider().dividerStyles()
    }
  }
  .onClick(() => {
    // 展示下划线
    this.hotDividerShow = false
    this.newDividerShow = true
    // 查询最新文章数据
    this.isShow = false
    this.page = 1
    this.getArticleNewDataList(true, true)
  })

  Column() {
    Text($r('app.string.article_best_hot'))
      .textStyles()
    // 选中标题会出现下划线
    if (this.hotDividerShow) {
      Divider().dividerStyles()
    }
  }
  .onClick(() => {
    this.newDividerShow = false
    this.hotDividerShow = true
    // 查询最热门文章数据
    this.isShow = false
    this.page = 1
  })
}

搜索功能实现

img

  • 通过 Search 组件输入关键字

  • 设置占位符和字体样式

  • 用户提交时将关键字编码后赋值给标题

  • 重置分页并调用 getArticleDataList 查询匹配的文章列表

  • 保证搜索结果与界面展示同步更新

// 搜索
Search({ placeholder: '请输入关键字', value: $$this.keyword })
  .placeholderFont({ size: 14 })
  .textFont({ size: 14 })
  .onSubmit(() => {
    // 处理标题
    this.title = encodeURIComponent(this.keyword)
    // 分页查询文章内容
    this.isShow = false
    this.page = 1
    this.getArticleDataList(true)
  })
  .width(CommonConstant.WIDTH_FULL)
  .height(30)

下拉刷新与上拉加载

img

  • 通过 Refresh 包裹 List 渲染文章内容

  • 每个列表项可点击跳转到文章详情页

  • 滚动到底部时判断是否有更多数据,若有则分页加载最新或最热门文章

  • 更新加载状态,防止重复请求

  • 下拉刷新时重置分页和加载状态

  • 重新获取对应分类的文章数据,保证列表展示与数据同步更新

Refresh({ refreshing: $$this.isRefreshing }) {
  // 内容组件
  List() {
    ForEach(this.articleContentList, (item: ArticleContentData) => {
      ListItem() {
        ArticleComponent({ articleContentData: item })
          .onClick(() => {
            // 路由到内容详情页
            router.pushUrl({
              url: RouterConstant.VIEWS_HOME_ARTICLE_INFO, params: {
                "articleId": item.id
              }
            })
          })
      }
    })
  }
  .onReachEnd(() => {
    if (!this.isLoad) {
      if (this.total > this.page * this.pageSize) {
        this.isLoad = true
        this.page++
        if (this.newDividerShow) {
          // 分页查询最新文章数据
          this.getArticleNewDataList(false, false)
        }
        if (this.hotDividerShow) {
          // 分页查询最热门文章数据
          this.getArticleHotDataList(false, false)
        }
      } else {
        this.textShow = true
      }
    }
  })
}
.onRefreshing(() => {
  if (!this.isLoad) {
    this.isLoad = true
    this.textShow = false
    // 页面恢复到1
    this.page = 1
    if (this.newDividerShow) {
      // 分页查询最新文章数据
      this.getArticleNewDataList(true, true)
    }
    if (this.hotDividerShow) {
      // 分页查询最热门文章数据
      this.getArticleHotDataList(true, true)
    }
  }
})

多级分类与筛选机制

状态设计与管理分类UI交互设计

img

  • 通过状态变量记录当前选中分类(全部、基础、进阶、高级)

  • 点击不同分类时切换下划线显示状态

  • 重置分页

  • 将选中的难度赋值给 difficultyCategory

  • 调用 getArticleList 重新加载对应分类的文章列表

  • 实现界面与数据同步更新的条件筛选效果

// 文章难度分类相关状态
@State allDividerShow: boolean = true
@State basicDividerShow: boolean = false
@State advancedDividerShow: boolean = false
@State seniorDividerShow: boolean = false

// 当前难度分类值
@State difficultyCategory: string = ''

// 当前内容分类(从路由参数获取)
@State contentCategory: string = ''

// 条件筛选标题栏
Row({ space: 10 }) {
  Column() {
    Text($r('app.string.all_condition'))
      .textStyles()
    // 选中标题会出现下划线
    if (this.allDividerShow) {
      Divider().dividerStyles()
    }
  }
  .onClick(() => {
    // 更新选中状态
    this.allDividerShow = true
    this.basicDividerShow = false
    this.advancedDividerShow = false
    this.seniorDividerShow = false
    // 重置分页并重新加载数据
    this.isShow = false
    this.page = 1
    this.getArticleList(true, true)
  })

  Column() {
    Text($r('app.string.basic_condition'))
      .textStyles()
    if (this.basicDividerShow) {
      Divider().dividerStyles()
    }
  }
  .onClick(() => {
    this.allDividerShow = false
    this.basicDividerShow = true
    this.advancedDividerShow = false
    this.seniorDividerShow = false
    this.isShow = false
    this.page = 1
    this.difficultyCategory = 'basic'
    this.getArticleList(true, true)
  })

  Column() {
    Text($r('app.string.advanced_condition'))
      .textStyles()
    if (this.advancedDividerShow) {
      Divider().dividerStyles()
    }
  }
  .onClick(() => {
    this.allDividerShow = false
    this.basicDividerShow = false
    this.advancedDividerShow = true
    this.seniorDividerShow = false
    this.isShow = false
    this.page = 1
    this.difficultyCategory = 'advanced'
    this.getArticleList(true, true)
  })

  Column() {
    Text($r('app.string.senior_condition'))
      .textStyles()
    if (this.seniorDividerShow) {
      Divider().dividerStyles()
    }
  }
  .onClick(() => {
    this.allDividerShow = false
    this.basicDividerShow = false
    this.advancedDividerShow = false
    this.seniorDividerShow = true
    this.isShow = false
    this.page = 1
    this.difficultyCategory = 'senior'
    this.getArticleList(true, true)
  })
}

总结

img

通过 "百得知识库" 项目充分体现鸿蒙应用在组件化设计、状态驱动渲染与分布式数据管理上的优势,借助 ArkTS 与方舟框架的声明式编程模型,结合 @State、AppStorage、Router 等特性,实现前后端解耦与流畅交互,依托分布式架构与组件复用能力,开发者可以在统一代码体系下快速构建多端协同场景,实现 "一次开发,多端部署"。

👉如果你也在探索鸿蒙轻量化应用开发,或是想第一时间 get 鸿蒙新特性适配,点击链接加入和开发者们一起交流经验吧!https://work.weixin.qq.com/gm/afdd8c7246e72c0e94abdbd21bc9c5c1

Logo

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

更多推荐