前言

网络失败只弹一个 Toast,看起来省事,真实项目里很容易坑用户。Toast 消失太快,用户没看清;列表又没有内容,他也不知道是网络断了、接口挂了,还是页面本来就空。

我现在写 HarmonyOS7 页面时,会把网络错误放进页面状态里。Toast 可以作为补充,但主反馈应该留在页面上:错误说明、重试按钮、旧数据保留、上次更新时间。只要失败会影响页面内容,就不要只靠 Toast。

一张适合技术教程文章的手绘笔记风信息图,主题是 HarmonyOS 7 页面不能只靠 Toast 提

为什么这个地方最容易做得很敷衍

因为弹一个 Toast 实在太快了。代码少、效果也立刻能看到,所以很多初学者会下意识觉得“提示已经有了,这事就算做完了”。

但网络错误不是一个轻飘飘的提醒,它会直接影响用户还能不能继续用这个页面。列表没出来、数据没更新、按钮点了没反应,这些都不是一个两秒钟消失的小提示能交代清楚的。

所以这篇文章真正想解决的,不是“怎么弹提示”,而是“失败之后页面到底该怎么接住用户”。

先把错误分层

网络错误不是一个统一状态。首次加载失败和刷新失败,页面处理方式完全不同。

场景页面反馈用户动作
首次加载失败显示错误面板点击重试
已有数据刷新失败保留旧数据,顶部提示稍后再试
接口返回空数据显示空状态去创建或调整条件
请求中展示加载态等待

这几个状态不能全都写成“加载失败”。用户看到的不是技术原因,而是下一步能不能继续操作。

一张手绘笔记风流程图,展示网络请求失败后的页面状态分层。流程从“请求中”开始,分支到“首次加载失败”

先把页面目标想清楚

在真正写代码之前,先别急着盯着 API。更有用的做法是先想清楚:请求失败之后,用户眼前应该剩下什么,是整个页面报错,还是保留旧数据继续可用。

像成员列表这种页面,用户最在意的通常不是“到底抛了什么异常”,而是“我现在还能不能继续看内容,能不能重试”。所以状态设计时,应该优先围绕这两个问题来拆。

对小白来说,这一步能帮你从“catch 里塞一句提示”进阶到“页面状态怎么跟业务动作对应”。

完整 ArkTS 示例

下面示例是一个成员列表。首次失败时显示错误面板;如果已经有数据,再刷新失败时保留旧列表,只在顶部提示。

import { http } from '@kit.NetworkKit'
import { promptAction } from '@kit.ArkUI'

interface UserItem21 {
  id: number
  name: string
  role: string
}

@Entry
@Component
struct NetworkErrorPage {
  @State loading: boolean = false
  @State errorMessage: string = ''
  @State lastUpdate: string = '尚未加载'
  @State users: UserItem21[] = []

  aboutToAppear(): void {
    this.loadUsers()
  }

  async loadUsers(): Promise<void> {
    if (this.loading) {
      return
    }
    this.loading = true
    this.errorMessage = ''

    const request = http.createHttp()
    try {
      const result = await request.request('https://example.com/api/users', {
        method: http.RequestMethod.GET,
        connectTimeout: 6000,
        readTimeout: 6000
      })

      if (result.responseCode >= 200 && result.responseCode < 300) {
        this.users = [
          { id: 1, name: '林澈', role: '产品经理' },
          { id: 2, name: '周南', role: 'ArkTS 开发' },
          { id: 3, name: '陈予', role: '测试工程师' }
        ]
        this.lastUpdate = '刚刚更新'
      } else {
        this.errorMessage = `服务暂时不可用,状态码 ${result.responseCode}`
      }
    } catch (err) {
      this.errorMessage = '网络连接失败,请检查网络后重试'
      promptAction.showToast({ message: '加载失败,页面已保留错误信息' })
    } finally {
      request.destroy()
      this.loading = false
    }
  }

  @Builder
  ErrorPanel() {
    Column({ space: 12 }) {
      Text('没有加载成功')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
      Text(this.errorMessage)
        .fontSize(14)
        .fontColor('#666666')
        .textAlign(TextAlign.Center)
      Button('重新加载')
        .onClick(() => this.loadUsers())
    }
    .width('100%')
    .padding(24)
    .backgroundColor('#FFF7F5')
    .borderRadius(12)
  }

  build() {
    Column({ space: 16 }) {
      Row() {
        Column() {
          Text('成员列表').fontSize(24).fontWeight(FontWeight.Bold)
          Text(`更新时间:${this.lastUpdate}`).fontSize(12).fontColor('#888888')
        }
        .alignItems(HorizontalAlign.Start)
        Blank()
        Button(this.loading ? '加载中' : '刷新')
          .enabled(!this.loading)
          .onClick(() => this.loadUsers())
      }
      .width('100%')

      if (this.errorMessage.length > 0 && this.users.length === 0) {
        this.ErrorPanel()
      } else {
        if (this.errorMessage.length > 0) {
          Text(this.errorMessage)
            .fontSize(13)
            .fontColor('#B3261E')
            .padding(10)
            .width('100%')
            .backgroundColor('#FFF0EE')
            .borderRadius(8)
        }

        List({ space: 10 }) {
          ForEach(this.users, (item: UserItem21) => {
            ListItem() {
              Row() {
                Column() {
                  Text(item.name).fontSize(17).fontWeight(FontWeight.Medium)
                  Text(item.role).fontSize(13).fontColor('#777777')
                }
                .alignItems(HorizontalAlign.Start)
                Blank()
                Text(`#${item.id}`).fontSize(12).fontColor('#999999')
              }
              .width('100%')
              .padding(14)
              .backgroundColor(Color.White)
              .borderRadius(10)
            }
          }, (item: UserItem21) => item.id.toString())
        }
        .layoutWeight(1)
      }
    }
    .padding(16)
    .width('100%')
    .height('100%')
    .backgroundColor('#F6F7F9')
  }
}

一张手绘笔记风框架图,概括 HarmonyOS 7 页面在网络错误场景下的状态设计思路。中心是“页面

把关键代码一段段拆开

errorMessage 是页面状态,不是 catch 里的临时变量。只要错误影响 UI,就应该进入 @State,这样重试、保留旧数据、顶部提示都能统一驱动。

this.errorMessage.length > 0 && this.users.length === 0 用来判断首次失败。没有旧数据时要给完整错误面板;有旧数据时,不要把列表清空,只需要在顶部提示刷新失败。

finally 里调用 request.destroy()。请求成功、失败、状态码异常都会走这里,资源释放不会漏。

业务里还要补两层保护

第一层是错误来源。超时、服务端异常、登录过期、无网络,给用户看的文案不应该完全一样。

错误来源用户文案页面动作
超时当前网络较慢,请稍后重试保留重试按钮
服务端异常服务暂时不可用展示状态码或问题编号
登录过期登录状态已失效引导重新登录
无网络请检查网络连接保留旧数据

第二层是重试节奏。请求期间按钮要禁用,弱网下连续点重试很容易发出多次请求,后返回的旧结果还可能覆盖新结果。

新手最容易踩的坑

第一个坑,是一失败就把列表清空。这样用户上一秒还能看到内容,下一秒页面直接空掉,体验会很差。

第二个坑,是把所有失败都写成同一句文案。超时、无网络、登录过期、服务端异常,虽然都叫“失败”,但用户下一步该做的事并不一样。

第三个坑,是只在 catch 里弹提示,却没有把失败状态放进页面。等 Toast 消失之后,用户就不知道自己该干嘛了。

放进真实项目还要补什么

真实项目里,错误提示通常还要和埋点、日志、登录态、重试策略一起考虑。比如登录过期要不要直接跳登录页,弱网重试要不要做节流,这些都不是单靠一个提示文案能解决的。

如果页面已经有旧数据,很多时候更稳的做法是“保留旧内容 + 顶部提示刷新失败 + 给用户重试入口”。这比直接把内容全抹掉,更符合真实使用场景。

写在最后

Toast 适合提醒,不适合兜底。只要网络失败会影响页面主内容,你就应该把错误状态留在页面上,让用户看得见、点得动、知道下一步该做什么。

Logo

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

更多推荐