Spesso, nelle nostre applicazioni Flex dobbiamo caricare grandissime quantità di dati tramite un Web Service, un Remote Service o un HTTP Service, e potremmo andare incontro ad enormi tempi di latenza tra la richiesta dei dati e la risposta del server. Inoltre, gestire grandi quantità di dati comporta un utilizzo intenso della memoria.
Quando dobbiamo caricare un elenco lungo di record, possiamo adoperare la tecnica della paginazione, che ci consente di caricare piccole porzioni di dati a seconda della “pagina" che l'utente richiede di visualizzare.
James Ward, technical evangelist per Adobe, ha messo a punto una classe da utilizzare in combinazione all'oggetto AsyncListView
che consente di caricare “on fly" (il caricamento è effettuato su richiesta tramite un meccanismo lazy) gli elementi di un array collection definito come dataprovider di un component list based (datagrid, list ecc.).
La classe ideata da James Ward è un wrapper ad ArrayList e si chiama PagedList ed è scaricabile dal repository GitHub.
Come funziona il caricamento lazy
AsyncListView
è un wrapper alle collezioni di Flex e può essere utilizzata come dataprovider per i component list-based quali List o Datagrid. Lo scopo fondamentale di AsyncListView
è fornire un listener per gli eventi ItemPendingError
, che si verificano quando viene richiesta la visualizzazione di un elemento di una lista che in realtà non è disponibile.
Sfuttando questa caratteristica, la classe PagedList
consente di gestire la paginazione al volo dei dati da caricare all'interno di una lista.
Quando un elemento viene richiesto alla PagedList e non è stato ancora caricato tramite il servizio remoto, verrà generato un errore di tipo ItemPendingError
che verrà, poi, intercettato dal listener definito sul component AsyncListView
.
Di seguito è riportato un metodo della classe PagedList
, che si occupa di controllare se l'elemento da visualizzare è già presente in memoria o se deve essere richiesto al server:
public function getItemAt(index:int, prefetch:int = 0) : Object { if (fetchedItems[index] == undefined) { throw new ItemPendingError("itemPending"); } return _list.getItemAt(index, prefetch); }
Si noti come venga controllato lo stato dell'oggetto richiesto: se non è stato già caricato viene generato un errore di tipo ItemPendingError
, altrimenti viene recuperato dalla lista degli oggetti già disponibili.
Sviluppiamo un esempio
Sviluppiamo, ora, un esempio per chiarire in maniera esaustiva il modo in cui PagedList deve essere utilizzato. Ci limitiamo, però, ad utilizzare un servizio HTTP, effettuando una richiesta ad uno script PHP che restituisce una lista di oggetti encodati in JSON. Allo script verranno passati, ad ogni richiesta, l'indice da cui partire e il numero di elementi da restituire.
Per gestire la codifica e la decodifica di stringhe Json, utilizziamo la classe JSON presente nella libreria as3corelib, che è stata inclusa nel file con l'esempio completo sviluppato che potete scaricare alla fine di questo articolo.
Le richieste vengono inviate allo script PHP dal component PagedList in maniera automatica, ogni volta che nel datagrid viene richiesta la visualizzazione di un elemento che non è stato ancora caricato.
Prima di tutto, configuriamo le variabili che ci serviranno per invocare il caricamento degli oggetti da remoto, come mostrato di seguito (tra i tag fx:Declarations).
<fx:Declarations>
<fx:int id="items_per_pagina">25</fx:int>
<local:PagedList id="elenco_citta_paging" length="8094"/>
<s:AsyncListView id="async_elenco_citta" createPendingItemFunction="onItemPending" list="{elenco_citta_paging}" />
</fx:Declarations>
La variabile items_per_pagina
indica quanti elementi verranno restituiti dallo script ad ogni richiesta, mentre “elenco_citta_paging
” è l'oggetto PagedList che si occuperà di astrarre la funzionalità di caricamento on-demand dell'elenco di oggetti. L'oggetto “async_elenco_citta
” è, invece, l'handler degli eventi di errore che si verificano quando il datagrid cerca di visualizzare un elemento al momento non disponibile.
Ora dobbiamo implementare il metodo onItemPending
, che viene invocato dall'oggetto AsyncListView
per gestire l'errore di visualizzazione all'interno del datagrid. L'implementazione di questo metodo è divisa in due parti: la prima in cui vengono calcolati l'indice e il numero di elementi da caricare e la seconda parte in cui viene effettuata la richiesta allo script php e viene gestita la risposta.
Nella prima parte, quindi, calcoliamo l'indica da cui iniziare a caricare gli elementi. Da notare è il controllo che viene effettuato alla fine di questa prima parte, importante se ci troviamo alla fine della lista: in questo caso gli elementi da caricare potrebbero essere meno di “items_da_caricare” e quindi viene rimodulato questo valore. Questo è un caso particolare (lo potremmo definire “condizione al contorno”) che, comunque, deve essere gestita per evitare errori nell'applicazione:
var pagina:uint = Math.floor(index / items_per_pagina); if (pagineCaricate[pagina] == undefined) { //calcolo dell'indice di pagina e numero di elementi da caricare var items_da_caricare:uint = items_per_pagina; var start_index:uint = items_per_pagina * pagina; var end_index:uint = start_index + items_per_pagina - 1; if (end_index > elenco_citta_paging.length) { items_da_caricare = elenco_citta_paging.length - start_index; } [...]
Nella seconda parte, invece, ci preoccupiamo di effettuare la richiesta remota e di gestirne il risultato. Lo script php utilizzato in questo esempio vuole due parametri, passati tramite metodo GET del protocollo HTTP: start_index
e page_size
. Il risultato restituito dall'invocazione dello script è una lista di oggetti che verranno copiati nel nostro oggetto PagedList
, utilizzato a sua volta come dataprovider del datagrid.
[...] var httpService:HTTPService = new HTTPService(); httpService.method = "GET"; httpService.url = urlString; var parameters:Object = new Object(); parameters.start_index = start_index; parameters.page_size = items_da_caricare; var asyncToken:AsyncToken = httpService.send(parameters); asyncToken.addResponder(new AsyncResponder(function result(event:ResultEvent, token:Object = null) : void { var s:String = event.result as String; var list:Array = JSON.decode(s) as Array; for (var i:int = 0; i < list.length; i++) { elenco_citta_paging.setItemAt(list[i], token + i); } pagineCaricate[pagina] = true; }, function fault(event:FaultEvent, token:Object = null):void {}, start_index)); } return null; }
Mostriamo, per completezza, anche lo script PHP che legge l'elenco di città da un file CSV ed effettua la codifica JSON della porzione di array richiesta dall'applicazione Flex. Si nota come i parametri richiesti siano “start_index“ e “page_size”.
<?php $handle = fopen("elenco.csv", "r"); $list = array(); while(($r = fgetcsv($handle)) !== FALSE) { $list[] = $r; } fclose($handle); $return = array_slice($list, $_GET['start_index'], $_GET['page_size']); echo json_encode($return); ?>
Potete scaricare il file zip con i codici sorgenti, sia Flex che PHP.
Nota: nell'esempio (anch'esso disponibile per il download) è presente un file di configurazione xml, in cui è impostato l'url dello script php utilizzato dall'applicazione Flex per ottenere i dati da visualizzare.
Conclusione
In applicazioni reali, in alcuni casi, la quantità di dati da caricare è enorme e non si può rischiare di far attendere all'utente tempi molto lunghi per poter interagire con l'interfaccia grafica. La soluzione presentata in questo articolo, già applicata con opportuni raffinamenti in contesti reali, si è mostrata molto prestante in quanto consente di ridurre notevolmente i tempi di attesa dell'utente e di migliorare il tempo di risposta delle applicazioni web.
In generale, il concetto di lazy loading, dovrebbe essere molto diffuso nello sviluppo di applicazioni web per evitare l'allungamento dei tempi di risposta e il conseguente calo dell'usabilità delle applicazioni.