Questa guida include best practice e un'architettura consigliata per creare app robuste e di alta qualità.
Esperienze utente nell'app mobile
Una tipica app per Android contiene più componenti dell'app, tra cui attività, frammenti, servizi, provider di contenuti e ricevitori di trasmissioni. La maggior parte di questi componenti dell'app vengono dichiarati nel file manifest dell'app. Il sistema operativo Android utilizza questo file per decidere come integrare la tua app nell'esperienza utente generale del dispositivo. Poiché una tipica app per Android potrebbe contenere più componenti e gli utenti spesso interagiscono con più app in un breve periodo di tempo, le app devono adattarsi a diversi tipi di flussi di lavoro e attività guidati dagli utenti.
Diversi fattori di forma
Le app possono essere eseguite su più fattori di forma, inclusi non solo gli smartphone, ma anche tablet, pieghevoli, dispositivi ChromeOS e altro ancora. Un'app non può presupporre un orientamento verticale o orizzontale; potrebbe essere eseguita in entrambi o in uno solo in una singola sessione. Le modifiche alla configurazione, ad esempio quando un utente cambia la postura di un dispositivo pieghevole in modalità tablet o libro, possono forzare la ricomposizione della UI dell'app, il che può influire sui dati e sullo stato dell'app.
Vincoli delle risorse
Tieni presente che anche i dispositivi mobili hanno risorse limitate, quindi in qualsiasi momento il sistema operativo potrebbe interrompere alcuni processi delle app per fare spazio a quelli nuovi.
Condizioni di lancio variabili
Date le condizioni di questo ambiente, è possibile che i componenti dell'app vengano avviati singolarmente e in modo non sequenziale e che il sistema operativo o l'utente possano distruggerli in qualsiasi momento. Poiché questi eventi non sono sotto il tuo controllo, non devi memorizzare o conservare in memoria dati o stati dell'applicazione nei componenti dell'app e i componenti dell'app non devono dipendere l'uno dall'altro.
Principi architettonici comuni
Se non devi utilizzare i componenti dell'app per archiviare i dati e lo stato dell'applicazione, come devi progettare la tua app?
Man mano che le app per Android aumentano di dimensioni, è importante definire un'architettura che consenta all'app di scalare, si adatti a forme e dimensioni diverse, aumenti la robustezza dell'app e ne semplifichi il test.
Un'architettura dell'app definisce i confini tra le parti dell'app e le responsabilità di ciascuna parte. Per rispettare tutte le linee guida, devi progettare l'architettura dell'app in modo che segua alcuni principi specifici.
Separazione delle preoccupazioni
Il principio più importante da seguire è la separazione dei
problemi.
È un errore comune scrivere tutto il codice in un
Activity
o in un Fragment
. Queste classi basate sulla UI
devono contenere solo la logica che gestisce le interazioni con la UI e il sistema operativo. Mantenendo queste classi il più snelle possibile, puoi evitare molti problemi relativi al ciclo di vita dei componenti e migliorare la testabilità di queste classi.
Tieni presente che le implementazioni di Activity
e Fragment
non sono di tua proprietà.
Si tratta solo di classi di collegamento che rappresentano il contratto tra il
sistema operativo Android e la tua app. Il sistema operativo può distruggerle in qualsiasi momento in base alle interazioni degli utenti o a causa di condizioni di sistema come la memoria insufficiente. Per offrire un'esperienza utente soddisfacente e un'esperienza di manutenzione dell'app più gestibile, è meglio ridurre al minimo la dipendenza da questi servizi.
Layout adattivi
La tua app deve gestire correttamente le modifiche alla configurazione, ad esempio quando l'utente passa dall'orientamento verticale a quello orizzontale del dispositivo e a diverse dimensioni del display, ad esempio quando l'app viene eseguita su schermi di grandi dimensioni. Un'app che implementa i layout adattivi canonici offre un'esperienza utente ottimale su una serie di fattori di forma.
UI di Drive dai modelli di dati
Un altro principio importante è che devi basare la tua UI sui modelli di dati, preferibilmente modelli persistenti. I modelli di dati rappresentano i dati di un'app. Sono indipendenti dagli elementi dell'interfaccia utente e da altri componenti dell'app. Ciò significa che non sono legati al ciclo di vita dell'interfaccia utente e dei componenti dell'app, ma verranno comunque eliminati quando il sistema operativo decide di rimuovere il processo dell'app dalla memoria.
I modelli persistenti sono ideali per i seguenti motivi:
I tuoi utenti non perdono dati se il sistema operativo Android distrugge la tua app per liberare risorse.
La tua app continua a funzionare nei casi in cui una connessione di rete è instabile o non disponibile.
Se basi l'architettura della tua app sulle classi del modello di dati, la rendi più testabile e solida.
Unica fonte attendibile
Quando nella tua app viene definito un nuovo tipo di dati, devi assegnargli una singola fonte di verità (SSOT). L'SSOT è il proprietario di questi dati e solo l'SSOT può modificarli o mutarli. A questo scopo, l'SSOT espone i dati utilizzando un tipo immutabile e, per modificarli, espone funzioni o riceve eventi che altri tipi possono chiamare.
Questo pattern offre numerosi vantaggi:
- Centralizza tutte le modifiche a un particolare tipo di dati in un unico posto.
- Protegge i dati in modo che altri tipi non possano manometterli.
- Rende più tracciabili le modifiche ai dati. In questo modo, è più facile individuare i bug.
In un'applicazione offline-first, la fonte di verità per i dati dell'applicazione è in genere un database. In altri casi, la fonte di riferimento può essere un ViewModel o persino la UI.
Flusso di dati unidirezionale
Il principio di un'unica fonte attendibile viene spesso utilizzato nelle nostre guide con il pattern di flusso di dati unidirezionale (UDF). Nella funzione definita dall'utente, state fluisce in una sola direzione. Gli eventi che modificano il flusso di dati nella direzione opposta.
In Android, lo stato o i dati di solito scorrono dai tipi con ambito più ampio della gerarchia a quelli con ambito più ristretto. Gli eventi vengono in genere attivati dai tipi con ambito inferiore fino a raggiungere la SSOT per il tipo di dati corrispondente. Ad esempio, i dati dell'applicazione di solito vengono trasferiti dalle origini dati alla UI. Gli eventi utente, come la pressione dei pulsanti, vengono trasferiti dalla UI all'SSOT, dove i dati dell'applicazione vengono modificati ed esposti in un tipo immutabile.
Questo pattern mantiene meglio la coerenza dei dati, è meno soggetto a errori, è più facile da eseguire il debug e offre tutti i vantaggi del pattern SSOT.
Architettura dell'app consigliata
Questa sezione mostra come strutturare l'app seguendo le best practice consigliate.
Considerando i principi architettonici comuni menzionati nella sezione precedente, ogni applicazione dovrebbe avere almeno due livelli:
- Il livello UI che mostra i dati dell'applicazione sullo schermo.
- Il livello dati che contiene la logica di business della tua app ed espone i dati dell'applicazione.
Puoi aggiungere un ulteriore livello chiamato livello di dominio per semplificare e riutilizzare le interazioni tra i livelli UI e dati.

Architettura delle app moderne
Questa architettura moderna delle app incoraggia l'utilizzo delle seguenti tecniche, tra le altre:
- Un'architettura adattiva e stratificata.
- Flusso di dati unidirezionale (UDF) in tutti i livelli dell'app.
- Un livello UI con contenitori di stato per gestire la complessità della UI.
- Coroutine e flussi.
- Best practice per l'inserimento delle dipendenze.
Per saperne di più, consulta le sezioni seguenti, le altre pagine relative all'architettura nel sommario e la pagina dei consigli, che contiene un riepilogo delle best practice più importanti.
Livello UI
Il ruolo del livello UI (o livello di presentazione) è quello di visualizzare i dati dell'applicazione sullo schermo. Ogni volta che i dati cambiano, a causa dell'interazione dell'utente (ad esempio la pressione di un pulsante) o di un input esterno (ad esempio una risposta di rete), l'interfaccia utente deve aggiornarsi per riflettere le modifiche.
Il livello UI è composto da due elementi:
- Elementi dell'interfaccia utente che visualizzano i dati sullo schermo. Questi elementi vengono creati utilizzando funzioni View o Jetpack Compose. Sia Views che Jetpack Compose supportano i layout adattivi.
- Titolari dello stato (come le classi ViewModel) che contengono i dati, li espongono all'interfaccia utente e gestiscono la logica.

Per saperne di più su questo livello, consulta la pagina Livello UI.
Livello dati
Il livello dati di un'app contiene la logica di business. La logica di business è ciò che dà valore alla tua app. È costituita da regole che determinano il modo in cui la tua app crea, archivia e modifica i dati.
Il livello dati è costituito da repository che possono contenere da zero a molte origini dati. Devi creare una classe repository per ogni tipo di dati
gestiti nella tua app. Ad esempio, potresti creare una classe MoviesRepository
per i dati relativi ai film o una classe PaymentsRepository
per i dati
relativi ai pagamenti.

Le classi del repository sono responsabili delle seguenti attività:
- Esposizione dei dati al resto dell'app.
- Centralizzare le modifiche ai dati.
- Risoluzione dei conflitti tra più origini dati.
- Astraendo le origini dati dal resto dell'app.
- Contenente la logica di business.
Ogni classe di origine dati deve essere responsabile della gestione di una sola origine dati, che può essere un file, un'origine di rete o un database locale. Le classi dell'origine dati sono il ponte tra l'applicazione e il sistema per le operazioni sui dati.
Per saperne di più su questo livello, consulta la pagina del livello dati.
Livello del dominio
Il livello del dominio è un livello facoltativo che si trova tra i livelli UI e dati.
Il livello del dominio è responsabile dell'incapsulamento di logiche di business complesse o più semplici che vengono riutilizzate da più ViewModel. Questo livello è facoltativo perché non tutte le app hanno questi requisiti. Dovresti utilizzarlo solo quando necessario, ad esempio per gestire la complessità o favorire la riusabilità.

Le classi in questo livello sono comunemente chiamate casi d'uso o interattori. Ogni caso
d'uso deve avere la responsabilità di una singola funzionalità. Ad esempio, la tua app potrebbe avere una classe GetTimeZoneUseCase
se più ViewModel si basano sui fusi orari per visualizzare il messaggio corretto sullo schermo.
Per saperne di più su questo livello, consulta la pagina del livello di dominio.
Gestire le dipendenze tra i componenti
Le classi nella tua app dipendono da altre classi per funzionare correttamente. Puoi utilizzare uno dei seguenti pattern di progettazione per raccogliere le dipendenze di una determinata classe:
- Inserimento delle dipendenze (DI): l'inserimento delle dipendenze consente alle classi di definire le proprie dipendenze senza costruirle. In fase di runtime, un'altra classe è responsabile della fornitura di queste dipendenze.
- Service Locator: Il pattern Service Locator fornisce un registro in cui le classi possono ottenere le proprie dipendenze anziché costruirle.
Questi pattern ti consentono di scalare il codice perché forniscono pattern chiari per la gestione delle dipendenze senza duplicare il codice o aggiungere complessità. Inoltre, questi pattern ti consentono di passare rapidamente dalle implementazioni di test a quelle di produzione.
Best practice generali
La programmazione è un campo creativo e la creazione di app per Android non fa eccezione. Esistono molti modi per risolvere un problema. Puoi comunicare dati tra più attività o frammenti, recuperare dati remoti e renderli persistenti in locale per la modalità offline o gestire un numero qualsiasi di altri scenari comuni che le app non banali incontrano.
Sebbene i seguenti consigli non siano obbligatori, nella maggior parte dei casi seguirli rende la tua base di codice più solida, testabile e gestibile nel lungo periodo:
Non archiviare dati nei componenti dell'app.
Evita di designare i punti di ingresso della tua app, come attività, servizi e broadcast receiver, come origini dati. Devono invece coordinarsi solo con altri componenti per recuperare il sottoinsieme di dati pertinente a quel punto di ingresso. Ogni componente dell'app ha una durata piuttosto breve, a seconda dell'interazione dell'utente con il dispositivo e dello stato di salute attuale complessivo del sistema.
Riduci le dipendenze dalle classi Android.
I componenti dell'app devono essere le uniche classi che si basano sulle API dell'SDK del framework Android, ad esempio Context
o Toast
. L'astrazione di altre classi nella tua
app aiuta a migliorare la testabilità e riduce
l'accoppiamento
all'interno dell'app.
Definisci confini di responsabilità chiari tra i moduli della tua app.
Ad esempio, non distribuire il codice che carica i dati dalla rete su più classi o pacchetti nel codebase. Allo stesso modo, non definire più responsabilità non correlate, come la memorizzazione nella cache dei dati e il data binding, nella stessa classe. Seguire l'architettura dell'app consigliata ti aiuterà in questo.
Mostra il minor numero possibile di informazioni di ogni modulo.
Ad esempio, non creare una scorciatoia che esponga un dettaglio di implementazione interno di un modulo. Potresti guadagnare un po' di tempo nel breve periodo, ma è probabile che tu debba affrontare un debito tecnico molte volte superiore man mano che la tua codebase si evolve.
Concentrati sul nucleo unico della tua app in modo che si distingua dalle altre.
Non reinventare la ruota scrivendo lo stesso codice boilerplate più e più volte. Concentra invece il tuo tempo e le tue energie su ciò che rende unica la tua app e lascia che le librerie Jetpack e altre librerie consigliate gestiscano il boilerplate ripetitivo.
Utilizza layout canonici e pattern di progettazione delle app.
Le librerie Jetpack Compose forniscono API robuste per la creazione di interfacce utente adattive. Utilizza i layout canonici nella tua app per migliorare l'esperienza utente su più fattori di forma e dimensioni di visualizzazione. Consulta la galleria di pattern di progettazione delle app per selezionare i layout più adatti ai tuoi casi d'uso.
Valuta come rendere testabile ogni parte della tua app in modo isolato.
Ad esempio, avere un'API ben definita per recuperare i dati dalla rete semplifica il test del modulo che li salva in un database locale. Se invece combini la logica di questi due moduli in un unico punto o distribuisci il codice di rete in tutto il codebase, diventa molto più difficile, se non impossibile, eseguire test efficaci.
I tipi sono responsabili della propria policy di concorrenza.
Se un tipo esegue un lavoro di blocco a lunga esecuzione, deve essere responsabile dello spostamento del calcolo nel thread corretto. Questo tipo specifico conosce il tipo di calcolo che sta eseguendo e in quale thread deve essere eseguito. I tipi devono essere sicuri per il thread principale, il che significa che possono essere chiamati dal thread principale senza bloccarlo.
Conserva il maggior numero possibile di dati pertinenti e aggiornati.
In questo modo, gli utenti possono usufruire delle funzionalità della tua app anche quando il dispositivo è in modalità offline. Ricorda che non tutti i tuoi utenti dispongono di una connettività costante e ad alta velocità e, anche se così fosse, potrebbero avere una ricezione scarsa in luoghi affollati.
Vantaggi dell'architettura
L'implementazione di una buona architettura nella tua app offre molti vantaggi ai team di ingegneria e di progetto:
- Migliora la manutenibilità, la qualità e la robustezza dell'app nel suo complesso.
- Consente all'app di scalare. Più persone e più team possono contribuire allo stesso codebase con conflitti di codice minimi.
- Aiuta con l'onboarding. Poiché l'architettura porta coerenza al tuo progetto, i nuovi membri del team possono mettersi rapidamente al passo ed essere più efficienti in meno tempo.
- È più facile da testare. Una buona architettura incoraggia tipi più semplici che sono generalmente più facili da testare.
- I bug possono essere esaminati in modo metodico con processi ben definiti.
Investire nell'architettura ha anche un impatto diretto sugli utenti. beneficiano di un'applicazione più stabile e di più funzionalità grazie a un team di ingegneri più produttivo. Tuttavia, l'architettura richiede anche un investimento di tempo iniziale. Per aiutarti a giustificare questo tempo al resto della tua azienda, dai un'occhiata a questi case study in cui altre aziende condividono le loro storie di successo quando hanno un'architettura valida nella loro app.
Campioni
I seguenti esempi di Google mostrano una buona architettura dell'app. Esplorali per vedere queste indicazioni in pratica:
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Livello dati
- Livello UI
- Eventi UI