Una form di attesa per operazioni lunghe in una mobile application (iOS/Android)
Introduzione
Un fattore fondamentale del successo delle nostre applicazioni mobile è la loro fluidità: molti utenti “pretendono” che le nostre applicazioni siano sempre reattive e non ci siano momenti di “freeze” che vengono immediatamente classificati come malfunzionamenti.
Nello sviluppo di una applicazione mobile, può capitare di dover effettuare delle operazioni lunghe in termini di tempo che possono pregiudicare la responsività della nostra interfaccia utente. Il problema non è certo nuovo e anche in applicazioni tradizionali (desktop) per risolverlo si può ricorrere alla programmazione multithread, eseguendo l’operazione in background e lasciando il thread principale della nostra applicazione libero di rispondere alle esigenze dell’utente.
Vediamo come, in una applicazione FireMonkey mobile, si può realizzare una form di attesa generica, utile per dare l’impressione al nostro utente che l’applicazione sia effettivamente “viva” e allo stesso tempo fornire alcuni dettagli sull’operazione in corso.
Prenderò come esempio di operazione lunga una GET HTTP, caso abbastanza comune e caratterizzato da alcune caratteristiche peculiari:
- si tratta di un’operazione bloccante (cioè il flusso di esecuzione è interrotto dall’inizio alla fine dell’operazione);
- può richiedere un tempo variabile (a seconda della connettività e della risorsa HTTP richiesta);
- sono disponibili dei dettagli sullo stato di avanzamento dell’operazione (es. bytes scaricati).
- l’esecuzione di operazioni su un database locale;
- l’apertura di un dataset remoto;
- la fruizione di servizi remoti (REST, SOAP, XML-RPC ecc);
- operazioni di calcolo complesse;
- operazioni di rendering grafico;
- qualunque operazione che abbia tempi di attesa di più di qualche decimo di secondo.
1) Eseguire una operazione bloccante
Supponiamo di avere creato una nuova applicazione FireMonkey mobile e avere un componente TIdHTTP da usare per eseguire la GET HTTP e salvare la risposta del server in un TMemoryStream:
var LContent: TMemoryStream; LDetails: string;
// …
LContent := TMemoryStream.Create;
try
IdHttp1.Get(‘http://www.adomain.com/test/large_image.png’, LContent);
LDetails := LContent.Size.ToString + ‘ bytes’;
finally
LContent.Free;
end;
2) Otteniamo i dettagli di avanzamento dell’operazione
- il primo permette di sapere che l’operazione è stata avviata e come parametro (AWorkCountMax) offre l’indicazione della dimensione totale del file da scaricare;
- OnWorkEnd è, come è facile intuire, la notifica di operazione completata (non ci interessa particolarmente);
- OnWork viene chiamato periodicamente durante l’operazione e ha un parametro AWorkCount che ci permette di sapere a che punto siamo con il download del file.
3) Prepariamo una form di attesa da mostrare all’utente
- una TLabel (TitleLabel) mostrerà un titolo nella parte superiore della form;
- un TLayout (ProgressLayout) ospiterà delle informazioni sullo stato di avanzamento dell’operazione (usando una TProgressBar e un’altra TLabel), nella parte inferiore della form;
- un TLayout (ImageLayout) ospiterà una TImage con della grafica (che potete personalizzare a piacimento, io ho usato l’immagine di una rotella da ingranaggio).
4) Incastoniamo l’esecuzione dell’operazione durante la visualizzazione della form
- lanciare l’operazione lunga;
- mostrare la form di attesa “viva” (cioè animata e comunque responsiva ad eventuali azioni dell’utente, come una richiesta di annullamento dell’operazione stessa);
- alla conclusione dell’operazione, chiudere la form di attesa e magari eseguire qualcosa con i dati scaricati.
Cerchiamo di impacchettare il codice dell’operazione in un metodo anonimo per passarlo “come se fosse una variabile” alla form di wait che lo eseguirà al momento opportuno (cioè dopo essersi mostrata all’utente e aver avviato l’animazione):
procedure var LContent: TMemoryStream; begin LContent := TMemoryStream.Create; try IdHttp1.Get(eHTTPGetURL.Text, LContent); finally LContent.Free; end; end);
Prepariamo un metodo nella form di attesa, che riceva questo metodo anonimo che rappresenta il Task da eseuire, insieme ad altri due metodi anonimi che rappresentano il codice da eseguire al completamento dell’esecuzione del task e quello da eseguire in caso avvengano delle eccezioni durante l’esecuzione dello stesso:
procedure TWaitForm.Run(ATask, AOnComplete: TProc(twaitform); AOnError: TProc(twaitform, exception)); begin Show; WaitAnimation.Start;
TThread.CreateAnonymousThread(
procedure
begin
try
FHadErrors := False;
ATask(Self);
except on E:Exception do
DoErrorHandling(E, AOnError);
end;
DoOnCompleted(AOnComplete);
end).Start;
end;
- i parametri ATask, AOnComplete e AOnError sono dei riferimenti a metodi anonimi con un certo numero e tipo di parametri. La sintassi utilizzata è quella dei generics (altro potente meccanismo introdotto nelle ultime versioni di Delphi e presente il molti altri linguaggi di programmazione), con le parentesi angolari: ATask e AOnComplete sono due procedure che hanno come parametro un’istanza di TWaitForm.
L’idea è quella di passare al metodo Run le tre cose da fare (esecuzione task, esecuzione codice dopo completamento del task e esecuzione codice in caso di errori), in modo che possa comporle nel modo più appropriato; - l’esecuzione di ATask è protetta da un blocco try..except che garantisce l’esecuzione di AOnError in caso di errori (e trappa l’eccezione, in modo che non affiori all’esterno);
- la form ha un field FHadErrors che tiene traccia del presentarsi di eventuali eccezioni (utile per esempio nel codice di AOnComplete);
- l’esecuzione del task, della gestione delle eccezioni e delle operazioni post-conclusione del task vengono effettuate in un thread separato, grazie all’utilizzo di TThread.CreateAnonymousThread, che permette di eseguire un metodo anonimo in un nuovo thread, in background;
- eseguire un pezzo di codice variabile (ATask) in un thread secondario;
- avere una chance di agganciare altro codice variabile (AOnError e AOnComplete) a due eventi principali di interesse: i casi di errore e la notifica di completamento di ATask;
- avere a disposizione in questi pezzi di codice variabili (metodi anonimi) il riferimento alla form di attesa specifica, che può esporre alcuni metodi utili per la modifica del contenuto a video (es. stato di avanzamento), avendo l’opportunità di nascondere al programmatore la necessità di sincronizzare l’esecuzione fra thread diversi.
procedure SetupProgress(const AProgress, AMin, AMax: Single; const AMessage: string = '');
procedure UpdateProgress(const AProgress: Single; const AMessage: string = ”);
Ad esempio, riporto l’implementazione di UpdateProgress che fa uso di TThread.Synchronize per eseguire il codice che manipola gli elementi della GUI in modo safe rispetto al thread principale di esecuzione:
procedure TWaitForm.UpdateProgress(const AProgress: Single; const AMessage: string); begin TThread.Synchronize( TThread.CurrentThread, procedure begin ProgressBar.Value := AProgress; ProgressLabel.Text := AMessage; if AMessage = '%' then ProgressLabel.Text := Format('%.1f', [100*(ProgressBar.Value / ProgressBar.Max)]) + '%';
ProgressBar.Visible := AProgress > 0;
ProgressLabel.Visible := AMessage <> ”;
end);
end;
5) Esempi di utilizzo
Se alcuni passaggi esposti finora vi sembrano un po’ complicati o poco chiari, non dovete preoccuparvi (al netto magari di voler approfondire qualche argomento 🙂 ) perchè in realtà il codice necessario per utilizzare la form è molto più semplice, ad esempio potreste avere un button con il seguente handler di OnClick:
var LDetails: string; begin FWaitForm := TWaitForm.CreateAndRun( 'Long operation: HTTP GET', // titolo
procedure(AWaitForm: TWaitForm) // ATask
var
LContent: TMemoryStream;
begin
LDetails := ”;
LContent := TMemoryStream.Create;
try
IdHttp1.Get(eHTTPGetURL.Text, LContent);
LDetails := LContent.Size.ToString + ‘ bytes’;
finally
LContent.Free;
end;
end,
procedure(AWaitForm: TWaitForm) // AOnCompleted
begin
if not AWaitForm.HadErrors then
Memo1.Lines.Add(‘Completed: ‘ + LDetails);
end,
procedure (AWaitForm: TWaitForm; AException: Exception) // AOnError
begin
Memo1.Lines.Add(‘Error: ‘ + AException.Message);
end
);
end;
Nei gestori di evento del componente TIdHTTP, grazie al fatto che ci salviamo il riferimento alla TWaitForm in FWaitForm, possiamo scrivere semplicemente:
procedure TForm1.IdHTTP1Work(ASender: TObject; AWorkMode: TWorkMode; AWorkCount: Int64); begin FWaitForm.UpdateProgress(AWorkCount, '%'); end;
procedure TForm1.IdHTTP1WorkBegin(ASender: TObject; AWorkMode: TWorkMode;
AWorkCountMax: Int64);
begin
FWaitForm.SetupProgress(0, 0, AWorkCountMax, ‘Downloading…’);
end;
Il risultato è mostrato (senza animazione ovviamente 🙂 ) di seguito. Il primo screenshot mostra l’applicazione in esecuzione nel simulatore iOS, mentre il secondo è preso direttamente dal mio smartphone Android:
Conclusioni
Materiale
Source code – Link to DropBox
Sei un grande. Grazie
Ottima. Complimenti.
Tutto molto bello, unica pecca che ho notato è il codice ridondante passato al parametro ATask: TProc
In sostanza in tutti e 3 i bottoni è stata passata la medesima funzione.
E' possibile dichiarare la funzione una sola volta e poi passarla come parametro in tutti e 3 i bottoni?
Se si, come si fa?
Very useful, Thank you
La sto provando su win, alla fine del processo mi chiude la form chiamante mandandomi in crash l’applicazione. Mi potete aiutare ? Grazie