ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

鸿蒙ArkTS分类数据获取与页面渲染:从网络请求到状态管理实战

鸿蒙ArkTS分类数据获取与页面渲染:从网络请求到状态管理实战

1. DAY9这堂课,核心只解决两件事

开源鸿蒙跨平台训练营走到第九天,前面的课程基本把ArkTS语法、ArkUI组件、状态管理这些地基打完了。到了DAY9,目标非常明确:让数据真正流动起来。这节课的标题"分类数据获取与页面渲染实践",说白了就是解决两件事——怎么把后台的数据按分类拿回来,以及怎么把拿回来的数据渲染到页面上。

我自己的感受是,DAY9是训练营里第一个真正"前后端打通"的实操日。前几节课你写的组件、列表、交互都是本地写死数据,看起来没问题,但一接到真实接口,各种问题就冒出来了。数据格式对不上、请求超时、列表不刷新、加载状态没处理,这些细碎的问题几乎每个人都会遇到一遍。训练营把这一天单独拎出来,不是在教你怎么调一个接口,而是在帮你建立一条从"网络请求"到"状态管理"再到"UI渲染"的完整认知链路。

1.1 怎么理解"分类数据获取"

先拆一下"分类数据获取"这个词。它里面其实包含了三层意思:分类、数据、获取。

分类,指的是数据是有类别的。比如一个资讯类应用,内容分成了科技、生活、体育、娱乐;一个电商应用,商品分成了手机、家电、服饰。这个分类本身通常是后台维护的,前端要获取分类列表,然后按分类去请求对应的数据。模拟接口的做法一般是后端给你一个分类数组,再给你一个内容数组,内容数组每条带一个分类字段。

数据,指的是接口返回的东西。训练营DAY9用的是模拟JSON接口,返回的结构一般是分类数组加某个分类下的内容数组。这里有个关键点:分类数据和内容数据可能是同一个接口返回的,也可能是分开的两个接口。实操里我建议用一个接口返回,因为数据量小,减少一次请求逻辑,方便把精力放在渲染上。

获取,指的是网络请求。在鸿蒙的ArkTS环境下,我们用@ohos.net.http这个系统自带的网络模块,不需要引入第三方库就能发HTTP请求。这个模块提供了请求创建、请求发送、响应处理等能力,虽然API风格偏底层的回调式,但包一层Promise用起来就很顺手了。

把这三层拆开之后,DAY9的实操逻辑就很清晰:先请求分类列表,再按照分类请求内容列表,数据回来后更新状态、渲染界面。整个过程可以串成一句话:数据从哪来、怎么拿、拿到以后怎么显示。

1.2 页面渲染在鸿蒙里为什么值得单独讲

很多从Web前端转过来的同学,会觉得页面渲染没什么好讲的,不就是循环渲染列表吗?但在鸿蒙的ArkUI里,渲染这件事有几个不一样的地方。

第一,ArkUI是声明式的,但它不完全等价于React或者Vue。它的ForEach有自己的key生成和复用机制,用不好会产生渲染错乱或者性能问题。比如你给ForEach不传第三参数的key生成函数,列表改动时可能会出现组件复用错位,表现就是内容明明变了,页面却还是旧数据。

第二,ArkUI的@State状态变化触发UI更新是有条件的。尤其涉及对象嵌套的场景,你不小心改了成员变量的值但没有触发状态更新,页面就是不刷新,而且不报错,排查起来特别头疼。Web前端用Vue的reactive或者React的不可变数据习惯了,到了ArkTS这里思路要稍微调整一下。

第三,鸿蒙的页面结构有自己的组件树和生命周期。列表渲染放在build()的什么位置、数据初始化的时机放在onPageShow还是aboutToAppear,都会影响实际效果。网络请求是异步的,如果页面已经销毁了才返回数据,就会面临要不要更新状态的判断题。

所以说,"渲染"在鸿蒙里不是把数据套进模板那么简单,它涉及到状态管理、组件复用、生命周期这些整个体系的东西。DAY9把它和"数据获取"放在一起讲,本身就是想让大家从数据到界面形成一条完整的链路认知。

1.3 这篇笔记适合谁看

如果你是正在跟着训练营进度学习的入门开发者,这篇笔记可以帮你把DAY9的内容再串一遍,尤其是代码里容易踩的坑和训练营老师口头提过但没有展开讲的细节。我会把从请求封装、数据建模、页面渲染到问题排查的完整过程写下来,代码片段都是ArkTS环境下可以直接跑的,不是伪代码。

如果你已经有一定鸿蒙开发基础、想看看别人怎么落地分类列表页这种常见需求,也可以把这篇当做一个mini项目复盘来看。重点可以放在第五章的问题排查和第六章的实操经验上,那部分是我自己踩过坑之后沉淀下来的东西。

2. 开跑之前:ArkTS工程的底层逻辑先理清楚

2.1 ArkTS:它不是纯TypeScript

先纠正一个常见的误解:ArkTS虽然长得像TypeScript,但它在类型系统上做了严格化处理,同时增加了ArkUI的声明式UI扩展。

ArkTS对TS做的主要限制包括:不允许使用any类型、不允许在运行时动态修改对象结构、强制使用interface或class来定义对象类型。这些限制在练习阶段可能感受不深,但一旦写复杂项目就会理解——正是这些限制,让DevEco Studio能在编译期做更充分的静态检查,减少运行时错误。

另一个重要的点是,ArkTS配合ArkUI提供了一套状态管理体系。你写的组件类上挂@State、@Prop、@Link这些装饰器,声明"这个数据变了要通知UI",框架在运行时帮你做响应式更新。这跟Vue的数据劫持、React的setState思路不同,更接近声明式的"数据驱动视图"模型。

DAY9实操时,你至少要把@State和@Entry搞清楚:@Entry标记的是页面入口组件,@State标记的是组件内部需要驱动UI变化的状态变量。这两个装饰器是整个页面能跑起来的最小组合。

2.2 DAY9项目的基本工程结构

训练营到DAY9,用的工程模板通常是Empty Ability模板。创建完工程后,你会看到这样几个关键目录:

entry/src/main/ets/ ├── entryability/ │ └── EntryAbility.ets // 应用入口Ability ├── pages/ │ ├── Index.ets // 首页 │ └── CategoryPage.ets // 分类详情页(DAY9新增) ├── common/ │ └── Constants.ets // 常量,比如接口地址 └── model/ └── Item.ets // 数据模型定义(建议新增)

DAY9的实操通常是在pages/Index.ets里完成分类列表页,或者新建一个CategoryPage.ets来做分类Tab切换加内容列表。我建议直接在Index.ets里做首页分类Tab加列表,因为不需要跳转,逻辑更集中在单页面上,调试起来更简单。

还有一个文件要留意:entry/src/main/resources/base/profile/main_pages.json,这是路由配置文件。新建页面以后,需要在这个文件里注册路由,才能用router.pushUrl()跳转过去。

2.3 状态管理先过一遍:@State到底怎么触发刷新

DAY9页面里用到最多的装饰器是@State。在开始写代码之前,先把它的刷新机制理解透,不然调试的时候会一头雾水。

@State的作用是:让被装饰的变量和UI之间建立响应式绑定。变量值变化时,依赖这个变量的组件会被重新渲染。但这里有几个隐性规则:

第一,@State对数组的push、splice这类原地操作不敏感。如果你的@State变量是一个数组,你在代码里写this.itemList.push(newItem),这个操作不会触发列表刷新。你需要整体重新赋值,也就是this.itemList = [...this.itemList, newItem]这种方式。

注意:这是训练营里很多人第一天就踩的坑。@State数组的原地修改不会触发UI更新,必须整体重新赋值。这个规则跟React的不可变数据思想很像,但ArkTS文档里写得不直接,容易忽略。

第二,@State修饰的对象类型,首层属性变化可以触发更新,但嵌套对象的深层属性变化不会。DAY9的数据获取场景比较简单,itemList是数组、currentCategory是字符串,用"整体重新赋值"的方式就够用了。如果你后续要做更复杂的嵌套对象状态管理,需要去了解@Observed配合@ObjectLink的深度观察方案。

理清楚这两条,后面写列表刷新逻辑时基本不会出大的方向性问题。

3. 分类数据获取:从网络请求到数据模型

3.1 用@ohos.net.http发请求的正确姿势

在ArkTS里做网络请求,最直接的方式是用HarmonyOS自带的@ohos.net.http模块。不需要装第三方库,开箱即用。

训练营DAY9的实操中常用的封装方式是这样的:

import http from '@ohos.net.http'; export interface ApiResponse<T> { code: number; message: string; data: T; } export function get<T>(url: string, params?: Record<string, string>): Promise<T> { return new Promise((resolve, reject) => { const httpRequest = http.createHttp(); let fullUrl = url; if (params) { const queryString = Object.keys(params) .map((key) => `${key}=${encodeURIComponent(params[key])}`) .join('&'); fullUrl = `${url}?${queryString}`; } httpRequest.request( fullUrl, { method: http.RequestMethod.GET, header: { 'Content-Type': 'application/json' }, connectTimeout: 10000, readTimeout: 10000 }, (err, data) => { if (err) { reject(err); return; } try { const resp = JSON.parse(data.result as string) as ApiResponse<T>; if (resp.code === 0) { resolve(resp.data); } else { reject(new Error(`业务错误: ${resp.message}`)); } } catch (e) { reject(e); } finally { httpRequest.destroy(); } } ); }); }

这里有几点需要说明。

第一,http.createHttp()创建的是一个请求客户端,请求完成后要记得destroy()。如果不销毁,每个页面拦截器都会持有上次请求创建的连接对象,高频请求场景下会出现连接资源耗尽的问题。训练营里数据量小感受不深,但在真实项目里这点是会出事的。

第二,request的回调风格默认是回调式的。如果你想用async/await,可以像我这样包一层Promise。这里有一个小细节:data.result是string | Object | ArrayBuffer类型,需要as string转一下再交给JSON.parse。不转类型,编译阶段会报类型不匹配的错误。

第三,接口统一返回code/message/data这种包一层结构,方便在请求层统一判断业务状态码。训练营的模拟接口code为0表示成功,不等于0时抛出业务错误,页面层只需要关心data字段,不需要每个页面都去判断业务状态码。

3.2 数据模型:用interface和class把结构钉死

很多同学在训练营阶段会觉得定义interface是在浪费时间,数据直接拿JSON.parse的结果塞进变量里不就行了?但实际写下来你会发现,不定义类型,后面所有的字段访问都是风险行为。ArkTS的类型系统是严格模式,你访问一个未声明字段,编译期就直接报错,反而是个帮助。

DAY9模拟接口返回的数据大概是这样的:

{ "code": 0, "message": "ok", "data": { "categories": [ { "code": "all", "name": "全部" }, { "code": "tech", "name": "科技" }, { "code": "life", "name": "生活" } ], "items": [ { "id": "1001", "title": "鸿蒙入门指南", "category": "tech", "summary": "从零开始搭建第一个ArkTS应用", "thumbnail": "https://example.com/thumb/1001.png", "views": 1024, "updatedAt": "2025-04-10" } ] } }

对应到ArkTS里,我会这样定义模型:

export interface Category { code: string; name: string; } @Observed export class Item { id: string = ''; title: string = ''; category: string = ''; summary: string = ''; thumbnail: string = ''; views: number = 0; updatedAt: string = ''; constructor(init?: Partial<Item>) { if (init) { Object.assign(this, init); } } }

用class而不用interface来定义Item,不只是为了@Observed深度观察,更实际的用处是:class可以写构造函数,可以给默认值,解析的时候即使接口少返回一个字段,也不会出现读取undefined的崩溃问题。

构造函数里用Partial<Item>做类型,让调用方可以只传部分字段,配合Object.assign完成批量赋值。这是ArkTS里写数据模型的常用姿势。

从JSON.parse出来的纯对象到class实例的转换,可以这样写:

const rawData = JSON.parse(response.result as string); const jsonItems = rawData.data.items as Record<string, Object>[]; const itemList = jsonItems.map((raw) => new Item(raw as Partial<Item>));

3.3 分类逻辑:接口分组还是前端过滤

DAY9有个设计选择题值得展开一下:分类数据是后端返回好,还是前端从全量数据里过滤?

训练营模拟接口的做法通常是返回全量数据,前端自己按category字段过滤。好处是逻辑直观,切换分类不需要重新发请求,响应是即时的。坏处是如果数据量大,一次性返回全量数据对流量和内存都不友好。

真实项目里更常见的做法是:请求时带上category参数,由服务端做查询并返回该分类下的数据。这样好处是数据量可控、接口可以独立做分页、字段也可以按需裁剪,坏处是切换分类时会有网络延迟,需要自己处理loading和竞态。

DAY9采用前端过滤的方案,主要是让同学们先掌握"数据拿到手之后如何处理"的思路,暂不涉及分页和参数传递。但你可以自己扩展一下——把get函数里的params用起来,在切换分类时带上category参数请求新数据:

async loadItemsByCategory(categoryCode: string) { this.loading = true; try { const data = await get<Item[]>('/api/items', { category: categoryCode }); this.itemList = data; } catch (e) { this.hasError = true; } finally { this.loading = false; } }

结合DAY9的训练目标,前端过滤是更合适的方案,它让你把全部注意力放在状态管理和渲染上。但你在心里要有这根弦:将来数据量一大,就要切到后端过滤模式。判断标准很简单——如果一条数据包含大图、详情文本这类重字段,或者总量超过几百条,就别再全量拉回来前端过滤了。

4. 页面渲染:数据怎么变成看得见的界面

4.1 分类Tab栏的实现

DAY9的页面布局,按照训练营的标准要求,通常长这样:顶部是可以横向滚动的分类Tab栏,中间是内容列表,列表随当前选中的分类切换而更新。我先把Tab栏的完整代码贴出来,再逐段解释。

@Entry @Component struct CategoryPage { @State currentCategory: string = 'all'; @State categoryList: Category[] = []; @State itemList: Item[] = []; @State loading: boolean = true; @State hasError: boolean = false; private get filteredItems(): Item[] { if (this.currentCategory === 'all') { return this.itemList; } return this.itemList.filter((item) => item.category === this.currentCategory); } async loadData() { this.loading = true; this.hasError = false; try { const [categoryResponse, itemResponse] = await Promise.all([ get<Category[]>('/api/categories'), get<Item[]>('/api/items') ]); this.categoryList = categoryResponse; this.itemList = itemResponse; } catch (e) { this.hasError = true; } finally { this.loading = false; } } aboutToAppear() { this.loadData(); } build() { Column() { this.buildCategoryTabs() if (this.loading) { this.buildLoading() } else if (this.hasError) { this.buildError() } else { this.buildItemList() } } .width('100%') .height('100%') .backgroundColor('#F5F6F8') } @Builder buildCategoryTabs() { Scroll() { Row({ space: 12 }) { ForEach(this.categoryList, (category: Category) => { Column() { Text(category.name) .fontSize(this.currentCategory === category.code ? 16 : 14) .fontColor(this.currentCategory === category.code ? '#007DFF' : '#666666') .fontWeight(this.currentCategory === category.code ? FontWeight.Bold : FontWeight.Normal) } .padding({ left: 16, right: 16, top: 8, bottom: 8 }) .backgroundColor(this.currentCategory === category.code ? 'rgba(0,125,255,0.1)' : '#F1F3F5') .borderRadius(20) .onClick(() => { this.currentCategory = category.code; }) }, (category: Category) => category.code) } .padding(12) } .scrollable(ScrollDirection.Horizontal) .scrollBar(BarState.Off) .align(Alignment.Start) }

这段代码里有几个值得注意的训练营级细节。

ForEach的第三个参数是键值生成器,这里用category.code作为唯一标识。如果不用键值或者键值重复,列表渲染时组件复用的位置可能错乱,轻则闪烁,重则导致点击事件和数据对不上。我见过有人用数组索引做key,然后分类数组动态变化时列表错位,排查了半天才发现是key的问题。

Tab选中状态的切换纯粹靠比较this.currentCategory === category.code来判断。这里刻意不维护一个"选中项对象",而是用字符串状态值。这样做的原因是让状态源只有一个,避免出现"这边改了选中项、那边还引用旧对象"的数据不同步问题。ArkUI里状态源越多,出问题的可能性越大。

@Builder装饰器可以把重复的UI模板抽成构建函数。ArkUI里build()方法中不能写太多重复代码,用@Builder抽离是官方推荐的做法。上面的代码里我把Tab栏、加载态、错误态、列表分别抽成了四个构建函数,这让build()方法保持了清晰的结构。

4.2 内容列表:List + ForEach的正规写法

内容列表是DAY9的重头戏。ArkUI里列表渲染用List组件包ListItem,在ListItem内部写具体的内容行布局。

@Builder buildItemList() { List({ space: 12 }) { ForEach(this.filteredItems, (item: Item) => { ListItem() { Row() { Image(item.thumbnail) .width(72) .height(72) .borderRadius(8) .objectFit(ImageFit.Cover) .backgroundColor('#F1F3F5') Column() { Text(item.title) .fontSize(16) .fontColor('#1A1A1A') .fontWeight(FontWeight.Medium) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(item.summary) .fontSize(13) .fontColor('#999999') .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) .margin({ top: 4 }) Text(`${item.views} 次浏览 · ${item.updatedAt}`) .fontSize(11) .fontColor('#BBBBBB') .margin({ top: 6 }) } .alignItems(HorizontalAlign.Start) .layoutWeight(1) .padding({ left: 12 }) } .padding(12) .width('100%') .backgroundColor(Color.White) .borderRadius(12) } }, (item: Item) => item.id) } .width('100%') .layoutWeight(1) .padding({ left: 16, right: 16 }) .backgroundColor('#F5F6F8') .edgeEffect(EdgeEffect.Spring) }

几个要点:

Row外层的宽度要设置成100%,不然内容行会被内容撑出异常布局,在真机上尤其明显。右侧的Column要给layoutWeight(1)让它占满剩余宽度,图片固定宽高,这样布局不会挤压变形。

文字超出处理是列表项最容易忽略的部分。Text默认会换行撑高,列表里每行内容高度不统一,视觉上会很乱。加上maxLines和textOverflow可以保证每行最多显示1到2行,超出部分用省略号代替。

List组件的edgeEffect参数设置成EdgeEffect.Spring,列表拉到边缘时会有弹簧回弹效果,交互质感好很多。这是ArkUI默认支持的,不需要额外引入。

点击跳转在ListItem上直接挂.onClick(() => this.openDetail(item))。如果要带参跳转,用router.pushUrl的时候把item.id序列化到params里就行。这里有个小提醒:不要把整个item对象传进去,路由传参建议只传id,目标页面再通过id请求详情。原因是路由参数会序列化到URL,大对象导致参数过长,且语义不清晰。

我在DAY9实际操作中发现,把列表项的圆角、背景色、内边距这些样式统一抽成常量类,后续多个列表页可以复用,整个项目的视觉一致性也好维护。训练营里不会讲这个,但这是项目实践中很重要的工程习惯。

4.3 加载中、错误态与空数据的处理

DAY9的页面里,数据没回来的时候不能白屏,请求失败的时候要有反馈,分类下没有内容时也不能显示空白列表。这三个状态都要处理。

加载状态比较简单:

@Builder buildLoading() { Column() { LoadingProgress() .width(60) .height(60) .color('#007DFF') Text('正在加载...') .fontSize(14) .fontColor('#999999') .margin({ top: 12 }) } .width('100%') .layoutWeight(1) .justifyContent(FlexAlign.Center) }

错误态的页面要给出重试入口,不然用户遇到断网只能干瞪眼:

@Builder buildError() { Column({ space: 12 }) { Text('加载失败,请检查网络') .fontSize(14) .fontColor('#999999') Button('重试', { type: ButtonType.Capsule }) .backgroundColor('#007DFF') .fontColor(Color.White) .onClick(() => { this.loadData(); }) } .width('100%') .layoutWeight(1) .justifyContent(FlexAlign.Center) }

空数据态也不能忽略。某些分类下没有内容时,最好明确告诉用户"这个分类还没有内容",比一个空白列表强得多:

@Builder buildEmpty() { Column() { Text('暂无内容') .fontSize(14) .fontColor('#999999') } .width('100%') .layoutWeight(1) .justifyContent(FlexAlign.Center) }

这三个@Builder函数在build()里按条件切换。需要注意每个分支都要考虑占位空间,layoutWeight(1)可以让不同状态的视图占满同一块区域,避免跳变。比如加载态转到错误态时,如果没设置layoutWeight(1),内容可能会被压缩到顶部一小块区域,布局突然跳一下,观感很差。

我习惯把状态切换的逻辑集中写在一个地方:

if (this.loading) { this.buildLoading() } else if (this.hasError) { this.buildError() } else if (this.filteredItems.length === 0) { this.buildEmpty() } else { this.buildItemList() }

这段条件渲染是ArkUI的官方推荐的写法。注意分支顺序:加载中优先、错误次之、空数据再次、正常列表兜底。如果你把空数据判断放在错误判断之前,接口请求失败导致的数据为空会被误判成"暂无内容",用户看不到错误提示,体验就会很误导。

5. 实操中逃不掉的问题与排查思路

5.1 训练营里最常遇到的四个报错

DAY9实操过程中,最常遇到的报错集中在编译期类型错误、请求失败、渲染错乱这几类。我把它们按出现频率排序,逐个说清楚现象、原因和解决办法。

报错一:Cannot find module '@ohos.net.http'

这个多见于手工建工程或者拷贝代码时未同步依赖。先确认oh-package.json5里的配置,再看build-profile.json5的签名配置。多数情况下,在DevEco Studio里执行一次Sync就能解决。@ohos.net.http是系统内置模块,不需要在oh-package.json5里显式声明依赖,如果报找不到,不是依赖问题,是工程同步状态不对——清理缓存、重新Sync即可。

报错二:Property 'xxx' does not exist on type ...

这类编译期报错最常见的原因是:接口返回的JSON字段和interface里定义的名字对不上。比如接口返回的是create_time,你定义的是createTime,ArkTS严格类型检查不允许你访问未定义字段。解决办法是先把原始JSON打印出来,逐个确认字段名,再修正数据模型。这个错在训练营里几乎人人都遇到,属于基础但典型的类型安全练习。

报错三:ForEach key Duplicated

ArkUI控制台会报ForEach键值重复。常见原因是你用列表索引做key,列表在过滤或排序后索引发生错位;或者数据里有重复的id字段值。解决办法是给数据模型一个全局唯一的id字段,并用它生成key。如果接口不返回稳定的id,可以自己用UUID生成一个,或者把index + 内容特征组合成key。注意,用索引做key在纯静态列表里看着没问题,但只要列表内容会变,就会出现复用错位。

报错四:列表点击后页面不跳转

这个不是编译错误,是"没反应"。多半是忘了在main_pages.json里注册新页面路由。你新建的CategoryPage.ets,必须到main_pages.json中加入"pages/CategoryPage"这个条目,否则router.pushUrl找不到目标页面。

5.2 列表卡顿与数据更新异常

有些同学DAY9做完以后,发现滑动列表有明显掉帧,或者在快速切换分类Tab时,列表内容跟分类对不上。

先分析切换Tab时内容错乱的原因。快速点击Tab会触发多个网络请求并发返回,由于网络请求耗时不同,后发出的请求可能先返回,先发出的请求反而后返回,结果就是把旧分类的数据覆盖到了新分类的界面上。这就是典型的异步竞态问题。

我推荐的土办法是维护一个请求序号:

private requestSeq: number = 0; async loadItemsByCategory(categoryCode: string) { const seq = ++this.requestSeq; const data = await get<Item[]>('/api/items', { category: categoryCode }); if (seq !== this.requestSeq) { return; // 旧请求的结果不处理 } this.itemList = data; }

每次切换Tab都递增requestSeq,等请求返回时比对序号,只有最新一次请求的结果才允许写入状态。这个方法简单可靠,在不引入复杂状态管理库的前提下能解决90%的竞态问题。前端过滤方案因为不需要发请求,不存在这个问题,但如果你扩展成后端过滤,一定要加这个保护。

至于列表卡顿,常见原因有两个。

一个是Image组件加载网络图片时没有合理的占位和缓存策略。ArkUI的Image加载网络图片默认会缓存,但如果你列表里塞了一堆大图,每张图几MB,加载会明显卡顿。建议服务端返回时给出缩略图地址,列表里只加载缩略图,点击进去再加载原图。这是所有大图列表应用都在用的策略。

另一个是ForEach渲染项里做了太多计算。比如在渲染函数里写复杂的字符串拼接或者多层filter,每帧都可能触发重新计算。像filteredItems这种计算属性写在getter里没问题,但如果你的过滤逻辑很复杂,建议先把过滤结果提前算好存入@State,而不是在build()里实时计算。

5.3 真机调试时的权限与配置

训练营DAY9大家基本都用模拟器调试,但如果你要用真机跑,有几个前置配置必须提前做好。

第一,网络权限。HarmonyOS应用访问网络需要在module.json5里声明权限:

{ "module": { "requestPermissions": [ { "name": "ohos.permission.INTERNET" } ] } }

模拟器调试时这个权限通常默认放行,但真机上不声明这个权限,http请求会直接失败,表现是请求发不出去、一直报超时。这个坑我在真机调试时踩过,当时花了很长时间排查接口地址和网络环境,最后发现只是少加了一行权限声明。

第二,明文HTTP限制。如果训练营为你准备的模拟接口是http://开头的明文地址,在HarmonyOS里明文流量默认会被限制。解决办法是在module.json5的同级目录下配置网络安全策略,允许特定域名或全局允许明文流量。具体做法是在resources/base/profile/下新建network_config.json,然后配置cleartext相关策略,并在module.json5中指定networkConfig属性。

需要注意的是,官方文档默认建议生产环境关闭明文流量,仅开发调试阶段开启。所以当你把配置切回https接口时,记得同步关闭明文开关。

第三,拦截器与调试。真机调试时,可以用DevEco Studio自带的网络调试工具查看请求和响应,或者直接在代码里打日志打印接口原始返回。DAY9阶段我强烈建议多打日志,把response.result和解析后的类型都打出来。看原始数据、确认字段、再写类型——这个顺序永远不要反。

6. 从DAY9实操里沉淀的几个经验习惯

DAY9整个实操下来,我的感觉是:分类列表页看似简单,但它其实是很多业务页面(首页、频道页、消息列表、商品列表)的原型。把"分类数据获取 + 页面渲染"这条链路彻底搞明白,后面做更复杂的页面基本心里有底。

给你几个我实践下来之后,会直接带到后续项目里的习惯。

第一,所有的网络请求都走统一的封装层,不要在每个页面里裸写http.createHttp()。统一封装意味着统一错误处理、统一超时、统一状态码判断,后面要接日志、接统计、接token刷新,只需要改一个地方。如果你在每个页面各自为战,后面维护成本会直线上升。

第二,数据模型单独放一个目录,不要散落在页面文件里。DAY9可能只有一两个页面,但页面一多,模型不抽出来就会出现一改接口字段、所有页面编译报错的情况。把Item、Category这些类放到model/目录下,页面通过import来引用,结构清晰,类型也容易复用。

第三,列表页一定要提前做加载态、错误态和空数据态。哪怕你的接口暂时返回的都是满数据,这三段代码也要先写好。因为线上用户遇到断网、后台临时清库、接口返回空数组,都是大概率事件。如果这三个状态不在,用户面对的就是白屏或"看起来很正常的假死页面"。

第四,多利用DevEco Studio的预览器和ArkUI Inspector定位布局问题。ArkUI Inspector可以查看组件树中每个节点的层级、属性、尺寸,排查"列表内容被挤压""图片对象不对齐""文字被截断"这类问题,比对着代码猜要快得多。DAY9阶段你可能会觉得预览器有点慢,但等页面复杂起来,预览器的价值会非常明显。

最后再分享一个我在训练营里悟到的、一直用到现在的经验:调试网络请求时,先在控制台里把接口返回的原始JSON完整打印出来,再写数据模型。先看"数据长什么样",再定义"类型应该长什么样",这个顺序不能反。反过来的话,你会对着猜出来的字段名写一堆解析代码,然后逐个编译报错再去改,白白浪费大量时间。

DAY9的实践内容到列表正常渲染出来时就算完成了,但真正值得你反复打磨的,是那些"数据没回来的瞬间页面长什么样""接口返回异常字段时程序会不会崩""快速切换分类日志对不对得上号"的边界情况。把这些处理妥当,你的ArkTS页面才算是真的可供用户使用,而不是只会在理想数据下跑通的Demo。后续课程里你还会遇到数据库操作、跨设备流转、分布式能力这些更复杂的主题,但每一次实操最后都会回到"数据怎么来、怎么渲染、出错怎么办"这三个老问题上。DAY9解决的是这条链路的地基,地基扎实了,上面盖什么楼都稳。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进