Skip to content
Go back

Dependency Injection vs Inversion of Control in Android: A Practical Guide

DI vs IoC Progression

Two terms appear constantly in Android architecture discussions — Dependency Injection (DI) and Inversion of Control (IoC). They sound similar, they are related, but they solve slightly different problems. The best way to understand both is to watch a single example evolve through three stages.


The Setup: Car, Engine, Motor

Imagine three classes:

Car cannot function without Engine, and Engine cannot function without Motor. That relationship — where one class cannot do its job without another — is called a dependency. Engine is a dependency of Car.


V0 — Hardcoded Dependencies (The Wrong Way)

public class Motor {
    public Motor() {}
}

public class Engine {
    private final Motor motor;
    public Engine() {
        this.motor = new Motor(); // creates dependency internally
    }
}

public class Car {
    private final Engine engine;
    public Car() {
        this.engine = new Engine(); // creates dependency internally
    }
}

To use a Car:

Car car = new Car(); // one line — looks clean

Seems convenient. But there are real problems hiding here:


V1 — Dependency Injection

The fix is straightforward: instead of creating dependencies inside a class, pass them in from outside.

public class Motor {
    public Motor() {}
}

public class Engine {
    private final Motor motor;
    public Engine(Motor motor) { // Motor is injected
        this.motor = motor;
    }
}

public class Car {
    private final Engine engine;
    public Car(Engine engine) { // Engine is injected
        this.engine = engine;
    }
}

The caller is now responsible for assembling the graph:

public void createCar() {
    Motor motor   = new Motor();
    Engine engine = new Engine(motor);
    Car car       = new Car(engine);
}

This is Dependency Injection — the dependencies are provided (injected) from outside the class rather than created inside it.

What improved:

The cost: The caller has to build the whole chain manually. For 3 classes that is fine. For a real project with 20 chained dependencies, it becomes a lot of boilerplate — which is exactly why frameworks like Dagger exist.


V2 — Inversion of Control

V1 still passes concrete classes. If tomorrow you need a TurboEngine instead of Engine, every call site that builds a Car needs to change.

Inversion of Control takes DI one step further: pass an interface, not a class. The class depends on a contract, not an implementation.

public interface Motor {
    void run();
}

public class FastMotor implements Motor {
    public void run() { /* high RPM logic */ }
}

public class SlowMotor implements Motor {
    public void run() { /* economy logic */ }
}

public class Engine {
    private final Motor motor;
    public Engine(Motor motor) { // accepts the interface
        this.motor = motor;
    }
}

public class Car {
    private final Engine engine;
    public Car(Engine engine) {
        this.engine = engine;
    }
}

Now the caller chooses the behaviour:

// High-performance car
Motor motor = new FastMotor();
Engine engine = new Engine(motor);
Car fastCar = new Car(engine);

// Economy car — same Car class, different behaviour
Motor motor = new SlowMotor();
Engine engine = new Engine(motor);
Car slowCar = new Car(engine);

Car and Engine are completely unchanged. You swap behaviour purely by choosing which implementation to inject. This is polymorphism through interfaces, enabled by Inversion of Control.


DI vs IoC — The Exact Difference

Dependency InjectionInversion of Control
What you passA concrete classAn interface
FlexibilityFixed implementationSwap implementations freely
TestingCan inject a subclassCan inject any conforming mock
CouplingLowLowest

Rule of thumb: If you inject a class, it’s DI. If you inject an interface, it’s IoC. IoC is a superset — you cannot have IoC without DI, but you can have DI without IoC.


Why Good Code Means More Lines

One observation worth sitting with: following these principles forces you to write more code. Comparing V0 and V2, the call site grew from one line to four. That trade-off is intentional.

The extra lines buy you:

A DI framework like Dagger automates the boilerplate. It reads annotations on your classes and generates the assembly code — the new Motor(), new Engine(motor), new Car(engine) chain — so you never have to write it by hand. But understanding why that chain needs to exist is essential before letting a framework hide it.


Share this post on:

Previous Post
IO vs Computation Thread Pools — Why Android Has Two and When to Use Each
Next Post
Keep-Alive Time, Multiple Thread Pools, and Why 4 Threads Can Beat 40