Implementación práctica de listas y carga diferida en HarmonyOS NEXT

Implementación práctica de HarmonyOS NEXT##Desarrollo de aplicaciones HarmonyOS##Educación##


Objetivo: Implementar un diseño de lista que cargue los elementos de manera diferida mediante lazy loading.

Requisito previo: Necesita solicitar el permiso ohos.permission.INTERNET.

Enfoque de implementación:

  1. Crear el modelo ArticuloModelo
  2. Crear la fuente de datos DatosBase
  3. Integrar DatosBase con ListaDatos personalizada
  4. implementar el bucle LazyForEach en la página

Restricciones de uso de LazyForEach

  • LazyForEach debe utilizarse dentro de un componente contenedor; solo List, Grid, Swiper y WaterFlow soportan carga diferida de datos (se puede configurar el atributo cachedCount, es decir, solo cargar la parte visible junto con una pequeña cantidad de datos adelante y atrás para缓冲), otros componentes todavía cargan todos los datos de una vez.
  • LazyForEach depende de las claves generadas para determinar si actualizar el subcomponente; si la clave no cambia, no se activará la actualización del subcomponente correspondiente en LazyForEach.
  • Al usar LazyForEach dentro de un componente contenedor, solo puede contener un LazyForEach. Por ejemplo, con List, no se recomienda incluir ListItem, ForEach y LazyForEach al mismo tiempo; tampoco se recomienda包含 múltiples LazyForEach.
  • LazyForEach debe crear y solo pemritir un subcomponente en cada iteración; es decir, la función de generación de subcomponentes de LazyForEach debe tener solo un componente raíz.
  • Los subcomponentes generados deben ser permitidos como subcomponentes del contenedor padre de LazyForEach.
  • Se permite que LazyForEach esté contenido en declaraciones de renderizado condicional if/else, y también se permiten declaraciones if/else dentro de LazyForEach.
  • El generador de claves debe generar un valor único para cada dato; si las claves son iguales,会出现 problemas de renderizado para los componentes de UI con claves duplicadas.
  • LazyForEach debe usar el objeto DataChangeListener para actualizar; reasignar el primer parámetro dataSource causará errores; cuando dataSource usa una variable de estado, el cambio de estado no activará la actualización de UI de LazyForEach.
  • Para un renderizado de alto rendimiento, al actualizar la UI a través del método onDataChange del objeto DataChangeListener, se necesita generar una clave diferente de la anterior para activar la actualización del componente.
  • LazyForEach debe usarse junto con el decorador @Reusable para activar la reutilización de nodos. Método de uso: aplique el decorador @Reusable al componente de la lista LazyForEach, ver reglas de uso.

ArticuloModelo

export interface ArticuloModelo{
  idArticulo:string,
  nombreArticulo:string,
  telefono:string,
  imagenUrl:string,
  idTienda:string,
  nombreTienda:string,
  nivelArticulo:string,
  numeroOrden:string,
}


DatosBase

export class DatosBase<T> implements IDataSource {
  private listeners: DataChangeListener[] = [];
  private datosOriginales: T[] = [];

  public totalElementos(): number {
    return 0;
  }

  public obtenerDato(indice: number): T {
    return this.datosOriginales[indice];
  }

  registrarListenerCambio(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      console.info('agregar listener');
      this.listeners.push(listener);
    }
  }

  eliminarListenerCambio(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      console.info('eliminar listener');
      this.listeners.splice(pos, 1);
    }
  }

  notificarRecarga(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded();
    })
  }

  notificarAgregado(indice: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(indice);
    })
  }

  notificarCambio(indice: number): void {
    this.listeners.forEach(listener => {
      listener.onDataChange(indice);
    })
  }

  notificarEliminacion(indice: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(indice);
    })
  }

  notificarMovimiento(desde: number, hasta: number): void {
    this.listeners.forEach(listener => {
      listener.onDataMove(desde, hasta);
    })
  }

  notificarCambioConjunto(operaciones: DataOperation[]): void {
    this.listeners.forEach(listener => {
      listener.onDatasetChange(operaciones);
    })
  }
}


ListaDatos

import { DatosBase } from "./DatosBase";
import { ArticuloModelo } from "./ArticuloModelo";

export class ListaDatos extends DatosBase<ArticuloModelo> {
  private articulos: ArticuloModelo[] = [];

  public totalElementos(): number {
    return this.articulos.length;
  }

  public obtenerDato(indice: number): ArticuloModelo {
    return this.articulos[indice];
  }

  obtenerTodos():ArticuloModelo[] | null{
    return this.articulos
  }

  public agregarDato(dato: ArticuloModelo): void {
    this.articulos.push(dato);
    this.notificarAgregado(this.articulos.length - 1);
  }
}


PaginaListaDemo

import { router } from '@kit.ArkUI';
import { ListaDatos } from './ListaDatos';
import { ArticuloModelo } from './ArticuloModelo';

@Entry
@Component
struct PaginaListaDemo {
@StorageProp('alturaRectInferior')
alturaRectInferior: number = 0;
@StorageProp('alturaRectSuperior')
alturaRectSuperior: number = 0;
@State paginaActual: number = 1
total: number = 0
private fuente: ListaDatos = new ListaDatos();
estaCargando: boolean = false;
@State cargaExitosa: boolean = async aboutToAppear(): Promise {
await this.inicializarDatos()
}

async inicializarDatos() {
this.estaCargando = true;
await this.obtenerLista()
this.estaCargando = false;
}

async obtenerLista() {
const parametros: Parametros = {
"data": { "idTienda": 331, "idCiudad": 320100 },
"numeroPagina": this.paginaActual,
"tamanoPagina": 10
}
//Simular datos
this.total = 20;
for (let i = 0; i <= 20; i++) {
this.fuente.agregarDato({
idArticulo: i.toString(),
nombreArticulo: 'Producto' + (Math.floor(Math.random() * 100) + 1),
telefono: '12341234' + i,
imagenUrl: 'https://ejemplo.com/imagenes/producto' + i + '.jpg',
idTienda: 'idTienda' + i,
nombreTienda: 'nombreTienda' + i,
nivelArticulo: '1',
numeroOrden: i.toString(),
})
}
}

build() {
Column({ space: 10 }) {
this.encabezado()
this.contenido()
}
.width('100%')
.height('100%')
.padding({ top: this.alturaRectSuperior })
}

@Builder
encabezado() {
Row() {
Row({ space: 20 }) {
Image($r('app.media.icono_volver'))
.width(18)
.height(12)
.responseRegion([{
x: -9,
y: -6,
width: 36,
height: 24
}])
Text('Lista de Productos')
.fontWeight(700)
.fontColor('#525F7F')
.fontSize(16)
.lineHeight(22)
}

Row({ space: 6 }) {
SymbolGlyph($r('sys.symbol.limpiar'))
.fontSize(18)
.renderingStrategy(SymbolRenderingStrategy.SINGLE)
.fontColor([Color.Black])
Text('Limpiar caché local')
.fontSize(14)
.fontColor(Color.Black)
}
.onClick(() => {
router.replaceUrl({ url: 'pages/PaginaListaProductos' })
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding({ left: 20, right: 20 })
}

@Builder
contenido() {
List() {
LazyForEach(this.fuente, (elemento: ArticuloModelo) => {
ListItem() {
Row({ space: 10 }) {
this.construirImagen(elemento.imagenUrl != '' ? elemento.imagenUrl : 'https://ejemplo.com/imagenes/placeholder.jpg')

Column() {
Text(elemento.nombreArticulo)
}
.layoutWeight(1)
}
.width('100%')
.height(100)
}
.borderRadius(4)
.clip(true)
.backgroundColor(Color.White)
.margin({ right: 20, left: 20, top: 10 })
}, (elemento: string) => elemento)

ListItem().height(this.alturaRectInferior)
}
.width('100%')
.backgroundColor('#F8F9FE')
.layoutWeight(1)
.cachedCount(15)
.scrollBar(BarState.Off)
.onReachEnd(async () => {
if (!this.estaCargando) {
this.estaCargando = true;
this.paginaActual++
await this.obtenerLista()
this.estaCargando = false;
}
})
}

@Builder
construirImagen(src:string){
Row() {
if (this.cargaExitosa) {
Image(src)
.width('100%')
.height('100%')
.onError(() => {
this.cargaExitosa = false
})
} else {
Text('Error al cargar imagen...').margin(10).fontColor(Color.Gray)
}
}
.width('50%')
.height(100)
.backgroundColor('#eeeeee')
.justifyContent(FlexAlign.Center)
}
}

interface Parametros {
"data": Record;
"numeroPagina": number;
"tamanoPagina": number;
}

Publicado el 6-30 05:28