L'inserimento di dipendenze (DI) è una tecnica ampiamente utilizzata nella programmazione e adatta allo sviluppo di Android. Seguendo i principi della DI, getta le basi per una buona architettura delle app.
L'implementazione dell'inserimento delle dipendenze offre i seguenti vantaggi:
- Riutilizzabilità del codice
- Facilità di refactoring
- Facilità di esecuzione dei test
Concetti fondamentali dell'inserimento delle dipendenze
Prima di parlare nello specifico dell'inserimento delle dipendenze in Android, questa pagina fornisce una panoramica più generale sul funzionamento dell'inserimento delle dipendenze.
Che cos'è l'inserimento delle dipendenze?
I corsi spesso richiedono riferimenti ad altre classi. Ad esempio, una classe Car
potrebbe richiedere un riferimento a una classe Engine
. Queste classi obbligatorie sono chiamate
dependencies e in questo esempio la classe Car
dipende
dalla presenza di un'istanza della classe Engine
da eseguire.
Esistono tre modi in cui una classe può ottenere un oggetto di cui ha bisogno:
- La classe crea la dipendenza di cui ha bisogno. Nell'esempio precedente,
Car
crea e inizializza la propria istanza diEngine
. - Prendilo da qualche altra parte. Alcune API per Android, come i getter
Context
egetSystemService()
, funzionano in questo modo. - Deve essere specificato come parametro. L'app può fornire queste dipendenze al momento della creazione della classe oppure trasmetterle alle funzioni che richiedono ciascuna dipendenza. Nell'esempio precedente, il costruttore
Car
riceverebbeEngine
come parametro.
La terza opzione è l'inserimento delle dipendenze. Con questo approccio, puoi definire le dipendenze di una classe anziché farle ottenere dall'istanza di classe.
Ecco un esempio. Senza inserimento di dipendenze, la rappresentazione di un Car
che
crea la propria dipendenza Engine
nel codice ha il seguente aspetto:
Kotlin
class Car { private val engine = Engine() fun start() { engine.start() } } fun main(args: Array) { val car = Car() car.start() }
Java
class Car { private Engine engine = new Engine(); public void start() { engine.start(); } } class MyApp { public static void main(String[] args) { Car car = new Car(); car.start(); } }
![Classe auto senza inserimento di dipendenze](https://developer.android.com/static/images/training/dependency-injection/1-car-engine-no-di.png?authuser=6&hl=it)
Questo non è un esempio di inserimento delle dipendenze perché la classe Car
sta creando la propria classe Engine
. Questo può essere problematico perché:
Car
eEngine
sono caratterizzati dall'alto accoppiamento: un'istanza diCar
utilizza un solo tipo diEngine
e non è facile utilizzare sottoclassi o implementazioni alternative. SeCar
dovesse creare il proprioEngine
, dovresti creare due tipi diCar
invece di riutilizzare lo stessoCar
per i motori di tipoGas
eElectric
.L'estrema dipendenza da
Engine
rende i test più difficili.Car
utilizza un'istanza reale diEngine
, impedendoti così di utilizzare un doppio di test per modificareEngine
per diversi scenari di test.
Come si presenta il codice con l'inserimento delle dipendenze? Invece di ogni istanza di Car
che costruisce il proprio oggetto Engine
all'inizializzazione, riceve un oggetto Engine
come parametro nel suo costruttore:
Kotlin
class Car(private val engine: Engine) { fun start() { engine.start() } } fun main(args: Array) { val engine = Engine() val car = Car(engine) car.start() }
Java
class Car { private final Engine engine; public Car(Engine engine) { this.engine = engine; } public void start() { engine.start(); } } class MyApp { public static void main(String[] args) { Engine engine = new Engine(); Car car = new Car(engine); car.start(); } }
![Classe auto che utilizza l'inserimento delle dipendenze](https://developer.android.com/static/images/training/dependency-injection/1-car-engine-di.png?authuser=6&hl=it)
La funzione main
utilizza Car
. Poiché Car
dipende da Engine
, l'app crea
un'istanza di Engine
e la utilizza per creare un'istanza di Car
. I vantaggi di questo approccio basato sull'IA sono:
Riutilizzabilità di
Car
. Puoi trasferire diverse implementazioni diEngine
aCar
. Ad esempio, potresti definire una nuova sottoclasse diEngine
denominataElectricEngine
che vuoi utilizzareCar
. Se utilizzi DI, devi solo passare un'istanza della sottoclasseElectricEngine
aggiornata eCar
continuerà a funzionare senza ulteriori modifiche.Test facile di
Car
. Puoi superare i doppi di test per testare i diversi scenari. Ad esempio, potresti creare un test double diEngine
chiamatoFakeEngine
e configurarlo per test diversi.
Esistono due modi principali per eseguire l'inserimento di dipendenze in Android:
Manufacturer Injection (Inserimento costruttore). Questo è il modo descritto sopra. Le dipendenze di una classe vengono trasferite al suo costruttore.
Iniezione di campo (o iniezione setter). Per alcune classi di framework Android, come attività e frammenti, viene creata un'istanza dal sistema, pertanto l'inserimento del costruttore non è possibile. Con l'inserimento dei campi, viene creata un'istanza delle dipendenze dopo la creazione della classe. Il codice sarà simile al seguente:
Kotlin
class Car { lateinit var engine: Engine fun start() { engine.start() } } fun main(args: Array) { val car = Car() car.engine = Engine() car.start() }
Java
class Car { private Engine engine; public void setEngine(Engine engine) { this.engine = engine; } public void start() { engine.start(); } } class MyApp { public static void main(String[] args) { Car car = new Car(); car.setEngine(new Engine()); car.start(); } }
Iniezione automatica delle dipendenze
Nell'esempio precedente, hai creato, fornito e gestito autonomamente le dipendenze delle diverse classi, senza fare affidamento su una libreria. Questo metodo è chiamato inserimento manuale delle dipendenze o inserimento manuale delle dipendenze. Nell'esempio Car
, era presente una sola dipendenza, ma più dipendenze e classi possono rendere più noiosa l'inserimento manuale delle dipendenze. L'inserimento manuale delle dipendenze
presenta anche diversi problemi:
Per le app di grandi dimensioni, gestire correttamente tutte le dipendenze e collegarle può richiedere una grande quantità di codice boilerplate. In un'architettura a più livelli, per creare un oggetto per un livello superiore, devi fornire tutte le dipendenze dei livelli sottostanti. Ad esempio, per costruire un'auto reale potrebbero servirsi un motore, una trasmissione, un telaio e altre parti; un motore a sua volta necessita di cilindri e candele.
Se non riesci a creare dipendenze prima di trasferirle, ad esempio se utilizzi inizializzazioni lazy o oggetti di ambito ai flussi della tua app, devi scrivere e gestire un container personalizzato (o un grafico delle dipendenze) che gestisca la durata delle dipendenze in memoria.
Esistono librerie che risolvono questo problema automatizzando il processo di creazione e fornitura delle dipendenze. Rientrano in due categorie:
Soluzioni basate sulla riflessione che collegano le dipendenze in fase di runtime.
Soluzioni statiche che generano il codice per connettere le dipendenze in fase di compilazione.
Dagger è una popolare libreria di inserimento di dipendenze per Java, Kotlin e Android gestita da Google. Dagger facilita l'utilizzo dell'IA nell'app creando e gestendo per te il grafico delle dipendenze. Fornisce dipendenze completamente statiche e in fase di compilazione che rispondono a molti dei problemi di sviluppo e prestazioni di soluzioni basate sulla riflessione come Guice.
Alternative all'inserimento delle dipendenze
Un'alternativa all'inserimento delle dipendenze è l'uso di un rilevatore di servizi. Il pattern di progettazione del localizzatore di servizi migliora anche il disaccoppiamento delle classi dalle dipendenze concrete. Puoi creare una classe nota come strumento di localizzazione dei servizi che crea e archivia le dipendenze e le fornisce on demand.
Kotlin
object ServiceLocator { fun getEngine(): Engine = Engine() } class Car { private val engine = ServiceLocator.getEngine() fun start() { engine.start() } } fun main(args: Array) { val car = Car() car.start() }
Java
class ServiceLocator { private static ServiceLocator instance = null; private ServiceLocator() {} public static ServiceLocator getInstance() { if (instance == null) { synchronized(ServiceLocator.class) { instance = new ServiceLocator(); } } return instance; } public Engine getEngine() { return new Engine(); } } class Car { private Engine engine = ServiceLocator.getInstance().getEngine(); public void start() { engine.start(); } } class MyApp { public static void main(String[] args) { Car car = new Car(); car.start(); } }
Il pattern del localizzatore dei servizi è diverso dall'inserimento delle dipendenze nel modo in cui vengono utilizzati gli elementi. Con il pattern del localizzatore di servizio, le classi hanno il controllo e chiedono l'inserimento degli oggetti; con l'inserimento delle dipendenze, l'app ha il controllo e inserisce proattivamente gli oggetti richiesti.
Rispetto all'inserimento delle dipendenze:
La raccolta di dipendenze richiesta da un localizzatore di servizi rende il codice più difficile da testare perché tutti i test devono interagire con lo stesso localizzatore globale di servizi.
Le dipendenze sono codificate nell'implementazione della classe, non nell'interfaccia API. Di conseguenza, è più difficile capire le esigenze di una classe dall'esterno. Di conseguenza, le modifiche a
Car
o alle dipendenze disponibili nel localizzatore del servizio potrebbero causare errori di runtime o test, a causa dell'errore dei riferimenti.La gestione della durata degli oggetti è più difficile se vuoi limitare l'ambito a elementi diversi dalla durata dell'intera app.
Utilizzare Hilt nell'app Android
Hilt è la libreria consigliata di Jetpack per l'inserimento di dipendenze in Android. Hilt definisce un modo standard di eseguire il DIO nell'applicazione, fornendo container per ogni classe Android nel progetto e gestendo automaticamente i loro cicli di vita.
Hilt è basato sulla famosa libreria DI Dagger per trarre vantaggio dalla correttezza del tempo di compilazione, dalle prestazioni di runtime, dalla scalabilità e dal supporto di Android Studio offerto da Dagger.
Per scoprire di più su Hilt, consulta la pagina relativa all'inserimento delle dipendenze con Hilt.
conclusione
L'inserimento delle dipendenze offre alla tua app i seguenti vantaggi:
Riutilizzabilità delle classi e disaccoppiamento delle dipendenze: è più facile scambiare le implementazioni di una dipendenza. Il riutilizzo del codice è migliorato grazie all'inversione del controllo e le classi non controllano più il modo in cui vengono create le dipendenze, ma funzionano con qualsiasi configurazione.
Facilità di refactoring: le dipendenze diventano una parte verificabile della superficie API, quindi possono essere controllate al momento della creazione degli oggetti o della compilazione, anziché essere nascoste come dettagli di implementazione.
Facilità di test: una classe non gestisce le sue dipendenze, quindi quando esegui il test puoi passare implementazioni diverse per testare tutti i diversi casi.
Per comprendere appieno i vantaggi dell'inserimento delle dipendenze, devi provarlo manualmente nell'app come illustrato nell'articolo Inserimento manuale delle dipendenze.
Risorse aggiuntive
Per scoprire di più sull'inserimento delle dipendenze, consulta le seguenti risorse aggiuntive.