news 2026/10/16 7:02:54

ArkTS分类数据获取与页面渲染:从数据建模到状态联动实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ArkTS分类数据获取与页面渲染:从数据建模到状态联动实践

DAY9训练营的题目发到手里的时候,我刚接触ArkTS不到两周。分类数据获取与页面渲染实践,听起来不算难——无非是拿到数据、丢到页面上。可真在OpenHarmony的ArkUI框架里跑起来,“分类数据”这四个字背后的东西比我预想的多得多:数据从哪来、类型怎么定、用哪种容器渲染、分类切换时页面怎么联动刷新,每一步都有跟Web开发完全不同的脾气。这篇文章我把DAY9整场实践做个复盘,内容包括数据模型设计、三种典型渲染方案的选型逻辑、状态管理驱动分类联动的完整思路,以及我在实盘过程中踩过的几个坑。如果你也在学ArkTS,或者正准备把一个带分类结构的业务页面迁移到鸿蒙环境,这篇应该能让你少走不少弯路。

1. 为什么“分类数据”值得单独花一天来啃

先说清楚我理解的“分类数据”。它不单指一个分类列表,而是业务里最常见的三层结构:分类维度、条目明细、两者的联动关系。电商的商品分类、资讯的话题标签、电子书的书架分组、即时通讯的联系人分组,本质都是同一套东西。训练营把DAY9专门留给它,是因为掌握了这个链路,基本就等于掌握了一个常规业务页面的完整骨架。

拆开来看,分类数据页面至少包含这几个能力:按维度展示分类列表;每个分类对应一批明细数据;用户切换分类时,明细区域要快速响应;加载过程要有可感知的状态反馈。这几个能力放到ArkTS环境下,分别对应数据请求、类型声明、列表渲染、状态管理和条件渲染,几乎把ArkUI的核心概念都串了一遍。而且这套框架的语法和编程范式跟常见的前端栈有差异,你不能用React或Vue的直觉直接套,所以必须单独练。

另一个现实原因是,ArkTS对类型的要求比普通TS更严格。分类数据往往来自一个网络接口,返回结构里可能带着null、可选字段、嵌套对象,如果你直接把接口给的数据丢进页面里,编译器那一关就过不去。我在DAY9里最深的感受是:这个框架逼着你先把数据结构想清楚,再动手写UI。这跟传统页面开发里“先画界面、后补数据”的习惯正好相反。第一天不适应,但跑通之后你会发现,数据先行的模式反而让后续渲染逻辑变得很干净。

2. 数据从哪来:本地模拟数据与网络请求的取舍

2.1 先用静态数据把页面结构搭起来

DAY9的前半程,我建议不要一上来就接网络,先把静态数据放在本地,把整个页面渲染的骨架跑通。这不是绕路,而是把“数据获取”和“页面渲染”两个问题拆开解决。你在调试渲染逻辑时,如果每次刷新都要等网络返回,会非常浪费时间。本地数据固定可控,也方便你验证不同的渲染方案。

我当时建了一个模拟数据文件,结构大概长这样:

// models/MockData.ets import { Book, BookCategory } from './Models'; export const mockCategories: BookCategory[] = [ { id: 'tech', name: '技术', icon: '📘' }, { id: 'sci-fi', name: '科幻', icon: '🚀' }, { id: 'history', name: '历史', icon: '🏛️' }, { id: 'novel', name: '文学', icon: '📚' } ]; export const mockBooks: Book[] = [ { id: 'b001', title: 'ArkTS实战笔记', author: '某开发者', categoryId: 'tech', rating: 4.8 }, { id: 'b002', title: '分布式系统设计', author: '某架构师', categoryId: 'tech', rating: 4.5 }, // ...更多数据 ];

用静态数据的阶段,你的注意力可以完全放在组件编排上:分类栏放左边还是顶部、明细区用列表还是卡片、切换分类时怎么过滤数据。等到这个骨架稳定了,再替换成网络数据源,问题就会集中在请求层,而不是混在一起很难排查。

2.2 网络请求层:用系统http模块还是封装第三方库

到了DAY9的后半段,训练营要求把静态数据替换成网络接口返回的真实数据。OpenHarmony应用里做HTTP请求,最直接的方式就是使用系统提供的@ohos.net.http模块。它不需要额外引入依赖,API也足够完成绝大多数场景。我封装了一个带泛型的小工具,简化调用:

// services/ApiClient.ets import http from '@ohos.net.http'; export class ApiClient { static async get<T>(url: string): Promise<T> { const request = http.createHttp(); try { const response = await request.request(url, { method: http.RequestMethod.GET, connectTimeout: 10000, readTimeout: 10000, header: { 'Content-Type': 'application/json' } }); if (response.responseCode === 200) { return JSON.parse(response.result as string) as T; } throw new Error(`请求失败,状态码:${response.responseCode}`); } finally { request.destroy(); } } }

封装成一个带泛型的请求方法后,后面接分类接口和详情接口都只需要指定返回类型。这个过程中最容易漏掉的是权限配置,如果没有在module.json5里声明INTERNET权限,请求会静默失败或者直接抛错。记得加上:

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

2.3 请求层的兜底设计:别让页面卡在空白上

接上网络之后,你要立刻面对三个问题:网络慢、网络失败、数据为空。如果不做兜底,用户看到的就是一片白屏,体验很差。我在DAY9里做的兜底方案很简单:页面状态至少分为加载中、加载成功、加载失败三个形态。加载中用加载动画或骨架屏,失败时显示重试按钮,成功但数据为空时显示空态文案。

这里分享一个经验:请求的返回结构不要直接当成页面模型用。接口返回的往往是{ code: number, data: T, message: string }这种统一信封结构,而你页面真正需要的是data里那一层。在服务层里就完成解包和异常抛出,页面组件只接收已经处理好的、干净的数据。这样你的组件里不会到处都是res.data.data.list这种魔鬼嵌套,后续维护会轻松很多。

3. 把分类数据装进“架子”:ArkTS类型定义与数据模型设计

3.1 先写interface再写页面,是ArkTS给你的“强制要求”

刚开始写页面的时候,我习惯性地先摆组件,等编译报错再回去补类型。这套做法在ArkTS里会很难受,因为类型定义直接决定了一个对象能不能被赋值、能不能传给子组件、能不能被状态管理观察。所以 DAY9 的实践中,我换成了“数据模型先行”的工作流:先定义类型,再写服务层,最后写页面。

分类页面的核心模型,通常需要两个实体:分类实体和条目实体。分类实体描述一侧的分类维度,条目实体描述另一侧的明细内容。它们通过分类ID关联。

// models/Models.ets export interface BookCategory { id: string; name: string; icon?: string; description?: string; } export interface Book { id: string; title: string; author: string; categoryId: string; rating: number; summary?: string; coverUrl?: string; }

这里的关键是categoryId。它建立了“分类”和“条目”之间的关联,后续所有过滤逻辑都围绕着它展开。接口返回的Book如果还带一个categoryName的冗余字段,也不是不行,但我会尽量避免,因为冗余字段在数据变更时容易造成不一致。宁可渲染时通过分类ID去映射名称,也不要在条目数据里多存一份文本。

3.2 接口返回的数据要“收窄”成强类型

网络请求返回的数据在JSON.parse之后,只是一个Object,它在ArkTS的严格模式下不会自动跟你的interface兼容。很多初学者会遇到这个报错:把一个Object类型赋给BookCategory[],编译器直接报类型不兼容。解决办法是在解析后加一层校验和转换。

我写了一个小小的转换工具,对关键字段做检查和默认值兜底。

// services/BookService.ets import { Book, BookCategory } from '../models/Models'; import { ApiClient } from './ApiClient'; export class BookService { static async fetchCategories(): Promise<BookCategory[]> { const res = await ApiClient.get<{ code: number; data: BookCategory[] }>('/api/categories'); if (res.code !== 0) { throw new Error(`返回码异常:${res.code}`); } return res.data.map((item) => { return { id: item.id ?? '', name: item.name ?? '', icon: item.icon, description: item.description } as BookCategory; }); } static async fetchBooks(): Promise<Book[]> { const res = await ApiClient.get<{ code: number; data: Book[] }>('/api/books'); if (res.code !== 0) { throw new Error(`返回码异常:${res.code}`); } return res.data.map((item) => { return { id: item.id, title: item.title, author: item.author, categoryId: item.categoryId, rating: item.rating ?? 0, summary: item.summary, coverUrl: item.coverUrl } as Book; }); } }

这样做的好处是,之后的组件层拿到的一定是字段完整、类型正确的数据,页面里不需要再做一层if (item.name) {}这样的防御式判断。数据边界在服务层就处理掉了,页面代码会干净得多。

3.3 联合类型与可选属性:小细节里的大坑

ArkTS支持联合类型,比如type CategoryId = string | number,但如果你把一个字段声明为联合类型,之后做===比较或者switch分支时,编译器会让你先把类型收窄。DAY9里我测试过,分类ID如果是string,就不要为了省事把它在某个地方转成number,保持类型一致是最省心的做法。

可选属性(?)也是双刃剑。它表示这个字段可能不存在,一方面让模型能兼容不完整的接口数据,另一方面也意味着你在渲染时必须处理空值。我的建议是:非必要不用可选属性。如果某个字段在所有业务场景里都应该有值,就把它定义成必填,在服务层转换时用??给它一个默认值。这样组件层的渲染逻辑就可以少写很多判空分支。

4. 页面渲染的三板斧:List、Grid、Tabs怎么选

4.1 先理清分类页面的信息架构

拿到数据和模型之后,接下来要决定的是:这个分类页面长什么样。在ArkUI里,可用的容器组件非常多,常见的分类页面布局主要有三种形态。

布局形态适合场景核心组件特点
顶部Tab分类分类数量少(3~8个),用户需要频繁横向切换Tabs、TabContent单屏内完成切换,交互最直接
左侧分类栏+右侧明细分类数量多,需要上下滚动浏览明细List、List+联动滚动类似电商和资讯客户端的经典布局
宫格分类入口分类作为入口,点击后进入二级页面Grid信息密度高,常用于首页导航

训练营DAY9的模拟项目是一个电子书应用,我最早用的是顶部Tab方案,因为分类只有四个,放在顶部最直观。后来为了练习,也把左侧分类栏加右侧明细的方案做了一遍。两种方案各有优劣,关键在于分类数量和用户操作习惯,而不是哪个看起来更“高级”。

4.2 顶部Tab方案:Tabs组件一分钟搭起来

Tabs方案是分类页最省事的实现,因为它自带联动效果。你把每个TabContent直接对应一个分类的明细列表,渲染逻辑非常直白。

// pages/CategoryPage.ets import { BookCategory, Book } from '../models/Models'; @Component export struct CategoryPage { @State categories: BookCategory[] = []; @State books: Book[] = []; build() { Tabs({ barPosition: BarPosition.Start }) { ForEach(this.categories, (category: BookCategory) => { TabContent() { List({ space: 12 }) { ForEach(this.filterBooks(category.id), (book: Book) => { ListItem() { BookItem({ book: book }) } }, (book: Book) => book.id) } .padding(12) .layoutWeight(1) } .tabBar(category.name) }, (category: BookCategory) => category.id) } .scrollable(true) .barMode(BarMode.Fixed) } filterBooks(categoryId: string): Book[] { return this.books.filter((book: Book) => book.categoryId === categoryId); } }

Tabs的坑在于,它的滑动切换动画是由框架控制的,你很难去做一些自定义的过渡效果。但如果你只想快速实现一个可用的分类切换,Tabs绝对是最快路径。还有一个细节:Tabs的每个页面默认会预加载相邻的内容,如果你的明细数据很大,建议配合LazyForEach做懒加载,避免一开始就渲染全部TabContent。

4.3 左侧分类+右侧明细方案:联动滚动的手动实现

左侧分类栏加右侧明细的方案,其实比Tabs更贴近真实复杂业务。左侧用一个垂直List展示分类,右侧用一个嵌套List展示当前分类下的明细。点击左侧分类时,右侧重新计算过滤结果。

这种方案最核心的是右侧的过滤渲染:

// pages/CategoryListPage.ets @State selectedCategoryId: string = ''; build() { Row() { List() { ForEach(this.categories, (category: BookCategory) => { ListItem() { Text(category.name) .fontSize(this.selectedCategoryId === category.id ? 18 : 15) .fontColor(this.selectedCategoryId === category.id ? '#FF6B00' : '#666666') .padding(14) .width('100%') .textAlign(TextAlign.Center) .backgroundColor(this.selectedCategoryId === category.id ? '#FFF3E8' : Color.Transparent) .onClick(() => { this.selectedCategoryId = category.id; }) } }, (category: BookCategory) => category.id) } .width('30%') .height('100%') List({ space: 12 }) { ForEach(this.filterBooksBySelectedCategory(), (book: Book) => { ListItem() { BookItem({ book: book }) } }, (book: Book) => book.id) } .width('70%') .height('100%') .padding(12) } .width('100%') .height('100%') }

这种方案的交互逻辑更可定制,比如你可以让左侧分类滚动到可见区域时自动联动右侧,也可以通过点击左侧分类,让右侧滚动到顶部。DAY9里我实现的是点击切换分类、右侧列表自动回到顶部,做法是给右侧List加一个控制器Scroller,在切换分类时调用scrollToIndex(0, true)。

4.4 宫格方案:分类多时的高密度导航

第三个方案是Grid,适合分类数量超过8个、并且每个分类只作为一个入口的场景。比如首页上的16宫格功能导航,点进去才是真正的列表页。Grid的写法跟List类似,只是排列维度变成了二维网格。

Grid() { ForEach(this.categories, (category: BookCategory) => { GridItem() { Column() { Text(category.icon ?? '📄') .fontSize(32) Text(category.name) .fontSize(14) .margin({ top: 6 }) } .justifyContent(FlexAlign.Center) .width('100%') .height('100%') .onClick(() => { this.onCategoryClick(category.id); }) } }, (category: BookCategory) => category.id) } .columnsTemplate('1fr 1fr 1fr 1fr') .rowsGap(12) .columnsGap(12)

Grid方案本身没有太多的联动逻辑,因为它的目标就是把用户送向二级页面。需要注意的是,GridItem里的点击区域要尽量大,图片/文字的缩放比例要提前设计好,否则在不同屏幕尺寸下会出现格子错位或者内容溢出。

4.5 渲染性能与组件复用

不管选哪种方案,最终都要面对性能问题。分类页面的数据通常是几百条的规模,直接用ForEach全量渲染在开发阶段看不出问题,真机上一旦数据增加到几百条以上,滚动就会出现掉帧。所以训练营DAY9的尾声,我专门去把LazyForEach用了起来。

LazyForEach是ArkUI的懒加载机制,它只会渲染当前视口附近的条目,而不是一次性把几十个ListItem都塞进组件树。但它的使用门槛比ForEach高一些——你需要实现一个数据源类,提供totalCount、getData、注册和注销监听等方法。这里我给出一个简化的数据源框架:

// services/BookDataSource.ets import { Book } from '../models/Models'; export class BookDataSource implements IDataSource { private books: Book[] = []; private listeners: DataChangeListener[] = []; constructor(books: Book[]) { this.books = books; } totalCount(): number { return this.books.length; } getData(index: number): Book { return this.books[index]; } registerDataChangeListener(listener: DataChangeListener): void { this.listeners.push(listener); } unregisterDataChangeListener(listener: DataChangeListener): void { const idx = this.listeners.indexOf(listener); if (idx >= 0) { this.listeners.splice(idx, 1); } } updateBooks(books: Book[]): void { this.books = books; this.listeners.forEach((listener) => { listener.onDataReloaded(); }); } }

用LazyForEach替换ForEach时要注意:它要求每条数据有一个稳定的key,并且数据增删时要主动通知数据源变化。否则你可能会遇到视图不更新的问题,那是一个比普通ForEach更加隐蔽的坑。

5. 让页面动起来:状态管理如何驱动分类切换

5.1 声明式状态管理的核心逻辑

ArkUI页面和Web页面最大的不同在于:你不再手动操作DOM节点去修改页面内容,而是维护一个“状态”,框架根据状态自动重新渲染。分类页面的核心状态有三个:全部分类数组、当前选中的分类ID、当前分类下的明细条目。

@State categories: BookCategory[] = []; @State books: Book[] = []; @State selectedCategoryId: string = '';

@State装饰的变量一旦发生变化,所有依赖它的UI组件都会重新渲染。这一改动发生在“设计层面”,但它的实际体验很像你在Vue里用响应式数据驱动页面更新——只是ArkUI的实现细节更接近框架底层。

5.2 父子组件之间怎么传递数据:@Prop与@Link

页面组件变大之后,你要把分类列表或者条目卡片拆分到子组件。这时会用到@Prop和@Link。它们的区别在于:@Prop是单向同步,子组件里修改不会影响父组件;@Link是双向同步,子组件可以直接修改父组件的状态。

举个例子。如果我把右侧的BookItem拆成一个子组件,那它的数据用@Prop传入就够了,因为子组件不需要修改书籍本身:

@Component export struct BookItem { @Prop book: Book; build() { Row() { Column() { Text(this.book.title) .fontSize(16) .fontWeight(FontWeight.Medium) Text(this.book.author) .fontSize(13) .fontColor('#999999') .margin({ top: 4 }) } .alignItems(HorizontalAlign.Start) .layoutWeight(1) Text(`${this.book.rating}`) .fontSize(14) .fontColor('#FF6B00') } .padding(12) .backgroundColor(Color.White) .borderRadius(10) .width('100%') } }

但左侧分类列表的选中状态,子组件需要反向通知父组件。这时我用的是事件回调,而不是直接@Link。原因是:分类ID的变更会影响右侧的过滤逻辑,这个逻辑放在父组件里最清晰。子组件只负责响应用户点击,并把新的分类ID抛给父组件。

@Component export struct CategoryItem { @Prop category: BookCategory; @Prop isSelected: boolean; onSelect: (categoryId: string) => void = () => {}; build() { Text(this.category.name) .fontSize(this.isSelected ? 18 : 15) .fontColor(this.isSelected ? '#FF6B00' : '#666666') .padding(14) .width('100%') .textAlign(TextAlign.Center) .backgroundColor(this.isSelected ? '#FFF3E8' : Color.Transparent) .onClick(() => { this.onSelect(this.category.id); }); } }

这个模式的优点是把子组件变成了一个可复用的展示单元,它不关心父组件拿到categoryId之后做什么,只负责把事件传出去。实际开发里,这种“展示组件+回调”的组合,比到处传@Link更好维护。

5.3 过滤逻辑的放置:方法还是计算属性?

分类切换的核心,是“根据选中的分类ID过滤出对应的明细数组”。我用的是一个普通方法:

filterBooksBySelectedCategory(): Book[] { return this.books.filter((book: Book) => book.categoryId === this.selectedCategoryId); }

这个方法每次渲染时都会被调用。在数据量不大的情况下没问题,但如果明细数据上千条,每次渲染都全量过滤一次,性能就有点紧张。比较理想的优化是:在分类切换时把过滤结果缓存到一个@State变量里,只在selectedCategoryId变化时重新计算。不过这个方案也有代价——缓存变量需要和源数据保持同步,涉及更多的状态更新逻辑。DAY9阶段数据量不大,用方法过滤完全够用,如果后续数据量上来了,再考虑缓存也不迟。

5.4 数组更新不刷新的隐形雷区

在ArkUI里用数组时,你会遇到一个很经典的思维转换:直接改数组某个元素的值,页面并不会刷新。比如:

this.books[0] = updatedBook; // 这通常不会触发UI更新

原因是ArkUI的状态观察是基于对象替换和属性层面的,并不是数组索引层面的代理。你需要做的是重新赋值一个新数组:

const newBooks = [...this.books]; newBooks[0] = updatedBook; this.books = newBooks;

或者用this.books.splice(0, 1, updatedBook)这种能触发监听的数组变更方法。这个坑非常隐蔽,尤其是从Web开发转过来的朋友,很容易在这里卡住。DAY9的代码里我特意把这类操作都收敛成工具方法,避免在多个组件里重复写坑。

6. DAY9踩坑实录:三个让人抓狂的细节

6.1 分类切换后页面“没反应”:问题出在数组引用

我做左侧分类列表时,第一个版本是这样的逻辑:点击左侧分类,把该分类下的书籍push到一个右侧的@State booksToShow: Book[]里。

this.booksToShow.push(...filteredBooks);

结果发现点击分类后,右侧列表偶尔不更新,有时还会把上一类的内容叠加进来。排查过程是这样的:

  1. 先加日志,确认点击事件触发了,filteredBooks长度也正确了。
  2. 再检查push后booksToShow.length,发现数组对象确实变了。
  3. 最后才意识到:数组引用没有变,只是内容变了,ArkUI没有按预期刷新。

正确答案不是就地修改数组,而是重新赋值一个新数组。改成这样之后,问题立刻消失:

this.booksToShow = filteredBooks.slice();

这个案例的教训是:在ArkUI里,任何时候想“原地修改”一个被@State观察的数组,都要停下来想想替代方案。最简单的规则是:要换内容就换引用,不要原地改。

6.2 接口返回的数据“类型不匹配”:any类型的大麻烦

训练营提供的模拟接口,本来应该返回完整的分类和书籍信息。但我第一次接入时,在控制台发现启动直接报错:Type 'Object' is not assignable to type 'BookCategory[]'。这是因为接口层返回的解析结果在ArkTS的严格类型检查下,不能直接被强转到我的模型数组。

我当时的排查步骤:

  1. 打印接口返回值,确认JSON结构跟模型字段对得上。
  2. 尝试用as强转,编译器照样报错——这就是ArkTS跟TS的最大区别,它对as的信任度更低。
  3. 最终在服务层做了显式转换,把每个字段映射到新对象上,问题解决。

看起来是很琐碎的一步,但它其实是在提醒你:别把网络层的数据信任度带到页面层。类型安全不是靠“单个断言”实现的,而是靠每一层的数据转换完成的。

6.3 嵌套滚动容器:把List直接放进ListItem的翻车现场

做一个“分类下再挂明细列表”的需求时,我一度把整个List塞进ListItem内部,期望它实现嵌套滚动。结果页面渲染出来,内部列表高度塌陷,滚动区域互相打架,点击事件还经常串。

排查到最后,我发现问题不在于代码细节,而在于我选错了容器关系。ArkUI里,同一屏幕的滚动方向最好只有一个主控制器,嵌套滚动需要非常小心地设置高度策略。对我这个页面来说,正确的做法是:外层用Tabs或Row布局,把左右两侧拆成独立List,而不是把一个List嵌套进另一个List的ListItem。

这个坑看似基础,但它背后是ArkUI里一个非常重要的设计理念:组件树的层级关系,决定了一个页面能否正常滚动和点击。我复盘时总结了三条简单规则:

  • 同一方向不要轻易嵌套两个滚动容器。
  • 列表项内部如果需要展示多行信息,用Column布局,而不是内嵌List。
  • 真正需要复杂嵌套时,优先考虑Tabs、Swiper等官方容器,它们处理手势和滚动冲突的能力比我手写Row强得多。

6.4 调试手段:日志和状态快照比debugger好用

DAY9下半场,我发现console.log在ArkTS里的输出时机需要留意,有时候你看到某个状态变量的日志值已经是“旧值”,是因为日志打印的是对象引用,而不是当前快照。最靠谱的做法是打印一个显式拼接的字符串:

console.log(`selectedCategoryId=${this.selectedCategoryId}, count=${filteredBooks.length}`);

用模板字符串把整段信息拼出来,会比console.log(this.selectedCategoryId)这种单值打印更容易定位状态变化。另外,在组件里加一个临时的Text(...)显示当前selectedCategoryId, 会比反复在控制台翻日志更直观,特别是调试分类联动这种跟UI强相关的问题时。

7. 实践小结与下一步练习建议

囤一个分类数据页面的完整流程,今天我把它走完了。整个过程给我最深的印象有三个:类型先行,ArkTS用强类型逼着你先思考数据长什么样;状态驱动一切的渲染;网络阶段永远需要兜底设计。这些经验放到任何一个业务页面上都通用——分类数据只是一个最好的载体。

训练营DAY9之后,我自己还额外做了两个扩展练习,在这里写下来供你参考。

一是把静态数据完整替换成一个真实的本地模拟接口,再把请求层补上缓存。做法是在ApiClient里加一个简单的Map缓存,同一个URL在五分钟内不重复请求。这个小功能会帮你理解一个道理:网络层不只是“请求数据”,它还要处理重复请求、过期数据、失败重试这些真实世界的问题。

二是把“选中分类”的状态从单值升级成多选。比如改成“可同时勾选技术+科幻两个分类,右侧显示这两个分类的合集”。这个扩展练习看起来只是把selectedCategoryId从string改成string[],但过滤逻辑、UI高亮逻辑、数组更新逻辑全部要跟着变,是非常好的状态管理训练。

最后分享一个DAY9留给我的小技巧:写ArkTS页面时,先把组件用最笨的静态数据跑通,再逐步换成接口数据、加上状态联动。每一步只改一个变量,出问题时你的排查范围会小很多。分类数据获取与页面渲染实践这个主题,看起来简单,但它几乎覆盖了ArkUI开发里最核心的思考方式。你把这套思路吃透了,后面遇到搜索页、详情页、个人中心页,都会觉得顺手的多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 7:02:54

Flutter与OpenHarmony家具购买提醒功能实战

1. 从零开始拆解这个项目&#xff1a;为什么是“记录 提醒”这个组合拿到“Flutter for OpenHarmony 家具购买记录App实战&#xff1a;添加提醒实现”这个标题&#xff0c;我先给你说说我第一眼看到它的感觉&#xff1a;这个项目选得聪明。它没有贪大求全去做一个电商闭环&…

作者头像 李华
网站建设 2026/10/16 7:01:38

链表刷题第三天:虚拟头节点与双指针反转的实战思路

算法训练营第三天&#xff0c;任务列表里写着链表理论基础、203 移除链表元素、707 设计链表、206 反转链表。说实话&#xff0c;第一天学数组、第二天刷双指针&#xff0c;脑子里画的都是连续格子&#xff0c;今天突然要切换到链表的指针世界&#xff0c;确实有种被拽着转弯的…

作者头像 李华
网站建设 2026/10/16 7:01:30

B站视频下载与音频提取:yt-dlp+ffmpeg完整实践

如果你也跟我一样&#xff0c;遇到过好不容易刷到一条高质量教程&#xff0c;没过几天就被UP主动态删除的情况&#xff0c;或者想在通勤路上把视频内容变成音频反复复习&#xff0c;那你就会明白“BiliBili视频下载”和“提取音频”这两件事到底有多刚需。在线解析站和浏览器插…

作者头像 李华
网站建设 2026/10/16 7:01:03

STM32H743+QSPI驱动ST77903并适配LVGL的完整移植攻略

简介&#xff1a;面向穿戴设备显示方案的ST77903 LCD驱动与LVGL集成Demo&#xff0c;硬件平台为STM32H743&#xff0c;软件栈采用RT-Thread与QSPI接口&#xff0c;重点解决该IC无内置RAM时刷屏数据需连续传输、须由专用线程驱动的难点。资源包13.66MB&#xff0c;共2591个文件&…

作者头像 李华