A volte utilizzando le API del client Firebase per Android, è necessario che Firebase svolga alcune operazioni su richiesta dello sviluppatore in modalità asincrona, come nel caso di alcuni dati richiesti che potrebbero non essere immediatamente disponibili o alcune attività messe in coda da eventualmente eseguire in futuro. Quando diciamo che alcune operazioni devono essere eseguite in modo asincrono in un’app, intendiamo che vengono eseguite simultaneamente al momento dell’attività principale di rendering delle visualizzazioni dell’app, ma non interferiscono con quell’attività. Per eseguire correttamente queste operazioni in modo asincrono nelle app Android, non dovranno occupare tempo nel thread principale di Android, altrimenti l’app potrebbe ritardare il rendering di alcuni frame, introducendo "jank (elementi superflui)" durante l’esperienza dell’utente o, persino peggio, una temuta ANR! Alcuni classici esempi che possono causare ritardi sono le richieste di rete, la scrittura e lettura di file, e i calcoli lunghi e complicati. Generalmente lo chiamiamo "blocking work" e non dovremmo mai bloccare il thread principale!





Quando uno sviluppatore utilizza un'API Firebase per richiedere un'operazione che normalmente blocca il thread principale, l'API deve fare in modo di eseguirla su un thread diverso, al fine di evitare jank e ANR. Al termine di tutto ciò, i risultati di questa operazione devono tornare al thread principale al fine di aggiornare in modo sicuro le visualizzazioni.

E l’API Task per Play Services serve proprio a questo, ossia a fornire un framework semplice, leggero e compatibile con Android, affinché le API del client Firebase (e Play Services) possano eseguire il lavoro in modo asincrono. È stata introdotta in Play Services versione 9.0.0 insieme a Firebase. Se utilizzi le funzionalità Firebase nella tua app, è possibile che tu abbia già usato l’API Task senza nemmeno rendertene conto! Quindi in questa serie di blog mi piacerebbe svelare alcuni dei metodi con cui le API di Firebase sfruttano i Task e parlare di alcuni schemi più avanzati.

Prima di iniziare però, è importante chiarire che l’API Task non sostituisce in tutto e per tutto le altre tecniche di threading in ambito Android. Il team di Android ha redatto del materiale molto valido per descrivere gli altri strumenti di threading, come ad esempio Services, Loaders e Handlers. E su YouTube puoi trovare un’intera stagione di Application Performance Patterns che illustra le diverse opzioni a tua disposizione. Alcuni sviluppatori scelgono anche librerie di terze parti che possono essere molto utili nel threading per le app Android. Quindi sta a te informarti e determinare quale sia la soluzione migliore per le tue particolari esigenze di threading. Le API Firebase utilizzano i Task in maniera uniforme per gestire le operazioni strutturate, che poi potrai utilizzare insieme ad altre strategie, se lo desideri.

Un esempio semplice di Task


Se stai usando Firebase Storage, prima o poi ti imbatterai nei Task. Ecco un esempio semplice di recupero di metadati per un file che è già stato caricato su Storage, preso direttamente dalla documentazione per metadati del file:
    // Create a storage reference from our app StorageReference storageRef = storage.getReferenceFromUrl("gs://"); // Get reference to the file StorageReference forestRef = storageRef.child("images/forest.jpg"); forestRef.getMetadata().addOnSuccessListener(new OnSuccessListener() { @Override public void onSuccess(StorageMetadata storageMetadata) { // Metadata now contains the metadata for 'images/forest.jpg' } }).addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception exception) { // Uh-oh, an error occurred! } });

Anche se in questo codice non vediamo mai un "Task", in realtà qui ce n’è in ballo uno. L’ultima parte del codice qui sopra potrebbe essere riscritta in modo equivalente così:
    Task task = forestRef.getMetadata(); task.addOnSuccessListener(new OnSuccessListener() { @Override public void onSuccess(StorageMetadata storageMetadata) { // Metadata now contains the metadata for 'images/forest.jpg' } }); task.addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception exception) { // Uh-oh, an error occurred! } });

Ah, ma allora c’era un Task nascosto da qualche parte!

Prometto che lo farò!


Grazie al precedente esempio di codice riscritto, diventa più evidente come sia usato un Task per ottenere i metadati di un file. Il metodo getMetadata() nello StorageReference deve presumere che i metadati del file non siano immediatamente disponibili e quindi eseguire una richiesta di rete per ottenerli. Pertanto, al fine di evitare di bloccare il thread di chiamata su quell’accesso alla rete, getMetaData() restituisce un Task che può essere ascoltato nel caso di un eventuale successo o meno. L'API poi provvede a eseguire la richiesta su un thread che controlla. L’API nasconde i dettagli di questo threading, ma il Task restituito viene utilizzato per indicare quando i risultati diventano disponibili. Il Task restituito quindi garantisce che tutti i listener aggiunti siano richiamati al termine. Questa forma di API che gestisce i risultati del lavoro asincrono è talvolta chiamata Promise in altri ambienti di programmazione.

È da notare che qui il Task restituito viene parametrizzato in base al tipo di StorageMetadata, e questo è anche il tipo di oggetto che viene passato a onSuccess() nell’OnSuccessListener. In realtà tutti i Task devono dichiarare un tipo generico in questo modo per indicare il tipo di dati che generano e l’OnSuccessListener deve condividere quel tipo generico. Inoltre, quando si verifica un errore, un'Eccezione viene passata a onFailure() nell’OnFailureListener e probabilmente sarà l'eccezione che ha causato l'errore in tal caso. Se vuoi ulteriori dettagli su quella particolare Eccezione, è possibile che tu debba controllarne il tipo al fine di assegnarla in modo sicuro al tipo previsto.

L'ultima cosa da sapere su questo codice è che i listener saranno chiamati sul thread principale. L’API Task fa in modo che ciò avvenga automaticamente. Quindi, se vuoi fare qualcosa in risposta quando lo StorageMetadata diventa disponibile, che deve accadere sul thread principale, potrai farlo proprio lì nel metodo listener (ma ricordati di non fare nessun lavoro di blocking nel listener del thread principale!). Hai diverse opzioni riguardo al funzionamento di questi listener di cui parlerò proprio nei miei prossimi post.

Hai un’unica chance


Altre funzioni di Firebase forniscono altre API che accettano listener non associati ai Task. Ad esempio, se stai usando Firebase Authentication, molto probabilmente hai registrato un listener per sapere quando l'utente accede o esce dalla tua app:
    private FirebaseAuth auth = FirebaseAuth.getInstance(); private FirebaseAuth.AuthStateListener authStateListener = new FirebaseAuth.AuthStateListener() { @Override public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) { // Welcome! Or goodbye? } }; @Override protected void onStart() { super.onStart(); auth.addAuthStateListener(authStateListener); } @Override protected void onStop() { super.onStop(); auth.removeAuthStateListener(authStateListener); }

L’API del client FirebaseAuth client ti garantisce due cose quando aggiungi un listener con addAuthStateListener(). La prima è che chiamerà immediatamente il listener con l'attuale stato noto di accesso dell'utente. La seconda è che chiamerà di nuovo il listener con tutte le successive modifiche di stato di accesso dell'utente, fintantoché il listener viene aggiunto all’oggetto FirebaseAuth. Questo comportamento è molto diverso da quello di Tasks!

Il Task chiama tutti i listener aggiunti al massimo una volta, e solo dopo che il risultato è disponibile. Inoltre il Task richiamerà immediatamente un listener se il risultato è già disponibile prima che il listener in questione sia aggiunto. L'oggetto Task ricorda in modo efficace l'oggetto del risultato finale e continua a distribuirlo a tutti i listener futuri, fino a quando non ha più listener e alla fine viene contrassegnato per la rimozione. Quindi, se stai utilizzando un’API Firebase che funziona con i listener per qualcosa di diverso da un oggetto Task, accertati di comprenderne i comportamenti e le garanzie. Non dare per scontato che tutti i listener Firebase si comportino come quelli Task!

E non dimenticare questo passaggio importante


Considera il lifetime dei tuoi listener Task aggiunti. Ci sono un paio di aspetti che possono andare storti se non esegui questa operazione. In primo luogo, puoi causare una perdita di Attività se il Task continua oltre il lifetime di un’Attività e delle relative Visualizzazioni che si riferiscono a un listener aggiunto. In secondo luogo, il listener può eseguire quando non è più necessario, causando uno spreco di lavoro ed eventualmente svolgere operazioni che accedono allo stato di Attività quando non è più valido. La prossima parte della mia serie di blog illustrerà proprio questi aspetti in modo più dettagliato, e come evitarli.

In conclusione (parte 1 di questa serie)


Abbiamo parlato brevemente dell’API Task per Play Services e svelato alcuni dei suoi utilizzi (a volte nascosti) in un esempio di codice Firebase. I Task rappresentano il modo con cui Firebase ti consente di rispondere alle operazioni a durata sconosciuta e che devono essere eseguite al di fuori del thread principale. Possono anche fare in modo che i listener vengano eseguiti sul thread principale per gestire i risultati del lavoro. Tuttavia abbiamo appena scalfito la superficie di ciò che i Task possono fare per te. La prossima volta ci occuperemo delle diverse variazioni dei listener Task cosi potrai decidere quale faccia al caso tuo.

Per ulteriori domande, usa l’hashtag #AskFirebase su Twitter oppure vai a dare un’occhiata al gruppo Firebase-talk Google. Ti consigliamo anche di andare a vedere il nostro canale Firebase Slack. E non dimenticare di seguirmi su Twitter @CodingDoug.