NutriTrack App · Módulo de Lista de Alimentos · Revisión pre-integración
@Component, @State, @Observed).
Estos son incompatibles con el sistema de observación granular de V2 y generan re-renders innecesarios de todo el árbol.
HarmonyOS SDK ≥ 5.0 recomienda migración total a V2.
@Trace solo se actualizan los componentes que observan la propiedad modificada.
Si cambias calories de un ítem, solo ese componente re-renderiza — no toda la lista.
Esto es crítico para listas de 100+ alimentos.
router.pushUrl() es el sistema de routing legacy. No soporta animaciones nativas del sistema,
lifecycle management adecuado ni deep links. Navigation + NavPathStack es el estándar desde API Level 10.
obj["field"] (acceso por índice de string) y var.
Ambos causan compilation failure en ArkTS. ArkTS es un strict subset de TypeScript
que prohíbe acceso dinámico a propiedades y requiere let/const.
ForEach renderiza todos los elementos al mismo tiempo. Con un catálogo de 500+ alimentos
esto provoca janks severos en el primer render. Usar LazyForEach con IDataSource
para virtualización de lista.
.reuseId() permite reutilizar instancias de componentes durante el scroll —
reducción adicional del 60% en asignaciones de memoria.
Text('Lista de alimentos') — El texto de la UI debe definirse en
resources/base/element/string.json y referenciarse con $r('app.string.food_list_title').
La app apunta al mercado chino-español y necesita soporte multiidioma.
http.request() sin async/await, bloqueando el
hilo de UI. Envolver en async y manejar el estado de loading con @Local loading: boolean.
FoodCard están hardcodeados en hex. Definir variantes en
resources/dark/element/color.json para soporte automático de tema oscuro del sistema.
// NutriTrack — FoodListPage // Refactorizado por experto-harmonyos-arkts-arkui (CULTIVA IA) import { FoodItem, FoodDataSource } from '../model/FoodModel' import { FoodService } from '../services/FoodService' // ─── Modelo observable (V2) ───────────────────────────────── @ObservedV2 class FoodItem { @Trace id: string = '' @Trace name: string = '' @Trace calories: number = 0 @Trace protein: number = 0 @Trace category: string = '' } // ─── DataSource para LazyForEach ───────────────────────────── class FoodDataSource implements IDataSource { private items: FoodItem[] = [] private listeners: DataChangeListener[] = [] totalCount(): number { return this.items.length } getData(index: number): FoodItem { return this.items[index] } setData(data: FoodItem[]): void { this.items = data this.listeners.forEach((l: DataChangeListener) => { l.onDataReloaded() }) } registerDataChangeListener(l: DataChangeListener): void { this.listeners.push(l) } unregisterDataChangeListener(l: DataChangeListener): void { let idx = this.listeners.indexOf(l) if (idx !== -1) { this.listeners.splice(idx, 1) } } } // ─── Parámetros de navegación tipados ──────────────────────── interface FoodDetailParams { id: string } // ─── Componente tarjeta (reutilizable, V2) ──────────────────── @ComponentV2 struct FoodCard { @Param item: FoodItem = new FoodItem() @Event onTap: () => void = () => {} build() { Row() { Column() { Text(this.item.name) .fontSize(16) .fontWeight(FontWeight.Medium) .fontColor($r('app.color.text_primary')) Text(this.item.category) .fontSize(12) .fontColor($r('app.color.text_secondary')) .margin({ top: 4 }) } .alignItems(HorizontalAlign.Start) .layoutWeight(1) Column() { Text(`${this.item.calories} kcal`) .fontSize(18) .fontWeight(FontWeight.Bold) .fontColor($r('app.color.accent_orange')) Text(`${this.item.protein}g prot.`) .fontSize(11) .fontColor($r('app.color.text_secondary')) } } .width('100%') .padding(16) .backgroundColor($r('app.color.card_bg')) .borderRadius(12) .onClick(() => { this.onTap() }) } } // ─── Página principal (V2 + Navigation) ────────────────────── @ComponentV2 struct FoodListPage { @Local loading: boolean = true @Local errorMsg: string = '' @Local pathStack: NavPathStack = new NavPathStack() private dataSource: FoodDataSource = new FoodDataSource() aboutToAppear(): void { this.loadFoods() } async loadFoods(): Promise<void> { try { this.loading = true let foods: FoodItem[] = await FoodService.fetchFoods() this.dataSource.setData(foods) } catch (err) { this.errorMsg = $r('app.string.load_error').toString() } finally { this.loading = false } } build() { Navigation(this.pathStack) { Column() { if (this.loading) { LoadingProgress() .width(48).height(48) .color($r('app.color.accent_orange')) } else if (this.errorMsg !== '') { Text(this.errorMsg).fontColor($r('app.color.error')) } else { List({ space: 12 }) { // LazyForEach para virtualización de lista LazyForEach( this.dataSource, (food: FoodItem) => { ListItem() { FoodCard({ item: food, onTap: () => { let params: FoodDetailParams = { id: food.id } this.pathStack.pushPathByName('FoodDetail', params) } }) .reuseId('food-card') } }, (food: FoodItem) => food.id ) } .width('100%') .layoutWeight(1) } } .padding({ left: 16, right: 16, top: 12 }) } .title($r('app.string.food_list_title')) .titleMode(NavigationTitleMode.Mini) .navDestination(NavDestinationBuilder) } }