L'inserimento delle dipendenze (DI, Dependency injection) è una tecnica ampiamente utilizzata nella programmazione per lo sviluppo Android. Seguendo i principi dell'IA, si stabilisce 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 sull'inserimento delle dipendenze
Prima di parlare nello specifico dell'inserimento delle dipendenze in Android, questa pagina fornisce per una panoramica più generale sul funzionamento dell'inserimento delle dipendenze.
Che cos'è l'inserimento delle dipendenze?
I corsi richiedono spesso riferimenti ad altri corsi. Ad esempio, un corso Car
potrebbe richiedere un riferimento a una classe Engine
. Queste classi obbligatorie sono chiamate
dependencies e in questo esempio la classe Car
dipende da
avere un'istanza della classe Engine
da eseguire.
Una classe può ottenere un oggetto di cui ha bisogno in tre modi:
- La classe crea la dipendenza di cui ha bisogno. Nell'esempio precedente,
Car
creerà e inizializza la propria istanza diEngine
. - Afferralo da un'altra parte. Alcune API Android, come
Context
getter egetSystemService()
, lavorate in molti modi diversi. - Fai in modo che venga fornito come parametro. L'app può fornire questi
le dipendenze quando la classe viene creata oppure le passiamo alle funzioni
che richiedono ciascuna dipendenza. Nell'esempio precedente, il valore
Car
costruttore riceverebbeEngine
come parametro.
La terza opzione è l'inserimento delle dipendenze. Con questo approccio le dipendenze di una classe e le fornisce anziché avere la classe o li ottiene da sé.
Ecco un esempio. Senza inserimento di dipendenze, rappresenta un valore Car
che
crea la propria dipendenza Engine
nel codice come segue:
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(); } }
Questo non è un esempio di inserimento delle dipendenze perché la classe Car
è
creando il proprio Engine
. Questo può essere problematico perché:
Car
eEngine
sono caratterizzati dall'alto accoppiamento: un'istanza diCar
utilizza una di tipoEngine
e nessuna sottoclasse o implementazioni alternative può essere facilmente in uso. SeCar
dovesse creare il proprioEngine
, dovresti creare due tipi diCar
invece di riutilizzare lo stessoCar
per i motori di tipoGas
eElectric
.La forte dipendenza da
Engine
rende i test più difficili.Car
utilizza un reale diEngine
, impedendoti così di utilizzare un'istanza test double per modificareEngine
per diversi scenari di test.
Che aspetto ha il codice con l'inserimento delle dipendenze? Invece di ogni istanza
di Car
costruisce il proprio oggetto Engine
all'inizializzazione, riceve un
Engine
oggetto 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(); } }
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
. La
i vantaggi di questo approccio basato sull'AI sono:
Riutilizzabilità di
Car
. Puoi trasferire diverse implementazioni diEngine
aCar
. Ad esempio, puoi definire una nuova sottoclasse diEngine
chiamataElectricEngine
che vuoi cheCar
utilizzi. Se usi DI, devi solo fare passa in un'istanza della sottoclasseElectricEngine
aggiornata eCar
continua a funzionare senza ulteriori modifiche.Test semplici di
Car
. Puoi superare il test doppio per testare le diverse diversi scenari. Ad esempio, potresti creare un doppio di test diEngine
chiamatoFakeEngine
e configurala per test diversi.
Esistono due modi principali per eseguire l'inserimento delle dipendenze in Android:
Inserimento costruttore. Questo è il metodo descritto sopra. Passi il di una classe al suo costruttore.
Field Injection (Inserimento campo) o Setter Injection (Inserimento setter). Alcune classi di framework Android come attività e frammenti creano un'istanza dal sistema, quindi il costruttore non è possibile effettuare l'inserimento. Con l'inserimento dei campi, viene creata un'istanza delle dipendenze dopo la creazione del corso. 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(); } }
Inserimento automatico delle dipendenze
Nell'esempio precedente, hai creato, fornito e gestito le dipendenze
dei vari corsi senza fare affidamento su una biblioteca. Questo processo è chiamato
inserimento manuale delle dipendenze o inserimento manuale delle dipendenze. In Car
Ad esempio, c'era una sola dipendenza, ma più dipendenze e classi possono
rendere più noiosa l'inserimento manuale delle dipendenze. Inserimento manuale delle dipendenze
presenta anche diversi problemi:
Per le app di grandi dimensioni, connettere tutte le dipendenze può richiedere una grande quantità di codice boilerplate. In un ambiente multilivello architetturale, per creare un oggetto per uno strato superiore, devi fornire di tutte le dipendenze dei livelli sottostanti. Come esempio concreto, per creare una un'auto reale potrebbe servirti un motore, una trasmissione, un telaio e altre parti; e un motore a sua volta ha bisogno di cilindri e candele.
Quando non sei in grado di costruire le dipendenze prima di passarle, ad esempio Ad esempio, quando si utilizzano le inizializzazioni lazy o la definizione dell'ambito degli oggetti nei flussi devi scrivere e gestire un container personalizzato (o un grafico dipendenze) che gestisce la durata delle dipendenze in memoria.
Esistono librerie che risolvono questo problema automatizzando il processo creando e fornendo le dipendenze. Questi rientrano in due categorie:
Soluzioni basate sulla riflessione che connettono le dipendenze in fase di runtime.
Soluzioni statiche che generano codice per connettere le dipendenze al momento della compilazione.
Dagger è una popolare libreria di inserimento delle dipendenze per Java, Kotlin e Android gestito da Google. Dagger facilita l'uso di DI nella tua app creando e gestendo il grafico delle dipendenze per te. it fornisce dipendenze completamente statiche e in fase di compilazione che gestiscono molte problemi di sviluppo e prestazioni delle soluzioni basate sulla riflessione, come Guice.
Alternative all'inserimento delle dipendenze
Un'alternativa all'inserimento delle dipendenze è l'utilizzo di Service Locator. Anche il pattern di progettazione di Service Locator migliora il disaccoppiamento delle classi da dipendenze concrete. Tu crei un corso noto come service locator che crea e archivia le dipendenze, fornisce queste dipendenze 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 di localizzazione del servizio è diverso dall'inserimento delle dipendenze nel modo consumati dagli elementi. Con il modello Service Locator, le classi hanno controllare e chiedere che gli oggetti vengano inseriti; con l'inserimento delle dipendenze, l'app dispone del controllo e inserisce in modo proattivo gli oggetti richiesti.
Rispetto all'inserimento delle dipendenze:
La raccolta delle dipendenze richieste da un Service locator rende il codice più difficile da testare perché tutti i test devono interagire con lo stesso Service locator.
Le dipendenze sono codificate nell'implementazione della classe, non nell'area API. Di conseguenza, è più difficile sapere di cosa ha bisogno una classe dall'esterno. Di conseguenza, le modifiche
Car
o le dipendenze disponibili nel Service locator potrebbero causare un runtime o un test errori causando errori nei riferimenti.La gestione delle durate degli oggetti è più difficile se vuoi definire l'ambito a elementi diversi dalla durata dell'intera app.
Utilizzare Hilt nella tua app per Android
Hilt è la soluzione consigliata da Jetpack per l'inserimento delle dipendenze in Android. Hilt definisce un metodo standard nell'applicazione fornendo container per ogni classe Android nel tuo progetto e la gestione automatica dei rispettivi cicli di vita.
Hilt si basa sulla famosa libreria DI Dagger per trarre vantaggio la correttezza del tempo di compilazione, le prestazioni di runtime, la scalabilità e Android Studio il supporto offerto da Dagger.
Per scoprire di più su Hilt, visita il sito Inserimento delle dipendenze con Hilt.
Conclusione
L'inserimento delle dipendenze offre i seguenti vantaggi alla tua app:
Riutilizzabilità delle classi e disaccoppiamento delle dipendenze: è più facile scambiarle le implementazioni di una dipendenza. Il riutilizzo del codice è migliorato grazie all'inversione e le classi non controllano più il modo in cui vengono create le loro dipendenze, ma funzionano con qualsiasi configurazione.
Facilità di refactoring: le dipendenze diventano una parte verificabile dell'API superficie, quindi possono essere controllati al momento della creazione dell'oggetto o al momento della compilazione anziché essere nascosti come dettagli dell'implementazione.
Facilità di test: una classe non gestisce le sue dipendenze, quindi quando testarlo, puoi passare varie implementazioni per testare tutti i diversi casi.
Per comprendere appieno i vantaggi dell'inserimento di dipendenze, dovresti provare manualmente nell'app, come mostrato in Inserimento manuale delle dipendenze.
Risorse aggiuntive
Per scoprire di più sull'inserimento delle dipendenze, consulta le risorse aggiuntive che seguono.