Pattern di modularizzazione comuni

Non esiste una singola strategia di modularizzazione adatta a tutti i progetti. A causa di la natura flessibile di Gradle, esistono pochi vincoli su come per organizzare un progetto. Questa pagina fornisce una panoramica di alcune regole generali e pattern che puoi utilizzare durante lo sviluppo di app per Android multimodulo.

Principio di alta coesione e basso accoppiamento

Un modo per caratterizzare un codebase modulare consiste nell'utilizzare l'accoppiamento e coesione. L'accoppiamento misura il grado di coinvolgimento dei moduli dipendono l'uno dall'altro. La coesione, in questo contesto, misura il modo in cui gli elementi di una sono correlate dal punto di vista funzionale. Come regola generale, devi cercare di basso accoppiamento e coesione elevata:

  • Basso accoppiamento significa che i moduli dovrebbero essere il più indipendenti possibile l'uno con l'altro, in modo che le modifiche a un modulo abbiano un impatto nullo o minimo sulle e gli altri moduli. I moduli non devono conoscere il funzionamento interno di altri moduli.
  • Elevata coesione significa che i moduli devono comprendere una raccolta di codice che funge da sistema. Devono avere responsabilità chiaramente definite e rimanere entro i limiti di determinate conoscenze settoriali. Valuta un ebook di esempio un'applicazione. Potrebbe essere inappropriato combinare il codice relativo al libro e al pagamento nello stesso modulo, poiché sono due domini funzionali diversi.
di Gemini Advanced.

Tipi di moduli

Il modo in cui organizzi i moduli dipende principalmente dall'architettura dell'app. Inferiore sono alcuni tipi comuni di moduli che potresti introdurre nella tua app mentre segui la nostra architettura delle app consigliata.

Moduli di dati

Un modulo dati di solito contiene un repository, origini dati e classi di modelli. La tre responsabilità principali di un modulo dati sono:

  1. Incapsulare tutti i dati e la logica di business di un determinato dominio: ogni dato dovrebbe essere responsabile della gestione dei dati che rappresentano un dominio. Può gestire molti tipi di dati, purché correlati.
  2. Esponi il repository come API esterna: l'API pubblica di un data warehouse devono essere un repository in quanto sono responsabili dell'esposizione dei dati per il resto dell'app.
  3. Nascondere dall'esterno tutti i dettagli di implementazione e le origini dati: Le origini dati devono essere accessibili solo dai repository dello stesso modulo. Rimangono nascosti all'esterno. Puoi applicare in modo forzato questa funzionalità utilizzando Parola chiave per la visibilità di private o internal.
di Gemini Advanced.
.
Figura 1. Esempi di moduli di dati e relativi contenuti.

Moduli delle funzionalità

Una funzione è una parte isolata della funzionalità di un'app che di solito corrisponde a una schermata o a una serie di schermate strettamente correlate, come la registrazione o il pagamento flusso di lavoro. Se la tua app dispone di una barra di navigazione in basso, è probabile che ogni destinazione è una caratteristica.

Figura 2. Ogni scheda di questa applicazione può essere definita come una funzionalità.

Le funzionalità sono associate a schermate o destinazioni nella tua app. Pertanto, è probabile che abbiano una UI associata e ViewModel per gestire la logica e lo stato. Una singola funzionalità non deve essere necessariamente limitata a una singola visualizzazione o destinazione di navigazione. I moduli delle funzionalità dipendono dai moduli di dati.

Figura 3. Esempi di moduli delle funzionalità e relativi contenuti.

Moduli dell'app

I moduli dell'app sono un punto di accesso all'applicazione. Dipendono dalla funzionalità e di solito forniscono la navigazione root. È possibile compilare un singolo modulo dell'app a una serie di programmi binari diversi grazie alle varianti della build.

Figura 4. Grafico delle dipendenze dei moduli versione *Demo* e *Completa* per le versioni dei prodotti.

Se la tua app ha come target più tipi di dispositivi, ad esempio auto, Wear OS o TV, definisci un modulo dell'app per ciascuno. Questo aiuta a separare le piattaforme delle dipendenze.

Figura 5. Grafico delle dipendenze dell'app Wear.

Moduli comuni

I moduli comuni, noti anche come moduli principali, contengono codice che altri moduli che usano spesso. Riducono la ridondanza e non rappresentano alcuno strato specifico in l'architettura di un'app. Ecco alcuni esempi di moduli comuni:

  • Modulo UI: se utilizzi elementi UI personalizzati o un branding elaborato nelle tue app, dovresti considerare di incorporare la raccolta di widget in un modulo per poter riutilizzare tutte le caratteristiche. Questo può contribuire a rendere la UI coerente diverse funzionalità. Ad esempio, se i temi sono centralizzati, puoi evitare un doloroso refactoring quando avviene un rebranding.
  • Modulo Analytics: il monitoraggio è spesso dettato da requisiti aziendali con poca considerazione per l'architettura software. I tracker di Google Analytics spesso utilizzato in molti componenti non correlati. In questo caso, è possibile un modulo dedicato per l'analisi dei dati.
  • Modulo di rete: quando molti moduli richiedono una connessione di rete, è possibile che considera l'idea di creare un modulo dedicato alla fornitura di un client http. È particolarmente utile quando il client richiede una configurazione personalizzata.
  • Modulo di utilità: le utility, note anche come helper, sono di solito piccole parti di codice riutilizzato nell'applicazione. Esempi di utilità includono per il test, una funzione di formattazione della valuta, uno strumento di convalida email o un operatore.

Moduli di test

I moduli di test sono moduli Android utilizzati solo a scopo di test. I moduli contengono codice, risorse di test e dipendenze di test che sono e non sono necessari durante il runtime dell'applicazione. I moduli di test sono creati in modo da separare il codice specifico per il test dalla rendendo il codice del modulo più facile da gestire e mantenere.

Casi d'uso per i moduli di test

I seguenti esempi illustrano situazioni in cui l'implementazione dei moduli di test può essere particolarmente utile:

  • Codice di test condiviso. Se il progetto contiene più moduli e alcuni il codice di test sia applicabile a più di un modulo, puoi creare un per condividere il codice. In questo modo puoi ridurre i duplicati e rendere il tuo test e il codice più semplice da gestire. Il codice di test condiviso può includere classi di utilità o come asserzioni personalizzate o matcher, nonché dati di test come di risposte JSON simulate.

  • Configurazioni build più chiare: i moduli di test ti consentono di avere a disposizione configurazioni di build, in quanto possono avere un proprio file build.gradle. Non devi non riempire il file build.gradle del modulo dell'app con configurazioni che sono pertinente solo per i test.

  • Test di integrazione: è possibile utilizzare i moduli di test per archiviare l'integrazione test utilizzati per verificare le interazioni tra le diverse parti della tua app tra cui interfaccia utente, logica di business, richieste di rete e query del database.

  • Applicazioni su larga scala: i moduli di test sono particolarmente utili per applicazioni su larga scala con codebase complessi e più moduli. In tale casi, i moduli di test possono aiutare a migliorare l'organizzazione e la manutenibilità del codice.

di Gemini Advanced.
Figura 6. È possibile usare i moduli di test per isolare i moduli che altrimenti dipenderebbero l'uno dall'altro.

Comunicazione da modulo a modulo

I moduli raramente esistono in una separazione totale e spesso si basano su altri moduli e comunicare con loro. È importante mantenere basso l'accoppiamento anche quando i moduli collaborano e scambiano informazioni frequentemente. A volte diretta la comunicazione tra due moduli non è auspicabile, come nel caso vincoli dell'architettura. Potrebbe anche essere impossibile, come nel caso di modelli delle dipendenze.

Figura 7. Una comunicazione diretta e bidirezionale tra i moduli è impossibile a causa di dipendenze cicliche. Un modulo di mediazione è necessario per coordinare il flusso di dati tra altri due moduli indipendenti.

Per risolvere questo problema puoi creare un terzo modulo di mediazione tra altri due moduli. Il modulo mediatore può ascoltare i messaggi di entrambi dei moduli e inoltrali se necessario. Nella nostra app di esempio, la procedura di pagamento schermata deve sapere quale libro acquistare anche se l'evento ha avuto origine in una schermata separata che fa parte di una funzionalità diversa. In questo caso, il mediatore è il modulo proprietario del grafico di navigazione (di solito un modulo dell'app). In questo esempio, utilizziamo la navigazione per trasmettere i dati dalla funzione Home alla funzionalità di pagamento utilizzando il componente Navigazione.

navController.navigate("checkout/$bookId")

La destinazione di pagamento riceve un ID libro come argomento che utilizza per recuperare informazioni sul libro. Puoi utilizzare l'handle dello stato salvato per recuperare argomenti di navigazione all'interno di ViewModel di una funzionalità di destinazione.

class CheckoutViewModel(savedStateHandle: SavedStateHandle, ) : ViewModel() {

   val uiState: StateFlow<CheckoutUiState> =
      savedStateHandle.getStateFlow<String>("bookId", "").map { bookId ->
          // produce UI state calling bookRepository.getBook(bookId)
      }
      
}

Non devi passare oggetti come argomenti di navigazione. Usa invece ID semplici utilizzabili per accedere e caricare le risorse desiderate dal livello dati. In questo modo, mantieni basso l'accoppiamento e non violi la singola fonte di verità. dell'IA.

Nell'esempio in basso, entrambi i moduli delle funzionalità dipendono dallo stesso modulo dati. Questo consente di ridurre al minimo la quantità di dati di cui il modulo mediatore ha bisogno in avanti e mantiene basso l'accoppiamento tra i moduli. Invece di passare oggetti, i moduli dovrebbero scambiare ID primitivi e caricare le risorse da un modulo dati condiviso.

Figura 8. Due moduli di funzionalità che si basano su un modulo di dati condiviso.

Inversione delle dipendenze

L'inversione delle dipendenze si verifica quando organizzi il codice in modo che l'astrazione sia da un'implementazione concreta.

  • Astrazione: un contratto che definisce in che modo i componenti o i moduli nel tuo le applicazioni interagiscono tra loro. I moduli di astrazione definiscono l'API tuo sistema e contenere interfacce e modelli.
  • Implementazione concreta: moduli che dipendono dal modulo di astrazione e implementare il comportamento di un'astrazione.

I moduli che si basano sul comportamento definito nel modulo di astrazione devono dipendono dall'astrazione stessa, non dalle implementazioni specifiche.

Figura 9. Invece dei moduli di alto livello che dipendono direttamente dai moduli di basso livello, i moduli di alto livello e di implementazione dipendono dal modulo di astrazione.

Esempio

Immagina un modulo di funzionalità che necessita di un database per funzionare. Il modulo delle funzionalità non è preoccupati di come viene implementato il database, che si tratti di un database di stanze un'istanza Firestore remota. Deve solo archiviare e leggere i dati dell'applicazione.

Per ottenere questo risultato, il modulo delle funzionalità dipende dal modulo di astrazione anziché rispetto a una specifica implementazione del database. Questa astrazione definisce API di database. In altre parole, imposta le regole su come interagire con per configurare un database. Ciò consente al modulo delle funzionalità di utilizzare qualsiasi database senza dover conoscere i dettagli di implementazione di base.

Il modulo di implementazione concreto fornisce l'implementazione effettiva API definite nel modulo di astrazione. A questo scopo, l'implementazione dipende anche dal modulo di astrazione.

Inserimento delle dipendenze

A questo punto ti starai chiedendo in che modo il modulo delle funzionalità è collegato nel modulo di implementazione. La risposta è Dipendency Injection. La funzionalità non crea direttamente l'istanza di database richiesta. Invece, e specifica le dipendenze di cui ha bisogno. Queste dipendenze vengono quindi fornite all'esterno, di solito nel modulo dell'app.

releaseImplementation(project(":database:impl:firestore"))

debugImplementation(project(":database:impl:room"))

androidTestImplementation(project(":database:impl:mock"))

Vantaggi

I vantaggi di separare le API dalle loro implementazioni sono i seguenti:

  • Intercambiabilità: con una chiara separazione tra API e implementazione puoi sviluppare più implementazioni per la stessa API e passare senza modificare il codice che utilizza l'API. Potrebbe essere particolarmente utile negli scenari in cui vuoi fornire capacità o comportamenti in diversi contesti. Ad esempio, una simulazione l'implementazione per i test rispetto a un'implementazione reale per la produzione.
  • Disaccoppiamento: la separazione significa che i moduli che utilizzano le astrazioni non dipendono da una tecnologia specifica. Se scegli di modificare il database da Spazio a Firestore in un secondo momento, sarebbe più semplice perché si verificano nel modulo specifico in cui viene svolto il lavoro (modulo di implementazione) e non influisce su altri moduli utilizzando l'API del database.
  • Testabilità: separare le API dalle loro implementazioni può notevolmente per semplificare i test. Puoi scrivere scenari di test in base ai contratti API. Puoi usare implementazioni diverse per testare vari scenari e casi limite, incluse implementazioni fittizie.
  • Prestazioni di compilazione migliorate: quando separi un'API e i suoi l'implementazione in moduli diversi, le modifiche all'implementazione non costringono il sistema di compilazione a ricompilare i moduli, a seconda API di Google Cloud. Questo si traduce in tempi di creazione più rapidi e aumento della produttività, in particolare nei progetti di grandi dimensioni, dove i tempi di compilazione possono essere significativi.

Quando separare

È utile separare le API dalle loro implementazioni nel i seguenti casi:

  • Funzionalità diverse: se puoi implementare parti del sistema in in più modi, un'API chiara consente l'intercambiabilità implementazioni. Ad esempio, potresti avere un sistema di rendering che utilizza OpenGL o Vulkan oppure un sistema di fatturazione compatibile con Play o con la fatturazione interna tramite Google Cloud CLI o tramite l'API Compute Engine.
  • Applicazioni multiple: se stai sviluppando più applicazioni con funzionalità condivise per diverse piattaforme, puoi definire API sviluppare implementazioni specifiche per piattaforma.
  • Team indipendenti: la separazione consente a sviluppatori o team diversi di lavorare contemporaneamente su diverse parti del codebase. Gli sviluppatori dovrebbero concentrarsi sulla comprensione dei contratti API e sul loro utilizzo corretto. Non è necessario preoccuparsi dei dettagli di implementazione di altri moduli.
  • Codebase grande: quando il codebase è grande o complesso, l'API viene separata dell'implementazione rende il codice più gestibile. Ti permette di interrompere in unità più granulari, comprensibili e gestibili.

Come eseguire l'implementazione?

Per implementare l'inversione delle dipendenze, segui questi passaggi:

  1. Crea un modulo di astrazione: questo modulo deve contenere API (interfacce e modelli) che definisce il comportamento della caratteristica.
  2. Crea moduli di implementazione. I moduli di implementazione devono basarsi sulle modulo API e implementare il comportamento di un'astrazione.
    Invece dei moduli di alto livello che dipendono direttamente dai moduli di basso livello, i moduli di alto livello e di implementazione dipendono dal modulo di astrazione.
    Figura 10. I moduli di implementazione dipendono dal modulo di astrazione.
    .
  3. Rendi i moduli di alto livello dipendenti dai moduli di astrazione: invece di a seconda di un'implementazione specifica, fai in modo che i moduli moduli di astrazione. I moduli di alto livello non hanno bisogno di conoscere l'implementazione dettagli, hanno bisogno solo del contratto (API).
    I moduli di alto livello dipendono dalle astrazioni, non dall&#39;implementazione.
    Figura 11. I moduli di alto livello dipendono dalle astrazioni, non dall'implementazione.
    .
  4. Fornisci il modulo di implementazione: infine, devi fornire gli effettivi per le tue dipendenze. L'implementazione specifica dipende la configurazione del tuo progetto, ma di solito il modulo dell'app è un buon punto per farlo. Per fornire l'implementazione, specificala come dipendenza per il modello una variante di build o un set di origini di test.
    Il modulo dell&#39;app fornisce un&#39;implementazione effettiva.
    Figura 12. Il modulo dell'app fornisce un'implementazione effettiva.
    .

Best practice generali

Come accennato all'inizio, non esiste un solo modo giusto per sviluppare un'app multimodulo. Proprio come esistono molte architetture software, esistono molti modi per modularizzare un'app. Tuttavia, le seguenti informazioni generali possono aiutarti a rendere il codice più leggibile, gestibile e testabile.

Mantieni la configurazione coerente

Ogni modulo introduce l'overhead della configurazione. Se il numero di moduli raggiunge una certa soglia, la gestione di una configurazione coerente diventa sfida. Ad esempio, è importante che i moduli utilizzino dipendenze dello stesso completamente gestita. Se devi aggiornare un numero elevato di moduli solo per spostare un delle dipendenze, non è solo uno sforzo, ma anche uno spazio e gli errori continui di configurazione. Per risolvere il problema, puoi usare uno degli strumenti di Gradle per centralizza la configurazione:

  • I catalogi delle versioni sono un elenco sicuro dei tipi di dipendenze generate da Gradle durante la sincronizzazione. È uno spazio centrale per dichiarare tutti i ed è disponibile per tutti i moduli di un progetto.
  • Utilizza i plug-in di convenzione per condividere la logica di build tra i moduli.

Esposizione il meno possibile

L'interfaccia pubblica di un modulo dovrebbe essere minima ed esporre solo di base. Non deve rendere pubblici dettagli dell'implementazione all'esterno. Ambito tutto il più piccolo possibile. Usa private o internal di Kotlin l'ambito di visibilità per rendere il modulo delle dichiarazioni private. Al momento della dichiarazione dipendenze nel modulo, preferisci implementation rispetto a api. Quest'ultimo espone dipendenze transitive ai consumer del modulo. Utilizzo l'implementazione può migliorare i tempi di compilazione poiché riduce il numero di moduli che devono essere ricostruiti.

Preferisco Kotlin e Moduli Java

Android Studio supporta tre tipi essenziali di moduli:

  • I moduli dell'app sono un punto di accesso alla tua applicazione. Possono contenere codice sorgente, risorse, asset e un AndroidManifest.xml. L'output di un Il modulo dell'app è un Android App Bundle (AAB) o un pacchetto di applicazioni Android (APK).
  • I moduli della raccolta hanno gli stessi contenuti dei moduli dell'app. Sono usato da altri moduli Android come dipendenza. L'output di un modulo della libreria Android Archive (AAR) è strutturalmente identica ai moduli dell'app, vengono compilate in un file AAR (Android Archive) che può essere successivamente utilizzato da altri come dipendenza. Un modulo della libreria consente di incapsulano e riutilizzano la stessa logica e le stesse risorse in molti moduli dell'app.
  • Le librerie Kotlin e Java non contengono risorse, asset o risorse Android o manifest.

Poiché i moduli Android hanno un carico di lavoro, preferibilmente, è consigliabile utilizzare Kotlin o Java il più possibile.