
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 ApiResponseT { code: number; message: string; data: T; } export function getT(url: string, params?: Recordstring, string): PromiseT { 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 ApiResponseT; 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?: PartialItem) { if (init) { Object.assign(this, init); } } }用class而不用interface来定义Item不只是为了Observed深度观察更实际的用处是class可以写构造函数可以给默认值解析的时候即使接口少返回一个字段也不会出现读取undefined的崩溃问题。构造函数里用PartialItem做类型让调用方可以只传部分字段配合Object.assign完成批量赋值。这是ArkTS里写数据模型的常用姿势。从JSON.parse出来的纯对象到class实例的转换可以这样写const rawData JSON.parse(response.result as string); const jsonItems rawData.data.items as Recordstring, Object[]; const itemList jsonItems.map((raw) new Item(raw as PartialItem));3.3 分类逻辑接口分组还是前端过滤DAY9有个设计选择题值得展开一下分类数据是后端返回好还是前端从全量数据里过滤训练营模拟接口的做法通常是返回全量数据前端自己按category字段过滤。好处是逻辑直观切换分类不需要重新发请求响应是即时的。坏处是如果数据量大一次性返回全量数据对流量和内存都不友好。真实项目里更常见的做法是请求时带上category参数由服务端做查询并返回该分类下的数据。这样好处是数据量可控、接口可以独立做分页、字段也可以按需裁剪坏处是切换分类时会有网络延迟需要自己处理loading和竞态。DAY9采用前端过滤的方案主要是让同学们先掌握数据拿到手之后如何处理的思路暂不涉及分页和参数传递。但你可以自己扩展一下——把get函数里的params用起来在切换分类时带上category参数请求新数据async loadItemsByCategory(categoryCode: string) { this.loading true; try { const data await getItem[](/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([ getCategory[](/api/categories), getItem[](/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你定义的是createTimeArkTS严格类型检查不允许你访问未定义字段。解决办法是先把原始JSON打印出来逐个确认字段名再修正数据模型。这个错在训练营里几乎人人都遇到属于基础但典型的类型安全练习。报错三ForEach key DuplicatedArkUI控制台会报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 getItem[](/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解决的是这条链路的地基地基扎实了上面盖什么楼都稳。