Iniezione di dipendenze in Android

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:

  1. La classe crea la dipendenza di cui ha bisogno. Nell'esempio precedente, Car creerà e inizializza la propria istanza di Engine.
  2. Afferralo da un'altra parte. Alcune API Android, come Context getter e getSystemService(), lavorate in molti modi diversi.
  3. 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 riceverebbe Engine 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();
    }
}
Classe auto senza inserimento di dipendenze

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

  • Car e Engine sono caratterizzati dall'alto accoppiamento: un'istanza di Car utilizza una di tipo Engine e nessuna sottoclasse o implementazioni alternative può essere facilmente in uso. 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.

  • La forte dipendenza da Engine rende i test più difficili. Car utilizza un reale di Engine, impedendoti così di utilizzare un'istanza test double per modificare Engine 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();
    }
}
Classe auto con 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. La i vantaggi di questo approccio basato sull'AI sono:

  • Riutilizzabilità di Car. Puoi trasferire diverse implementazioni di Engine a Car. Ad esempio, puoi definire una nuova sottoclasse di Engine chiamata ElectricEngine che vuoi che Car utilizzi. Se usi DI, devi solo fare passa in un'istanza della sottoclasse ElectricEngine aggiornata e Car 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 di Engine chiamato FakeEngine 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.

Campioni