Injeksi dependensi pada Android

Injeksi dependensi (DI) adalah teknik yang banyak digunakan dalam pemrograman dan sesuai dengan pengembangan Android. Dengan mengikuti prinsip-prinsip DI, Anda mengatur dasar arsitektur aplikasi yang baik.

Implementasi injeksi dependensi memberikan beberapa keuntungan berikut:

  • Penggunaan kembali kode
  • Kemudahan dalam pemfaktoran ulang
  • Kemudahan dalam pengujian

Dasar-dasar injeksi dependensi

Sebelum membahas injeksi dependensi pada Android secara khusus, halaman ini memberikan ringkasan umum tentang cara kerja injeksi dependensi.

Apa itu injeksi dependensi?

Class sering kali memerlukan referensi ke class lain. Misalnya, class Car mungkin memerlukan referensi ke class Engine. Class wajib ini disebut dependensi, dan dalam contoh ini class Car bergantung pada sebuah instance dari class Engine untuk dijalankan.

Ada tiga cara bagi suatu class untuk mendapatkan objek yang dibutuhkan:

  1. Class menyusun dependensi yang dibutuhkan. Pada contoh di atas, Car akan membuat dan melakukan inisialisasi instance Engine-nya sendiri.
  2. Ambil dari tempat lain. Beberapa API Android, seperti pengambil Context dan getSystemService(), berfungsi seperti ini.
  3. Masukkan sebagai parameter. Aplikasi dapat menyediakan dependensi ini saat class dibuat atau meneruskannya ke fungsi yang memerlukan setiap dependensi. Pada contoh di atas, konstruktor Car akan menerima Engine sebagai parameter.

Opsi ketiga adalah injeksi dependensi! Dengan pendekatan ini, Anda mengambil dependensi class dan menyediakannya, bukan meminta instance class untuk mendapatkannya sendiri.

Berikut contohnya. Tanpa injeksi dependensi, representasi Car yang membuat dependensi Engine-nya sendiri dalam kode akan terlihat seperti ini:

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();
    }
}
Class mobil tanpa injeksi dependensi

Ini bukan contoh injeksi dependensi karena class Car menyusun Engine-nya sendiri. Hal ini dapat menjadi masalah karena:

  • Car dan Engine disandingkan dengan erat - instance Car menggunakan satu jenis Engine, dan tidak ada subclass atau implementasi alternatif yang dapat digunakan dengan mudah. Jika Car menyusun Engine-nya sendiri, Anda harus membuat dua jenis Car, bukan hanya menggunakan kembali Car yang sama untuk mesin berjenis Gas dan Electric.

  • Dependensi keras pada Engine membuat pengujian menjadi lebih sulit. Car menggunakan instance nyata Engine, sehingga Anda tidak perlu menggunakan pengujian ganda untuk mengubah Engine untuk kasus pengujian yang berbeda.

Seperti apa tampilan kode dengan injeksi dependensi? Setiap instance Car tidak menyusun objek Engine-nya sendiri pada saat inisialisasi, tetapi justru menerima objek Engine sebagai parameter dalam konstruktornya:

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();
    }
}
Class mobil menggunakan injeksi dependensi

Fungsi main menggunakan Car. Karena Car bergantung pada Engine, aplikasi akan membuat instance Engine, kemudian menggunakannya untuk membuat instance Car. Manfaat pendekatan berbasis DI ini adalah:

  • Penggunaan kembali Car. Anda dapat meneruskan berbagai implementasi Engine ke Car. Misalnya, Anda dapat menentukan subclass Engine baru yang disebut ElectricEngine dan Anda inginkan agar digunakan oleh Car. Jika menggunakan DI, Anda hanya perlu meneruskan instance subclass ElectricEngine yang diperbarui, dan Car akan tetap berfungsi tanpa adanya perubahan lebih lanjut.

  • Kemudahan pengujian Car. Anda dapat meneruskan pengujian ganda untuk menguji skenario yang berbeda. Misalnya, Anda dapat membuat pengujian ganda Engine yang disebut FakeEngine dan mengonfigurasinya untuk pengujian yang lain.

Ada dua cara utama untuk melakukan injeksi dependensi pada Android:

  • Injeksi Konstruktor. Ini adalah cara yang dideskripsikan di atas. Anda meneruskan dependensi class ke konstruktornya.

  • Injeksi Kolom (atau Injeksi Penyetel). Class framework Android tertentu, seperti aktivitas dan fragmen, dibuat instance-nya oleh sistem, sehingga injeksi konstruktor tidak dapat dilakukan. Dengan injeksi kolom, dependensi akan dibuat instance-nya setelah class dibuat. Kodenya akan terlihat seperti ini:

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();
    }
}

Injeksi dependensi otomatis

Pada contoh sebelumnya, Anda membuat, menyediakan, dan mengelola sendiri dependensi berbagai class, tanpa mengandalkan library. Ini disebut injeksi dependensi tanpa bantuan alat, atau injeksi dependensi manual. Dalam contoh Car, hanya ada satu dependensi. Namun, lebih banyak dependensi dan class dapat membuat injeksi dependensi manual menjadi lebih melelahkan. Injeksi dependensi manual juga mendatangkan beberapa masalah:

  • Untuk aplikasi besar, mengambil semua dependensi dan menghubungkannya dengan benar mungkin akan memerlukan jumlah kode boilerplate yang besar. Dalam arsitektur multi-lapisan, untuk membuat sebuah objek pada lapisan atas, Anda harus menyediakan semua dependensi lapisan di bawahnya. Sebagai contoh konkret, untuk membuat mobil sungguhan, Anda mungkin memerlukan mesin, transmisi, rangka, dan komponen lainnya; dan pada gilirannya, mesin memerlukan silinder dan busi.

  • Saat Anda tidak dapat membuat dependensi sebelum meneruskannya — misalnya ketika menggunakan inisialisasi yang lambat atau mencakup objek ke alur aplikasi — Anda harus menulis dan mempertahankan container kustom (atau grafik dependensi) yang mengelola masa aktif dependensi dalam memori.

Ada library yang dapat memecahkan masalah ini dengan mengotomatiskan proses pembuatan dan penyediaan dependensi. Kedua solusi tersebut sesuai dengan dua kategori:

  • Solusi berbasis refleksi yang menghubungkan dependensi pada runtime.

  • Solusi statis yang menghasilkan kode untuk menghubungkan dependensi pada waktu kompilasi.

Dagger adalah library injeksi dependensi yang populer untuk Java, Kotlin, dan Android yang dikelola oleh Google. Dagger memfasilitasi penggunaan DI pada aplikasi Anda dengan membuat dan mengelola grafik dependensi untuk Anda. Dagger menyediakan dependensi statis sepenuhnya dan waktu kompilasi yang mengatasi berbagai masalah performa dan pengembangan dengan solusi berbasis refleksi seperti Guice.

Alternatif untuk injeksi dependensi

Alternatif untuk injeksi dependensi adalah dengan menggunakan pencari lokasi layanan. Pola desain pencari lokasi layanan juga meningkatkan pemisahan class dari dependensi konkret. Anda membuat class yang dikenal sebagai pencari lokasi layanan yang membuat dan menyimpan dependensi, lalu menyediakan dependensi itu sesuai permintaan.

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();
    }
}

Pola pencari lokasi layanan berbeda dengan injeksi dependensi dalam cara elemen tersebut digunakan. Dengan pola pencari lokasi layanan, class memiliki kontrol dan meminta agar objek diinjeksikan; dengan injeksi dependensi, aplikasi memiliki kontrol dan menginjeksikan objek yang diperlukan secara proaktif.

Dibandingkan dengan injeksi dependensi:

  • Pengumpulan dependensi yang diperlukan oleh pencari lokasi layanan membuat kode lebih sulit diuji karena semua pengujian harus berinteraksi dengan pencari lokasi layanan global yang sama.

  • Dependensi dienkode dalam implementasi class, bukan pada permukaan API. Akibatnya, lebih sulit untuk mengetahui apa yang dibutuhkan oleh class dari luar. Akibatnya, perubahan pada Car atau dependensi yang tersedia pada pencari lokasi layanan dapat menyebabkan kegagalan pengujian atau runtime dengan menyebabkan referensi gagal.

  • Mengelola masa aktif objek akan lebih sulit jika Anda ingin mencakup apa pun selain masa aktif seluruh aplikasi.

Menggunakan Hilt di aplikasi Android Anda

Hilt adalah library yang direkomendasikan Jetpack untuk injeksi dependensi di Android. Hilt mendefinisikan cara standar untuk melakukan DI dalam aplikasi Anda dengan menyediakan container untuk setiap class Android dalam project Anda dan mengelola siklus prosesnya secara otomatis untuk Anda.

Hilt ditambahkan pada Dagger library DI yang populer untuk mendapatkan manfaat dari ketepatan waktu kompilasi, performa runtime, skalabilitas, dan dukungan Android Studio yang disediakan oleh Dagger.

Untuk mempelajari Hilt lebih lanjut, lihat Injeksi Dependensi dengan Hilt.

Kesimpulan

Injeksi dependensi memberi aplikasi Anda keuntungan sebagai berikut:

  • Penggunaan kembali class dan pemisahan dependensi: Lebih mudah untuk menukar implementasi dependensi. Penggunaan kembali kode ditingkatkan karena inversi kontrol, dan class tidak lagi mengontrol cara dependensi dibuat, tetapi berfungsi dengan konfigurasi apa pun.

  • Kemudahan pemfaktoran ulang: Dependensi menjadi bagian permukaan API yang dapat diverifikasi, sehingga dapat diperiksa pada waktu pembuatan objek atau pada waktu kompilasi, bukan disembunyikan sebagai detail implementasi.

  • Kemudahan pengujian: Class tidak mengelola dependensinya, sehingga saat sedang mengujinya, Anda dapat meneruskan berbagai implementasi untuk menguji semua kasus yang berbeda.

Untuk memahami sepenuhnya manfaat injeksi dependensi, Anda harus mencobanya secara manual di aplikasi Anda seperti yang ditunjukkan pada Injeksi dependensi manual.

Referensi lainnya

Untuk mempelajari injeksi dependensi lebih lanjut, lihat referensi tambahan berikut.

Contoh