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:
- Crear el modelo ArticuloModelo
- Crear la fuente de datos DatosBase
- Integrar DatosBase con ListaDatos personalizada
- 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;
}