L'architettura dell'app è la base di un'app per Android di alta qualità. Un'architettura ben definita ti consente di creare un'app scalabile e gestibile che può adattarsi all'ecosistema in continua espansione dei dispositivi Android, inclusi smartphone, tablet, dispositivi pieghevoli, dispositivi ChromeOS, display per auto e XR.
Composizione dell'app
Una tipica app per Android è composta da più componenti dell'app, come servizi, provider di contenuti e broadcast receiver. Dichiara questi componenti nel tuo file manifest dell'app.
Anche l'interfaccia utente di un'app è un componente. In passato, le UI venivano create
utilizzando più attività. Tuttavia, le app moderne utilizzano un'architettura a singola attività. Una singola Activity funge da contenitore per le schermate o le destinazioni Jetpack Compose.
Diversi fattori di forma
Le app possono essere eseguite su più fattori di forma, inclusi non solo smartphone, ma anche tablet, dispositivi pieghevoli, dispositivi ChromeOS e altro ancora. Non dare per scontato che la tua app rimanga sempre fissa in orientamento verticale o orizzontale. Le modifiche alla configurazione, come la rotazione del dispositivo o la piegatura e l'apertura di un dispositivo pieghevole, forzano la ricomposizione dell'UI dell'app, il che influisce sullo stato dell'app.
Vincoli delle risorse
I dispositivi mobili, anche quelli con schermi di grandi dimensioni, sono vincolati dalle risorse, pertanto in qualsiasi momento il sistema operativo potrebbe arrestare il processo dell'app per assegnare le risorse ad altri processi.
Condizioni di avvio variabili
In un ambiente con risorse limitate, i componenti dell'app possono essere avviati singolarmente e in ordine casuale; inoltre, il sistema operativo o l'utente possono eliminarli in qualsiasi momento. Di conseguenza, non archiviare dati o stati dell'applicazione nei componenti dell'app. Rendi i componenti dell'app autonomi e indipendenti l'uno dall'altro.
Principi architettonici comuni
Se non puoi utilizzare i componenti dell'app per archiviare i dati e lo stato dell'applicazione, come devi progettare l'app?
Man mano che le app Android aumentano di dimensioni, è importante definire un'architettura che consenta all'app di scalare. Un'architettura dell'app ben progettata definisce i limiti tra le parti dell'app e le responsabilità di ciascuna parte.
Separazione delle responsabilità
Progetta l'architettura dell'app in modo che segua alcuni principi specifici.
Il principio più importante è la separazione delle responsabilità: separare l'app in metodi, classi, file, pacchetti, moduli e livelli con responsabilità e limiti chiaramente definiti.
È un errore comune scrivere tutto il codice in un Activity.
Il ruolo principale di un Activity è ospitare l'UI dell'app. Il sistema operativo Android ne controlla il ciclo di vita, spesso distruggendole e ricreandole in risposta ad azioni dell'utente come la rotazione dello schermo o eventi di sistema come la memoria insufficiente.
Questa natura effimera le rende inadatte a contenere dati o stati dell'applicazione. Se memorizzi i dati in un Activity, questi vengono persi quando
il componente viene ricreato. Per garantire la persistenza dei dati e fornire un'esperienza utente stabile, non affidare lo stato a questi componenti dell'UI.
Layout adattivi
Crea app che gestiscano correttamente le modifiche alla configurazione, come le modifiche all'orientamento del dispositivo o alle dimensioni della finestra dell'app. Implementa i layout canonici adattivi per offrire un'esperienza utente ottimale su una varietà di fattori di forma.
UI basata su modelli di dati
Un altro principio importante è basare l'UI sui modelli di dati, preferibilmente modelli persistenti. I modelli di dati rappresentano i dati di un'app. Sono indipendenti dagli elementi dell'UI e da altri componenti dell'app. Ciò significa che non sono vincolati al ciclo di vita dell'UI e dei componenti dell'app, ma verranno comunque eliminati quando il sistema operativo rimuove il processo dell'app dalla memoria.
I modelli persistenti sono ideali per i seguenti motivi:
Gli utenti non perdono i dati se il sistema operativo Android elimina l'app per liberare risorse.
L'app continua a funzionare nei casi in cui una connessione di rete è intermittente o non disponibile.
Basa l'architettura dell'app sulle classi dei modelli di dati per renderla robusta e testabile.
Unica fonte di verità
Quando definisci un nuovo tipo di dati nella tua app, assegnagli un'unica fonte di verità (SSOT). La SSOT è il proprietario di questi dati e solo la SSOT può modificarli o mutarli. Per ottenere questo risultato, la SSOT espone i dati utilizzando un tipo immutabile; per modificare i dati, la SSOT espone funzioni o riceve eventi che altri tipi possono chiamare.
Questo pattern offre diversi vantaggi:
- Centralizza tutte le modifiche a un particolare tipo di dati in un'unica posizione.
- Protegge i dati in modo che altri tipi non possano manometterli.
- Rende più tracciabili le modifiche ai dati, in modo che sia 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 verità può essere un
ViewModel.
Flusso di dati unidirezionale
Il principio dell'unica fonte di verità viene spesso utilizzato con il pattern del flusso di dati unidirezionale (UDF). Nell'UDF, lo stato scorre in una sola direzione, in genere dal componente principale al componente secondario. Gli eventi che modificano il flusso di dati scorrono nella direzione opposta.
In Android, lo stato o i dati in genere scorrono dai tipi con ambito più elevato della gerarchia a quelli con ambito inferiore. 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 in genere scorrono dalle origini dati all'UI. Gli eventi utente, come le pressioni dei pulsanti, scorrono dall'UI alla 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.
Per saperne di più sull'UDF, consulta Flusso di dati unidirezionale in Jetpack Compose.
Architettura dell'app consigliata
Tenendo conto dei principi architettonici comuni, progetta ogni applicazione con almeno due livelli:
- Livello UI: mostra i dati dell'applicazione sullo schermo.
- Livello dati: contiene la logica di business dell'app ed espone i dati dell'applicazione.
Puoi aggiungere un livello aggiuntivo chiamato livello di dominio per semplificare e riutilizzare le interazioni tra i livelli UI e dati.
Architettura dell'app moderna
Un'architettura dell'app per Android moderna utilizza le seguenti tecniche (tra le altre):
- Architettura adattiva e a livelli.
- Flusso di dati unidirezionale (UDF) in tutti i livelli dell'app.
- Livello UI con contenitori di stato per gestire la complessità dell'UI.
- Coroutine e flussi.
- Best practice per l'iniezione delle dipendenze.
Per saperne di più, consulta Consigli per l'architettura Android.
Livello UI
Il ruolo del livello UI (o livello di presentazione) è mostrare 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 input esterni (ad esempio una risposta di rete), l'UI si aggiorna per riflettere le modifiche.
Il livello UI comprende due tipi di costrutti:
- Elementi dell'UI che eseguono il rendering dei dati sullo schermo. Crea questi elementi utilizzando le funzioni Jetpack Compose per supportare i layout adattivi.
- Contenitori di stato (ad esempio
ViewModel) che contengono i dati, li espongono all'UI e gestiscono la logica. I contenitori di stato devono avere la stessa durata dell'elemento dell'UI per cui forniscono lo stato. Ad esempio, un ViewModel per una schermata deve essere mantenuto in memoria finché la schermata non viene rimossa dallo stack di back navigation dell'app. Per saperne di più, consulta Durata degli stati.
Per le UI adattive, i contenitori di stato come gli oggetti ViewModel espongono lo stato dell'UI che
si adatta a diverse classi di dimensioni della finestra. Puoi utilizzare currentWindowAdaptiveInfo() per derivare questo stato dell'UI. I componenti come NavigationSuiteScaffold possono quindi utilizzare queste informazioni per passare automaticamente da un pattern di navigazione all'altro (ad esempio, NavigationBar, NavigationRail o NavigationDrawer) in base allo spazio disponibile sullo schermo.
Per saperne di più, consulta Livello UI e Architettura dell'UI di Compose.
Per saperne di più sulle app e sulla navigazione adattive, consulta Creare app adattive e Creare navigazione adattiva.
Livello dati
Il livello dati di un'app contiene la logica di business. La logica di business è ciò che dà valore alla tua app: comprende regole che determinano come l'app crea, archivia e modifica i dati.
Il livello dati è composto da repository, ognuno dei quali può contenere da zero a molte origini dati. Crea una classe repository per ogni tipo 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.
Le classi repository sono responsabili di:
- Esporre i dati al resto dell'app.
- Centralizzare le modifiche ai dati.
- Risolvere i conflitti tra più origini dati.
- Astrarre le origini dati dal resto dell'app.
- Contenere la logica di business.
Ogni classe di origine dati ha la responsabilità di lavorare con una sola origine dati, che può essere un file, un'origine di rete o un database locale. Le classi di origine dati sono il ponte tra l'applicazione e il sistema per le operazioni sui dati.
Per saperne di più, consulta la pagina del livello dati.
Strato di dominio
Il livello di dominio è un livello facoltativo tra i livelli UI e dati.
Il livello di dominio è responsabile dell'incapsulamento della logica di business complessa o della logica di business più semplice riutilizzata da più modelli di visualizzazione. Il livello di dominio è facoltativo perché non tutte le app hanno questi requisiti. Utilizzalo solo quando necessario, ad esempio per gestire la complessità o favorire la riusabilità.
Le classi nel livello di dominio sono comunemente chiamate casi d'uso o interattori.
Ogni caso d'uso ha la responsabilità di una singola funzionalità. Ad esempio, la tua app potrebbe avere una classe GetTimeZoneUseCase se più modelli di visualizzazione si basano sui fusi orari per mostrare il messaggio corretto sullo schermo.
Per saperne di più, consulta la pagina del livello di dominio.
Gestire le dipendenze tra i componenti
Le classi dell'app dipendono da altre classi per funzionare correttamente. Puoi utilizzare uno dei seguenti pattern di progettazione per raccogliere le dipendenze di una classe specifica:
- Iniezione delle dipendenze (DI): l'iniezione 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à. I pattern ti consentono anche di passare rapidamente dalle implementazioni di test a quelle di produzione.
Best practice generali
La programmazione è un campo creativo e la creazione di app Android non fa eccezione. Esistono molti modi per risolvere un problema: puoi comunicare i dati tra più Activity o Fragment, recuperare i dati remoti e renderli persistenti localmente 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 la loro applicazione rende la codebase più robusta, testabile e gestibile.
Non archiviare i dati nei componenti dell'app.
Evita di designare i punti di ingresso dell'app, come attività, servizi e broadcast receiver, come origini dati. Fai in modo che i punti di ingresso si coordinino con altri componenti per recuperare solo il sottoinsieme di dati pertinente a quel punto di ingresso. Ogni componente dell'app ha una durata breve, a seconda dell'interazione dell'utente con il dispositivo e della capacità del sistema.
Riduci le dipendenze dalle classi Android.
Fai in modo che i componenti dell'app siano le uniche classi che si basano sulle API SDK del framework Android
, come Context o Toast. L'astrazione di altre classi dell'app dai componenti dell'app facilita la testabilità e riduce
l'accoppiamento all'interno dell'app.
Definisci limiti di responsabilità chiari tra i moduli dell'app.
Non distribuire il codice che carica i dati dalla rete in più classi o pacchetti nella 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. Segui l'architettura dell'app consigliata.
Esporre il meno possibile da ogni modulo.
Non creare scorciatoie che espongano i dettagli di implementazione interni. Potresti guadagnare un po' di tempo a breve termine, ma è probabile che tu debba affrontare un debito tecnico molte volte superiore man mano che la codebase si evolve.
Concentrati sul nucleo unico della tua app in modo che si distingua dalle altre app.
Non reinventare la ruota scrivendo lo stesso codice boilerplate più e più volte. Concentra invece il tuo tempo e la tua energia su ciò che rende unica la tua app. 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 ottimizzare l'esperienza utente su più fattori di forma e dimensioni del display. Consulta la galleria di pattern di progettazione delle app per selezionare i layout più adatti ai tuoi casi d'uso.
Mantieni lo stato dell'UI durante le modifiche alla configurazione.
Quando progetti layout adattivi, mantieni lo stato dell'UI durante le modifiche alla configurazione, come il ridimensionamento del display, la piegatura e le modifiche all'orientamento. L'architettura deve verificare che lo stato attuale dell'utente venga mantenuto, offrendo un'esperienza ottimale.
Progetta componenti dell'UI riutilizzabili e componibili.
Crea componenti dell'UI riutilizzabili e componibili per supportare la progettazione adattiva. In questo modo, puoi combinare e riorganizzare i componenti per adattarli a varie dimensioni e posizioni dello schermo senza un refactoring significativo.
Valuta come rendere testabile in modo isolato ogni parte dell'app.
Un'API ben definita per recuperare i dati dalla rete facilita il test del modulo che rende persistenti questi dati in un database locale. Se invece combini la logica di queste due funzioni in un'unica posizione o distribuisci il codice di rete in tutta la codebase, il test diventa molto più difficile, se non impossibile.
I tipi sono responsabili della loro policy di concorrenza.
Se un tipo esegue un lavoro di blocco a lunga esecuzione, deve essere responsabile dello spostamento del calcolo nel thread corretto. Il tipo conosce il tipo di calcolo che sta eseguendo e in quale thread eseguire il calcolo. I tipi devono essere main-safe, il che significa che è sicuro chiamarli dal thread principale senza bloccarlo.
Rendi persistenti il maggior numero possibile di dati pertinenti e aggiornati.
In questo modo, gli utenti possono usufruire delle funzionalità dell'app anche quando il dispositivo è in modalità offline. Tieni presente che non tutti gli utenti usufruiscono di una connettività costante e ad alta velocità e, anche se lo fanno, possono 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 progetto e di ingegneria:
- Migliora la gestibilità, la qualità e la robustezza complessive dell'app.
- Consente all'app di scalare. Più persone e più team possono contribuire alla stessa 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 in genere sono più facili da testare.
- Consente di esaminare i bug in modo metodico con processi ben definiti.
Sebbene una buona architettura richieda un investimento di tempo iniziale, ha anche un impatto diretto sugli utenti. Questi ultimi usufruiscono di un'applicazione più stabile e di più funzionalità grazie a un team di ingegneria più produttivo.
Campioni
I seguenti esempi mostrano una buona architettura dell'app: