Questa guida contiene le best practice e un'architettura consigliata per la creazione di app solide e di alta qualità.
Esperienze utente delle app mobile
Un'app Android tipica contiene diversi componenti, tra cui attività, frammenti, servizi, provider di contenuti e ricevitori di trasmissione. Dichiari la maggior parte di questi componenti dell'app nel file manifest dell'app. Il sistema operativo Android utilizza quindi questo file per decidere come integrare l'app nell'esperienza utente complessiva del dispositivo. Poiché una tipica app per Android potrebbe contenere più componenti e che 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à gestiti dagli utenti.
Tieni presente che i dispositivi mobili sono anche vincolati alle risorse, pertanto in qualsiasi momento il sistema operativo potrebbe terminare alcuni processi dell'app per fare spazio a quelli nuovi.
Date le condizioni di questo ambiente, i componenti dell'app possono essere lanciati individualmente e non in ordine e il sistema operativo o l'utente possono eliminarli in qualsiasi momento. Questi eventi non sono sotto il tuo controllo e non devi archiviare o conservare in memoria dati o stato delle applicazioni nei componenti dell'app. Inoltre, i componenti dell'app non devono dipendere l'uno dall'altro.
Principi di architettura comuni
Se non dovresti utilizzare i componenti dell'app per archiviare dati e stato delle applicazioni, come dovresti progettare la tua app?
Man mano che le app Android crescono, è importante definire un'architettura che consenta all'app di scalare, aumenta la robustezza e rende più facile il test.
Un'architettura dell'app definisce i confini tra le parti dell'app e le responsabilità di ciascuna. Per soddisfare le esigenze sopra menzionate, devi progettare la tua architettura dell'app per seguire alcuni principi specifici.
Separazione dei problemi
Il principio più importante da seguire è la separazione delle preoccupazioni.
È un errore comune scrivere tutto il codice in
Activity
o
Fragment
. Queste classi basate sull'interfaccia utente devono contenere solo la logica che gestisce le interazioni UI e sistema operativo. Mantenendole
il più snelle possibili, puoi evitare molti problemi relativi al ciclo di vita dei
componenti e migliorarne la verificabilità.
Tieni presente che non possiedi le implementazioni Activity
e Fragment
; piuttosto, si tratta solo di classi di colla 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 del sistema quali la memoria insufficiente. Per offrire un'esperienza utente soddisfacente e un'esperienza di manutenzione delle app più gestibile, è meglio ridurre al minimo la dipendenza da tali dispositivi.
UI di Drive dai modelli di dati
Un altro principio importante è quello di indirizzare l'UI dai modelli di dati, preferibilmente modelli permanenti. I modelli dei dati rappresentano i dati di un'app. Sono indipendenti dagli elementi UI e da altri componenti della tua app. Ciò significa che non sono legati al ciclo di vita della UI 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:
Gli utenti non perdono dati se il sistema operativo Android distrugge la tua app per liberare risorse.
L'app continua a funzionare nei casi in cui la connessione di rete sia irregolare o non disponibile.
Se basi l'architettura dell'app su classi di modelli di dati, rendi la tua app più testabile e robusta.
Un'unica fonte attendibile
Quando nella tua app viene definito un nuovo tipo di dati, devi assegnargli un'unica fonte attendibile (SSOT). Il servizio SSOT è il proprietario dei dati e solo il servizio SSOT può modificarli o mutarli. A questo scopo, il servizio SSOT espone i dati utilizzando un tipo immutabile e, per modificarli, espone funzioni o riceve eventi che altri tipi possono chiamare.
Questo pattern offre diversi vantaggi:
- Centralizza in un'unica posizione tutte le modifiche apportate a un determinato tipo di dati.
- Protegge i dati in modo che altri tipi non possano manometterli.
- Apporta modifiche più precise ai dati. In questo modo, i bug sono più facili da individuare.
In un'applicazione incentrata sull'offline, la fonte attendibile per i dati dell'applicazione è in genere un database. In altri casi, la fonte di riferimento può essere un oggetto ViewModel o anche l'interfaccia utente.
Flusso di dati unidirezionale
Il principio di singola fonte di riferimento è spesso utilizzato nelle nostre guide con il pattern del flusso di dati unidirezionale (UDF). In UDF, lo stato fluisce in una sola direzione. Gli eventi che modificano il flusso di dati nella direzione opposta.
In Android, lo stato o i dati in genere si spostano dai tipi con ambito più gerarchico a quelli con ambito inferiore. In genere gli eventi vengono attivati dai tipi con ambito inferiore finché non raggiungono l'SSOT per il tipo di dati corrispondente. Ad esempio, i dati dell'applicazione in genere confluiscono dalle origini dati all'interfaccia utente. Eventi utente, come i pulsanti, passano dall'interfaccia utente al servizio SSOT, dove i dati dell'applicazione vengono modificati ed esposti in un tipo immutabile.
Questo pattern garantisce una maggiore coerenza dei dati, è meno soggetto a errori, è più facile da sottoporre a debug e offre tutti i vantaggi del pattern SSOT.
Architettura consigliata per l'app
Questa sezione mostra come strutturare l'app seguendo le best practice consigliate.
Considerando i principi architettonici comuni menzionati nella sezione precedente, ogni applicazione deve avere almeno due livelli:
- Il livello UI che mostra i dati dell'applicazione sullo schermo.
- Il livello dati contenente 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 l'interfaccia utente e i livelli di dati.

Architettura moderna delle app
Questa architettura per app moderna invita a utilizzare, tra le altre, le seguenti tecniche:
- Un'architettura reattiva e a più livelli.
- Flusso di dati unidirezionale (UDF) in tutti i livelli dell'app.
- Un livello UI con contenitore di stati per gestire la complessità della UI.
- Coroutine e flussi.
- Best practice per l'inserimento di dipendenze.
Per ulteriori informazioni, consulta le sezioni seguenti, le altre pagine dell'architettura nel sommario e la pagina Consigli che contiene un riepilogo delle best practice più importanti.
Livello UI
Il ruolo del livello UI (o livello di presentazione) è la visualizzazione dei dati dell'applicazione sullo schermo. Ogni volta che i dati cambiano, a causa dell'interazione dell'utente (come la pressione di un pulsante) o dell'input esterno (come una risposta di rete), l'interfaccia utente dovrebbe aggiornarsi per riflettere le modifiche.
Il livello UI è costituito da due elementi:
- Elementi dell'interfaccia utente che consentono di visualizzare i dati sullo schermo. Per creare questi elementi, utilizzi le funzioni View o Jetpack Compose.
- I titolari di stato (come le classi ViewModel) che contengono i dati, li espongono all'interfaccia utente e gestiscono la logica.

Per scoprire di più su questo livello, consulta la pagina del 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: si tratta di regole che determinano il modo in cui l'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 di repository per ogni tipo di dati gestito 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.

I corsi del repository sono responsabili delle seguenti attività:
- Esporre i dati al resto dell'app.
- Centralizzazione delle modifiche ai dati.
- Risolvere i conflitti tra più origini dati.
- Fonti di dati astratte dal resto dell'app.
- Contiene logica di business.
Ogni classe di origine dati dovrebbe avere la responsabilità di lavorare con una sola origine di dati, che può essere un file, un'origine di rete o un database locale. Le classi di origini dati sono il ponte tra l'applicazione e il sistema per le operazioni sui dati.
Per scoprire di più su questo livello, consulta la pagina del livello dati.
Livello di dominio
Il livello di dominio è un livello facoltativo che si trova tra i livelli UI e dati.
Il livello dominio è responsabile dell'incapsulamento di una logica di business complessa o di una semplice logica di business che viene riutilizzata da più ViewModels. Questo livello è facoltativo perché non tutte le app avranno questi requisiti. Utilizzalo solo quando necessario, ad esempio per gestire la complessità o favorire il riutilizzo.

Le classi in questo livello sono comunemente chiamate casi d'uso o interruttori. Ogni caso d'uso deve essere responsabile di una singola funzionalità. Ad esempio, l'app potrebbe avere una classe GetTimeZoneUseCase
se più ViewModels si basano sui fusi orari per visualizzare il messaggio corretto sullo schermo.
Per ulteriori informazioni su questo livello, consulta la pagina del livello di dominio.
Gestire le dipendenze tra i componenti
Per funzionare correttamente, le classi della tua app dipendono da altre classi. Puoi utilizzare uno dei seguenti pattern di progettazione per raccogliere le dipendenze di una classe specifica:
- Iniezione di dipendenza (DI): l'iniezione di dipendenza consente alle classi di definire le dipendenze senza costruirle. In fase di esecuzione, un'altra classe è responsabile di fornire queste dipendenze.
- Service locator: il pattern del Service locator fornisce un registro in cui le classi possono ottenere le dipendenze anziché crearle.
Questi pattern consentono di scalare il codice perché offrono pattern chiari per la gestione delle dipendenze senza duplicare il codice o aggiungere complessità. Inoltre, questi pattern consentono di passare rapidamente dall'implementazione di test all'implementazione di produzione.
Ti consigliamo di seguire i pattern di iniezione di dipendenza e di utilizzare la libreria Hilt nelle app per Android. Hilt crea automaticamente oggetti a livello di albero delle dipendenze, fornisce garanzie di tempo di esecuzione sulle dipendenze e crea container di dipendenza per le classi di framework Android.
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: potresti comunicare dati tra più attività o frammenti, recuperare i dati remoti e mantenerli in locale per la modalità offline oppure gestire un numero qualsiasi di altri scenari comuni riscontrati dalle app non essenziali.
Anche se i seguenti consigli non sono obbligatori, nella maggior parte dei casi li rendono il tuo codebase più robusto, testabile e gestibile nel lungo modo:
Non archiviare i dati nei componenti dell'app.
Evita di designare i punti di ingresso della tua app, ad esempio attività, servizi e ricevitori di trasmissione, come origini di dati. Devono invece coordinarsi solo con altri componenti per recuperare il sottoinsieme di dati pertinente al punto di ingresso. Ogni componente dell'app ha una vita breve, a seconda dell'interazione dell'utente con il dispositivo e dell'integrità complessiva del sistema.
Riduci le dipendenze dai corsi Android.
I componenti dell'app devono essere le uniche classi che si basano sulle API SDK del framework Android come Context
o Toast
. L'astrazione di altre classi nell'app contribuisce a favorire la verificabilità e riduce l'accoppiamento all'interno dell'app.
Crea limiti di responsabilità ben definiti tra i vari moduli dell'app.
Ad esempio, non distribuire il codice che carica i dati dalla rete tra più classi o pacchetti nel tuo codebase. Analogamente, non definire più responsabilità non correlate, come memorizzazione nella cache e associazione di dati, nella stessa classe. A questo scopo, ti consigliamo di attenerti all'architettura delle app consigliata.
Esponi il meno possibile da ciascun modulo.
Ad esempio, non tentare di creare una scorciatoia che mostri un dettaglio di implementazione interno da un modulo. Potresti ottenere un po' di tempo nel breve periodo, ma è probabile che tu possa incorrere in debiti tecnici molte volte man mano che il codebase evolve.
Concentrarsi sull'unicità della tua app in modo che si distingua dalle altre app.
Non reinventare la ruota scrivendo sempre lo stesso codice boilerplate. Concentrati sul tuo tempo e sulle tue energie su ciò che rende unica la tua app e lascia che le librerie Jepack e altre librerie consigliate gestiscano la replica ripetitiva.
Valuta come rendere ogni parte della tua app verificabile separatamente.
Ad esempio, un'API ben definita per il recupero dei dati dalla rete semplifica il test del modulo che memorizza i dati in un database locale. Se, invece, combini la logica di questi due moduli in un unico posto o distribuisci il codice di networking all'intero codebase, diventa più difficile, se non impossibile, testare in modo efficace.
I tipi sono responsabili delle rispettive norme di contemporaneità.
Se un tipo esegue operazioni di blocco a lunga esecuzione, dovrebbe essere responsabile spostare quel calcolo nel thread giusto. Tale tipo specifico conosce il tipo di calcolo effettuato e in quale thread deve essere eseguito. I tipi devono essere sicuri per il canale principale, il che significa che possono essere chiamati in sicurezza dal thread principale senza bloccarlo.
Mantieni i dati il più possibile pertinenti e aggiornati.
In questo modo, gli utenti possono sfruttare la funzionalità della tua app anche quando il dispositivo è in modalità offline. Ricorda che non tutti gli utenti usufruiscono di una connettività costante ad alta velocità e, anche se ci riescono, possono avere una cattiva ricezione in luoghi affollati.
Vantaggi dell'architettura
Una buona architettura implementata nella tua app porta molti vantaggi ai team di progetto e progettazione:
- 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 minimi del codice.
- Funziona con l'onboarding. Man mano che l'architettura crea coerenza nel tuo progetto, i nuovi membri del team possono diventare rapidamente operativi ed essere più efficienti in meno tempo.
- È più facile testarla. Una buona architettura incoraggia tipi più semplici, che in genere sono più facili da testare.
- I bug possono essere esaminati metodicamente con processi ben definiti.
Inoltre, investire nell'architettura ha un impatto diretto sui tuoi utenti. Beneficiano di un'applicazione più stabile e di più funzionalità grazie a un team di tecnici più produttivo. Tuttavia, l'architettura richiede anche un investimento iniziale in termini di tempo. Per aiutarvi a giustificare questo tempo al resto dell'azienda, date un'occhiata a questi case study in cui altre aziende condividono i loro casi di successo quando hanno una buona architettura nella loro app.
Samples
I seguenti esempi di Google dimostrano una buona architettura delle app. Scoprile in pratica:
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript non è attivo
- Livello dati
- Livello UI
- Eventi UI