Iniezione di dipendenze in Android

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:

  1. La classe crea la dipendenza di cui ha bisogno. Nell'esempio precedente, Car crea e inizializza la propria istanza di Engine.
  2. Prendilo da qualche altra parte. Alcune API per Android, come i getter Context e getSystemService(), funzionano in questo modo.
  3. 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 riceverebbe Engine 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

Questo non è un esempio di inserimento delle dipendenze perché la classe Car sta creando la propria classe Engine. Questo può essere problematico perché:

  • Car e Engine sono caratterizzati dall'alto accoppiamento: un'istanza di Car utilizza un solo tipo di Engine e non è facile utilizzare sottoclassi o implementazioni alternative. Se Car dovesse creare il proprio Engine, dovresti creare due tipi di Car invece di riutilizzare lo stesso Car per i motori di tipo Gas e Electric.

  • L'estrema dipendenza da Engine rende i test più difficili. Car utilizza un'istanza reale di Engine, impedendoti così di utilizzare un doppio di test per modificare Engine 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

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 di Engine a Car. Ad esempio, potresti definire una nuova sottoclasse di Engine denominata ElectricEngine che vuoi utilizzare Car. Se utilizzi DI, devi solo passare un'istanza della sottoclasse ElectricEngine aggiornata e Car 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 di Engine chiamato FakeEngine 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.

Samples