Guida all'architettura delle app

Questa guida descrive le best practice e l'architettura consigliata per creare app solide e di alta qualità.

Esperienze utente nelle app mobile

Una tipica app Android contiene più componenti dell'app, tra cui attività, frammenti, servizi, fornitori di contenuti e ricevitori di trasmissione. La maggior parte di questi componenti dell'app deve essere dichiarata nel file manifest dell'app. Il sistema operativo Android utilizza quindi questo file per decidere come integrare la tua app nell'esperienza utente complessiva del dispositivo. Dato che una tipica app 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 dall'utente.

Tieni presente che anche i dispositivi mobili hanno risorse limitate, 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 avviati singolarmente e non in ordine e il sistema operativo o l'utente possono distruggerli in qualsiasi momento. Poiché questi eventi non sono sotto il tuo controllo, non devi archiviare o mantenere in memoria alcun dato o stato dell'applicazione nei componenti dell'app e i componenti dell'app non devono dipendere l'uno dall'altro.

Principi di architettura comuni

Se non devi usare i componenti dell'app per archiviare i dati e lo stato dell'app, come dovresti progettare l'app?

Man mano che le dimensioni delle app Android aumentano, è importante definire un'architettura che consenta la scalabilità dell'app, ne aumenti la solidità e ne faciliti i test.

L'architettura di un'app definisce i confini tra le parti dell'app e le responsabilità che ogni parte dovrebbe avere. Per soddisfare le esigenze sopra indicate, devi progettare l'architettura dell'app seguendo alcuni principi specifici.

Separazione dei problemi

Il principio più importante da seguire è la separazione delle preoccupazioni. È un errore comune scrivere tutto il codice in un Activity o un Fragment. Queste classi basate sull'interfaccia utente devono contenere solo la logica che gestisce le interazioni dell'interfaccia utente e del sistema operativo. Mantenendo queste classi il più snelle possibili, puoi evitare molti problemi relativi al ciclo di vita dei componenti e migliorare la testabilità di queste classi.

Tieni presente che non possiedi le implementazioni di Activity e Fragment, ma 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 come 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 questi dispositivi.

UI di Drive da modelli dei dati

Un altro principio importante è che è necessario utilizzare modelli di dati, preferibilmente modelli permanenti, per l'interfaccia utente. I modelli di dati rappresentano i dati di un'app. Sono indipendenti dagli elementi UI e da altri componenti dell'app. Ciò significa che non sono vincolati al ciclo di vita dei componenti dell'app e dell'interfaccia utente, ma vengono comunque eliminati quando il sistema operativo decide di rimuovere il processo dell'app dalla memoria.

I modelli permanenti sono ideali per i seguenti motivi:

  • I tuoi utenti non perderanno dati se il sistema operativo Android elimina l'app per liberare risorse.

  • L'app continua a funzionare nei casi in cui la connessione di rete sia instabile o non disponibile.

Se basi l'architettura dell'app su classi di modelli dei dati, rendi l'app più testabile e solida.

Un'unica fonte attendibile

Quando nell'app viene definito un nuovo tipo di dati, devi assegnargli un'unica origine di verità (SSOT). L'SSOT è il proprietario dei dati e solo l'SSOT può modificarli o modificarli. Per ottenere questo risultato, 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 diversi vantaggi:

  • Centralizza in un unico posto tutte le modifiche apportate a un determinato tipo di dati.
  • Protegge i dati in modo che altri tipi non possano manometterli.
  • Ciò rende più tracciabili le modifiche ai dati. Di conseguenza, i bug sono più facili da individuare.

In un'applicazione offline, la fonte attendibile per i dati dell'applicazione è in genere un database. In altri casi, la fonte attendibile può essere un ViewModel o persino l'UI.

Flusso di dati unidirezionale

Il principio della singola fonte di dati viene spesso utilizzato nelle nostre guide con il pattern Unidirectional Data Flow (UDF). In UDF, state scorre in una sola direzione. Gli eventi che modificano il flusso di dati nella direzione opposta.

In Android, lo stato o i dati di solito passano dai tipi della gerarchia con ambito più elevato a quelli con ambito più basso. 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 solitamente passano dalle origini dati all'interfaccia utente. Gli eventi utente, come le pressioni dei pulsanti, passano dall'interfaccia utente all'SSOT, in cui i dati dell'applicazione vengono modificati ed esposti in un tipo immutabile.

Questo pattern garantisce meglio la coerenza dei dati, è meno soggetto a errori, è più facile da eseguire il debug e offre tutti i vantaggi del pattern SSOT.

Questa sezione illustra come strutturare l'app seguendo le best practice consigliate.

Considerando i principi di architettura 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 dominio per semplificare e riutilizzare le interazioni tra l'interfaccia utente e i livelli dati.

In una tipica architettura di app, il livello UI riceve i dati dell'applicazione dal livello dati o dal livello di dominio facoltativo, che si trova tra il livello UI e il livello dati.
Figura 1. Diagramma di un'architettura tipica di un'app.

Architettura di app moderne

Questa architettura moderna delle app incoraggia, tra le altre cose, a utilizzare le seguenti tecniche:

  • Un'architettura reattiva e a più livelli.
  • Flusso di dati unidirezionale in tutti i livelli dell'app.
  • Un livello UI con contenitori di stato per gestire la complessità dell'interfaccia utente.
  • Coroutine e flussi.
  • Best practice per l'inserimento delle dipendenze.

Per ulteriori informazioni, consulta le sezioni seguenti, le altre pagine Architettura nel sommario e la pagina dei suggerimenti che contiene un riepilogo delle best practice più importanti.

Livello UI

Il ruolo del livello dell'interfaccia utente (o livello di presentazione) è 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 è costituito da due elementi:

  • Elementi dell'interfaccia utente che mostrano i dati sullo schermo. Per creare questi elementi, puoi utilizzare le funzioni Views o Jetpack Compose.
  • Contenitori di stato (come le classi ViewModel) che contengono i dati, li espongono all'interfaccia utente e gestiscono la logica.
In una tipica architettura, gli elementi UI del livello UI dipendono dai titolari di stato, che a loro volta dipendono dalle classi del livello dati o del livello di dominio facoltativo.
Figura 2. Il ruolo del livello UI nell'architettura dell'app.

Per ulteriori informazioni 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: è composta da regole che determinano il modo in cui l'app crea, archivia e modifica i dati.

Il livello dati è costituito da repository che ciascuno può contenere da zero a molte origini dati. Devi creare una classe di repository per ogni tipo diverso di dati gestiti nell'app. Ad esempio, potresti creare una classe MoviesRepository per i dati relativi ai film o una classe PaymentsRepository per i dati relativi ai pagamenti.

In un'architettura tipica, i repository del livello dati forniscono dati al resto dell'app e dipendono dalle origini dati.
Figura 3. Il ruolo del livello dati nell'architettura dell'app.

Le classi di repository sono responsabili delle seguenti attività:

  • Esposizione dei dati al resto dell'app.
  • Centralizzazione delle modifiche ai dati.
  • Risoluzione dei conflitti tra più origini dati.
  • Sottrazione di origini di dati dal resto dell'app.
  • Contiene la logica di business.

Ogni classe di origine dati deve 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 ulteriori informazioni su questo livello, consulta la pagina Livello dati.

Livello dominio

Il livello dominio è un livello facoltativo che si trova tra i livelli dati e UI.

Il livello dominio è responsabile dell'incapsulamento di una logica di business complessa, o semplice, di business riutilizzata da più ViewModel. Questo livello è facoltativo perché non tutte le app avranno questi requisiti. Dovresti usarlo solo quando necessario, ad esempio per gestire la complessità o favorire la riusabilità.

Se incluso, il livello di dominio facoltativo fornisce dipendenze al livello UI e dipende dal livello dati.
Figura 4. Il ruolo del livello di dominio nell'architettura dell'app.

Le classi in questo livello vengono 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 utilizzano i fusi orari per mostrare il messaggio corretto sullo schermo.

Per ulteriori informazioni su questo livello, vedi la pagina del livello del dominio.

Gestisci 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 crearle. In fase di runtime, un'altra classe è responsabile di fornire queste dipendenze.
  • Localizzatore servizio: il pattern di localizzazione dei servizi fornisce un registry in cui le classi possono ottenere le proprie dipendenze invece di crearle.

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 consentono di passare rapidamente dall'implementazione di test a quella di produzione e viceversa.

Ti consigliamo di seguire i pattern di inserimento delle dipendenze e utilizzare la libreria Hilt nelle app per Android. Hilt crea automaticamente gli oggetti camminando nell'albero delle dipendenze, fornisce garanzie in tempo di compilazione sulle dipendenze e crea container di dipendenze per le classi di framework Android.

Best practice generali

La programmazione è un campo creativo e lo sviluppo di app Android non è un'eccezione. Esistono molti modi per risolvere un problema: puoi comunicare dati tra più attività o frammenti, recuperare i dati remoti e conservarli localmente per la modalità offline o gestire una serie di altri scenari comuni riscontrati dalle app non banali.

Anche se i seguenti suggerimenti non sono obbligatori, nella maggior parte dei casi seguirli rende il tuo codebase più solido, testabile e gestibile a lungo termine:

Non archiviare i dati nei componenti dell'app.

Evita di designare i punti di ingresso dell'app, ad esempio attività, servizi e ricevitori di trasmissione, come origini di dati. Devono invece coordinarsi con altri componenti solo per recuperare il sottoinsieme di dati pertinente per quel punto di ingresso. Ogni componente dell'app ha una durata piuttosto breve, a seconda dell'interazione dell'utente con il dispositivo e dell'integrità complessiva del sistema.

Riduci le dipendenze per le classi Android.

I componenti dell'app dovrebbero essere le uniche classi che si basano sulle API dell'SDK del framework Android, come Context o Toast. Astrarre da queste classi di altre classi dell'app favorisce la testbilità e riduce l'accoppiamento all'interno dell'app.

Crea limiti di responsabilità ben definiti tra i vari moduli nella tua app.

Ad esempio, non distribuire il codice che carica i dati dalla rete tra più classi o pacchetti nel tuo codebase. Allo stesso modo, non definire più responsabilità non correlate, come la memorizzazione nella cache dei dati e l'associazione di dati, nella stessa classe. Seguire l'architettura consigliata dell'app può essere utile.

Fornisci il minor numero possibile di informazioni da ogni modulo.

Ad esempio, non cedere alla tentazione di creare una scorciatoia che espone un dettaglio di implementazione interno da un modulo. Potresti guadagnare un po' di tempo nel breve termine, ma potresti incorrere in un debito tecnico molte volte man mano che il tuo codebase si evolve.

Concentrati sull'elemento fondamentale della tua app, in modo che si distingua dalle altre.

Non reinventare la ruota scrivendo lo stesso codice boilerplate più volte. Concentrati invece su ciò che rende unica la tua app e lascia che le librerie Jetpack e altre librerie consigliate gestiscano il boilerplate ripetitivo.

Pensa a come rendere ogni parte della tua app testabile in modo isolato.

Ad esempio, avere un'API ben definita per il recupero dei dati dalla rete semplifica il test del modulo che riporta i dati in un database locale. Se invece mischi la logica di questi due moduli in un unico posto o distribuisci il codice di networking nell'intero codebase, diventa molto più difficile, se non impossibile, eseguire test efficaci.

I tipi sono responsabili dei loro criteri di contemporaneità.

Se un tipo esegue operazioni di blocco a lunga esecuzione, dovrebbe essere responsabile dello spostamento del calcolo nel thread corretto. Quel tipo specifico conosce il tipo di calcolo che esegue e in quale thread deve essere eseguito. I tipi devono essere sicuri per l'ambiente principale, ovvero poter chiamare in sicurezza dal thread principale senza bloccarlo.

Fornisci quanti più dati pertinenti e aggiornati possibile.

In questo modo gli utenti possono sfruttare le funzionalità della tua app anche quando il dispositivo è in modalità offline. Ricorda che non tutti i tuoi utenti godono di una connettività costante e ad alta velocità e, anche se preferiscono, la ricezione in luoghi affollati è scarsa.

Vantaggi dell'architettura

L'implementazione di una buona architettura nella tua app offre molti vantaggi ai team di progetto e tecnici:

  • Migliora la manutenibilità, la qualità e la robustezza dell'app complessiva.
  • Consente la scalabilità dell'app. Più persone e team possono contribuire allo stesso codebase con il minimo conflitto di codice.
  • Agevola l'onboarding. Man mano che l'architettura apporta coerenza al tuo progetto, i nuovi membri del team possono aggiornarsi rapidamente ed essere più efficienti in meno tempo.
  • È più facile da testare. Una buona architettura incoraggia tipi più semplici da testare.
  • I bug possono essere esaminati metodicamente con processi ben definiti.

Investire nell'architettura ha anche un impatto diretto sui tuoi utenti. traggono vantaggio da un'applicazione più stabile e da più funzionalità grazie a un team di progettazione più produttivo. Tuttavia, l'architettura richiede anche un investimento di tempo iniziale. Per giustificare questa situazione al resto della tua azienda, dai un'occhiata a questi case study in cui altre aziende condividono i propri casi di successo quando utilizzano una buona architettura nella propria app.

Samples

I seguenti esempi di Google mostrano una buona architettura dell'app. Esplorale per vedere concretamente queste indicazioni: