L'architettura dell'app è la base di un'applicazione 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, tra cui smartphone, tablet, 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 ricevitori di trasmissioni. Dichiari questi componenti nel manifest dell'app.
Anche l'interfaccia utente di un'app è un componente. Storicamente, le UI venivano create
utilizzando più attività. Tuttavia, le app moderne utilizzano un'architettura
a singola attività. Un singolo Activity funge da contenitore per
le schermate implementate come fragment o destinazioni Jetpack Compose.
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. Le modifiche alla configurazione, come la rotazione del dispositivo o l'apertura e la chiusura di un dispositivo pieghevole, forzano la ricomposizione della UI dell'app, il che influisce sui dati e sullo stato dell'app.
Vincoli delle risorse
I dispositivi mobili, anche quelli con schermi grandi, 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
In un ambiente con risorse limitate, i componenti dell'app possono essere avviati singolarmente e in modo non sequenziale. Inoltre, il sistema operativo o l'utente può distruggerli in qualsiasi momento. Di conseguenza, non memorizzare dati o stati dell'applicazione nei componenti dell'app. I componenti dell'app devono essere autonomi, 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 la tua app?
Man mano che le app per Android aumentano di dimensioni, è importante definire un'architettura che consenta all'app di scalare. Un'architettura dell'app ben progettata definisce i confini tra le parti dell'app e le responsabilità di ciascuna parte.
Separazione delle preoccupazioni
Progetta l'architettura dell'app in modo che segua alcuni principi specifici.
Il principio più importante è la separazione dei problemi. È
un errore comune scrivere tutto il codice in un Activity o in un
Fragment.
Il ruolo principale di un Activity o Fragment è ospitare la UI della tua app. Il sistema operativo Android ne controlla il ciclo di vita, distruggendoli e ricreandoli di frequente in risposta ad azioni dell'utente come la rotazione dello schermo o a eventi di sistema come la memoria insufficiente.
Questa natura effimera li rende inadatti a contenere dati o stato delle applicazioni. Se memorizzi dati in un Activity o Fragment, 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 UI.
Layout adattivi
L'app deve gestire correttamente le modifiche alla configurazione, ad esempio i cambiamenti di orientamento del dispositivo o le modifiche alle dimensioni della finestra dell'app. Implementa i layout canonici adattivi per offrire un'esperienza utente ottimale su una vasta gamma 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 della UI e da altri componenti dell'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 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 distrugge l'app per liberare risorse.
La tua app continua a funzionare nei casi in cui la connessione di rete è intermittente o non disponibile.
Basare l'architettura dell'app sulle classi del modello di dati per renderla solida e testabile.
Unica fonte attendibile
Quando nella tua app viene definito un nuovo tipo di dati, devi assegnargli una singola fonte attendibile. 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; per modificare i dati, l'SSOT espone funzioni o riceve eventi che altri tipi possono chiamare.
Questo pattern presenta 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, quindi è 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 attendibile può essere un
ViewModel.
Flusso di dati unidirezionale
Il principio di un'unica fonte di verità viene spesso utilizzato con il pattern di flusso di dati unidirezionale (UDF). In UDF, state scorre in una sola direzione, in genere dal componente principale al componente secondario. 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
Considerando i principi architettonici comuni, ogni applicazione deve avere almeno due livelli:
- Livello UI: mostra i dati dell'applicazione sullo schermo
- Livello dati: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
Un'architettura moderna di app per Android utilizza le seguenti tecniche (tra le altre):
- Architettura adattiva e a più livelli
- Flusso di dati unidirezionale (UDF) in tutti i livelli dell'app
- 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 Consigli per l'architettura Android.
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 comprende due tipi di costrutti:
- Elementi dell'interfaccia utente che visualizzano i dati sullo schermo. Questi elementi vengono creati utilizzando le funzioni di Jetpack Compose per supportare i layout adattabili.
- Contenitori di stato (ad esempio
ViewModel) che contengono dati, li espongono all'interfaccia utente e gestiscono la logica
Per le UI adattive, i gestori di stato come gli oggetti ViewModel espongono lo stato della UI che
si adatta a diverse classi di dimensioni della finestra. Puoi utilizzare
currentWindowAdaptiveInfo() per derivare questo stato dell'interfaccia utente. 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 scoprire di più, 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: comprende regole che determinano il modo in cui la tua app crea, archivia e modifica i dati.
Il livello dati è costituito da repository, ognuno dei quali può contenere da zero a molte origini dati. Devi creare una classe repository per ogni tipo diverso di
dati che gestisci 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 di:
- 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 di origine dati sono il ponte tra l'applicazione e il sistema per le operazioni sui dati.
Per scoprire di più, consulta la pagina del data layer.
Livello del dominio
Il livello di dominio è un livello facoltativo 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ù 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 riutilizzabilità.
Le classi nel livello del dominio sono comunemente chiamate casi d'uso o interattori.
Ogni caso d'uso deve essere responsabile 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 visualizzare il messaggio corretto sullo schermo.
Per saperne di più, consulta la pagina del livello di dominio.
Gestire le dipendenze tra i componenti
Le classi della 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à. I pattern ti consentono anche di passare rapidamente da un'implementazione di test a una 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.
Non archiviare dati nei componenti dell'app.
Evita di designare i punti di accesso della tua app, come attività, servizi e ricevitori di trasmissione, come origini dati. I punti di accesso devono coordinarsi solo con altri componenti per recuperare il sottoinsieme di dati pertinente a quel punto di accesso. 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.
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 nell'app dai componenti dell'app contribuisce alla testabilità e riduce l'accoppiamento all'interno dell'app.
Definisci confini di responsabilità chiari tra i moduli della tua app.
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 binding dei dati, nella stessa classe. Seguire l'architettura dell'app consigliata ti aiuterà.
Mostra il minor numero possibile di informazioni di ogni modulo.
Non creare scorciatoie che espongono dettagli di implementazione interni. Potresti guadagnare un po' di tempo nel breve termine, ma è probabile che tu debba affrontare un debito tecnico molte volte superiore man mano che il 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. 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 di visualizzazione. Consulta la galleria di pattern di progettazione delle app per selezionare i layout più adatti ai tuoi casi d'uso.
Preserva lo stato della UI in seguito alle modifiche alla configurazione.
Quando progetti per layout adattivi, mantieni lo stato dell'interfaccia utente durante le modifiche alla configurazione, ad esempio ridimensionamento, chiusura e modifiche all'orientamento del display. La tua architettura deve verificare che lo stato attuale dell'utente venga mantenuto, garantendo un'esperienza ottimale.
Progetta componenti dell'interfaccia utente riutilizzabili e componibili.
Crea componenti UI riutilizzabili e componibili per supportare la progettazione adattiva. In questo modo puoi combinare e riorganizzare i componenti per adattarli a varie dimensioni e orientamenti dello schermo senza un refactoring significativo.
Valuta come rendere testabile ogni parte della tua app in modo isolato.
Un'API ben definita per il recupero dei dati dalla rete facilita il test del modulo che li archivia in un database locale. Se invece combini la logica di queste due funzioni in un unico punto o distribuisci il codice di rete in tutto il codebase, il test diventa molto più difficile, se non impossibile.
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. Il tipo 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 è sicuro chiamarli 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 ce l'hanno, 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 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 tecnico più produttivo. Tuttavia, l'architettura richiede anche un investimento di tempo iniziale. Per aiutarti a giustificare questo tempo al resto della tua organizzazione, dai un'occhiata a questi case study in cui altre aziende condividono le loro storie di successo relative a una buona architettura nella loro app.
Campioni
I seguenti esempi mostrano una buona architettura dell'app: