Menggunakan Dagger di aplikasi Android

Halaman Dasar-dasar Dagger menjelaskan cara Dagger dapat membantu Anda mengotomatiskan injeksi dependensi dalam aplikasi Anda. Dengan Dagger, Anda tidak perlu menuliskan kode boilerplate yang sangat panjang dan rawan error.

Ringkasan praktik terbaik

  • Gunakan injeksi konstruktor dengan @Inject untuk menambahkan jenis ke grafik Dagger jika memungkinkan. Jika tidak:
    • Gunakan @Binds untuk memberi tahu Dagger tentang implementasi yang harus dimiliki antarmuka.
    • Gunakan @Provides untuk memberi tahu Dagger cara menyediakan class yang tidak dimiliki project Anda.
  • Anda hanya boleh mendeklarasikan modul satu kali dalam sebuah komponen.
  • Beri nama anotasi cakupan sesuai dengan masa aktif saat anotasi digunakan. Contohnya meliputi @ApplicationScope, @LoggedUserScope, dan @ActivityScope.

Menambahkan dependensi

Untuk menggunakan Dagger dalam project, tambahkan dependensi ini pada aplikasi Anda di file build.gradle. Anda dapat menemukan versi terbaru Dagger dalam project GitHub ini.

Kotlin

plugins {
  id 'kotlin-kapt'
}

dependencies {
    implementation 'com.google.dagger:dagger:2.x'
    kapt 'com.google.dagger:dagger-compiler:2.x'
}

Java

dependencies {
    implementation 'com.google.dagger:dagger:2.x'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.x'
}

Dagger di Android

Pertimbangkan contoh aplikasi Android dengan grafik dependensi dari Gambar 1.

LoginActivity bergantung pada LoginViewModel, yang bergantung pada UserRepository,
  yang bergantung pada UserLocalDataSource dan UserRemoteDataSource, yang pada gilirannya akan
  bergantung pada Retrofit.

Gambar 1. Grafik dependensi dalam contoh kode

Di Android, Anda biasanya membuat grafik Dagger yang ada dalam class aplikasi karena Anda ingin instance dari grafik berada dalam memori selama aplikasi berjalan. Dengan cara ini, grafik disertakan ke siklus proses aplikasi. Dalam beberapa kasus, Anda mungkin juga ingin memiliki konteks aplikasi yang tersedia dalam grafik. Untuk itu, Anda juga perlu menempatkan grafik dalam class Application. Satu keuntungan dari pendekatan ini adalah bahwa grafik tersedia untuk class framework Android lainnya. Selain itu, pendekatan ini menyederhanakan pengujian dengan mengizinkan Anda menggunakan class Application kustom dalam pengujian.

Karena antarmuka yang menghasilkan grafik dianotasi dengan @Component, Anda dapat menyebutnya ApplicationComponent atau ApplicationGraph. Anda biasanya menyimpan instance komponen tersebut dalam class Application kustom dan memanggilnya setiap kali membutuhkan grafik aplikasi, seperti yang ditampilkan dalam cuplikan kode berikut:

Kotlin

// Definition of the Application graph
@Component
interface ApplicationComponent { ... }

// appComponent lives in the Application class to share its lifecycle
class MyApplication: Application() {
    // Reference to the application graph that is used across the whole app
    val appComponent = DaggerApplicationComponent.create()
}

Java

// Definition of the Application graph
@Component
public interface ApplicationComponent {
}

// appComponent lives in the Application class to share its lifecycle
public class MyApplication extends Application {

    // Reference to the application graph that is used across the whole app
    ApplicationComponent appComponent = DaggerApplicationComponent.create();
}

Karena class framework Android tertentu, seperti aktivitas dan fragmen, dibuat instance-nya oleh sistem, Dagger tidak dapat membuatnya untuk Anda. Khusus untuk aktivitas, setiap kode inisialisasi harus dimasukkan ke metode onCreate(). Artinya, Anda tidak dapat menggunakan anotasi @Inject dalam konstruktor class (injeksi konstruktor) seperti yang Anda lakukan dalam contoh sebelumnya. Sebagai gantinya, Anda harus menggunakan injeksi kolom.

Anda ingin agar Dagger mengisi dependensi tersebut untuk Anda, bukan membuat sendiri dependensi yang diperlukan aktivitas dalam metode onCreate(). Untuk injeksi kolom, Anda dapat menerapkan anotasi @Inject ke kolom yang ingin Anda dapatkan dari grafik Dagger.

Kotlin

class LoginActivity: Activity() {
    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject lateinit var loginViewModel: LoginViewModel
}

Java

public class LoginActivity extends Activity {

    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject
    LoginViewModel loginViewModel;
}

Untuk mempermudah, LoginViewModel bukanlah ViewModel Komponen Arsitektur Android. Ini hanya class reguler yang berfungsi sebagai ViewModel. Untuk informasi selengkapnya tentang cara menginjeksikan class ini, lihat kode dalam Implementasi Dagger Cetak Biru Android yang resmi, di cabang dev-dagger.

Salah satu pertimbangan dengan Dagger adalah kolom yang diinjeksikan tidak dapat bersifat pribadi. Kolom tersebut harus memiliki setidaknya visibilitas paket pribadi seperti dalam kode sebelumnya.

Memasukkan aktivitas

Dagger perlu memahami bahwa LoginActivity harus mengakses grafik agar dapat menyediakan ViewModel yang dibutuhkan. Dalam halaman Dasar-dasar Dagger Anda menggunakan antarmuka @Component untuk mendapatkan objek dari grafik dengan cara memaparkan fungsi dengan jenis nilai yang ditampilkan dari sesuatu yang ingin Anda dapatkan dari grafik tersebut. Dalam hal ini, Anda perlu memberi tahu Dagger tentang objek (LoginActivity dalam kasus ini) yang memerlukan dependensi untuk diinjeksikan. Untuk itu, Anda mengekspos fungsi yang menggunakan objek yang meminta injeksi sebagai parameter.

Kotlin

@Component
interface ApplicationComponent {
    // This tells Dagger that LoginActivity requests injection so the graph needs to
    // satisfy all the dependencies of the fields that LoginActivity is requesting.
    fun inject(activity: LoginActivity)
}

Java

@Component
public interface ApplicationComponent {
    // This tells Dagger that LoginActivity requests injection so the graph needs to
    // satisfy all the dependencies of the fields that LoginActivity is injecting.
    void inject(LoginActivity loginActivity);
}

Fungsi ini memberi tahu Dagger bahwa LoginActivity ingin mengakses grafik dan meminta injeksi. Dagger harus memenuhi semua dependensi yang diperlukan LoginActivity (LoginViewModel dengan dependensinya sendiri). Jika Anda memiliki beberapa class yang meminta injeksi, Anda harus secara khusus mendeklarasikan semuanya dalam komponen dengan jenis persisnya. Misalnya, jika Anda memiliki LoginActivity dan RegistrationActivity yang meminta injeksi, Anda akan memiliki dua metode inject(), bukan metode generik yang mencakup kedua kasus tersebut. Metode inject() generik tidak memberi tahu Dagger apa yang perlu disediakan. Fungsi dalam antarmuka dapat bernama apa saja, tetapi memanggilnya inject() saat menerima objek untuk diinjeksikan sebagai parameter merupakan suatu konvensi dalam Dagger.

Untuk memasukkan objek dalam aktivitas, Anda perlu menggunakan appComponent yang ditetapkan dalam class Application Anda dan memanggil metode inject(), meneruskan instance aktivitas yang meminta injeksi.

Saat menggunakan aktivitas, injeksikan Dagger dalam metode onCreate() aktivitas sebelum memanggil super.onCreate() untuk menghindari masalah pemulihan fragmen. Selama fase pemulihan dalam super.onCreate(), aktivitas akan menyertakan fragmen yang mungkin ingin mengakses binding aktivitas.

Saat menggunakan fragmen, injeksikan Dagger dalam metode onAttach() fragmen. Dalam hal ini, hal tersebut dapat dilakukan sebelum atau setelah memanggil super.onAttach().

Kotlin

class LoginActivity: Activity() {
    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        // Make Dagger instantiate @Inject fields in LoginActivity
        (applicationContext as MyApplication).appComponent.inject(this)
        // Now loginViewModel is available

        super.onCreate(savedInstanceState)
    }
}

// @Inject tells Dagger how to create instances of LoginViewModel
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

Java

public class LoginActivity extends Activity {

    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Make Dagger instantiate @Inject fields in LoginActivity
        ((MyApplication) getApplicationContext()).appComponent.inject(this);
        // Now loginViewModel is available

        super.onCreate(savedInstanceState);
    }
}

public class LoginViewModel {

    private final UserRepository userRepository;

    // @Inject tells Dagger how to create instances of LoginViewModel
    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

Beri tahu Dagger tentang cara menyediakan sisa dependensi untuk membuat grafik:

Kotlin

class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor(
    private val loginService: LoginRetrofitService
) { ... }

Java

public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

public class UserLocalDataSource {

    @Inject
    public UserLocalDataSource() {}
}

public class UserRemoteDataSource {

    private final LoginRetrofitService loginRetrofitService;

    @Inject
    public UserRemoteDataSource(LoginRetrofitService loginRetrofitService) {
        this.loginRetrofitService = loginRetrofitService;
    }
}

Modul dagger

Untuk contoh ini, Anda menggunakan library jaringan Retrofit. UserRemoteDataSource memiliki dependensi di LoginRetrofitService. Akan tetapi, cara untuk membuat instance LoginRetrofitService berbeda dengan yang Anda lakukan hingga saat ini. Ini bukan pembuatan instance class, tetapi merupakan hasil memanggil Retrofit.Builder() dan meneruskan parameter yang berbeda untuk mengonfigurasi layanan login.

Selain anotasi @Inject, ada cara lain untuk memberi tahu Dagger tentang cara menyediakan instance suatu class: informasi di dalam modul Dagger. Modul Dagger adalah class yang dianotasi dengan @Module. Di sana, Anda dapat menentukan dependensi dengan anotasi @Provides.

Kotlin

// @Module informs Dagger that this class is a Dagger Module
@Module
class NetworkModule {

    // @Provides tell Dagger how to create instances of the type that this function
    // returns (i.e. LoginRetrofitService).
    // Function parameters are the dependencies of this type.
    @Provides
    fun provideLoginRetrofitService(): LoginRetrofitService {
        // Whenever Dagger needs to provide an instance of type LoginRetrofitService,
        // this code (the one inside the @Provides method) is run.
        return Retrofit.Builder()
                .baseUrl("https://example.com")
                .build()
                .create(LoginService::class.java)
    }
}

Java

// @Module informs Dagger that this class is a Dagger Module
@Module
public class NetworkModule {

    // @Provides tell Dagger how to create instances of the type that this function
    // returns (i.e. LoginRetrofitService).
    // Function parameters are the dependencies of this type.
    @Provides
    public LoginRetrofitService provideLoginRetrofitService() {
        // Whenever Dagger needs to provide an instance of type LoginRetrofitService,
        // this code (the one inside the @Provides method) is run.
        return new Retrofit.Builder()
                .baseUrl("https://example.com")
                .build()
                .create(LoginService.class);
    }
}

Modul adalah cara untuk merangkum informasi tentang cara menyediakan objek secara semantik. Seperti yang Anda lihat, Anda memanggil class NetworkModule untuk mengelompokkan logika penyediaan objek yang berkaitan dengan jaringan. Jika aplikasi diperluas, Anda juga dapat menambahkan cara untuk menyediakan OkHttpClient di sini, atau cara untuk mengonfigurasi Gson atau Moshi.

Dependensi metode @Provides adalah parameter metode tersebut. Untuk metode sebelumnya, LoginRetrofitService dapat disediakan tanpa dependensi karena metode tersebut tidak memiliki parameter. Jika Anda telah mendeklarasikan OkHttpClient sebagai suatu parameter, Dagger harus menyediakan instance OkHttpClient dari grafik untuk memenuhi dependensi LoginRetrofitService. Contoh:

Kotlin

@Module
class NetworkModule {
    // Hypothetical dependency on LoginRetrofitService
    @Provides
    fun provideLoginRetrofitService(
        okHttpClient: OkHttpClient
    ): LoginRetrofitService { ... }
}

Java

@Module
public class NetworkModule {

    @Provides
    public LoginRetrofitService provideLoginRetrofitService(OkHttpClient okHttpClient) {
        ...
    }
}

Agar grafik Dagger mengetahui tentang modul ini, Anda harus menambahkannya ke antarmuka @Component sebagai berikut:

Kotlin

// The "modules" attribute in the @Component annotation tells Dagger what Modules
// to include when building the graph
@Component(modules = [NetworkModule::class])
interface ApplicationComponent {
    ...
}

Java

// The "modules" attribute in the @Component annotation tells Dagger what Modules
// to include when building the graph
@Component(modules = NetworkModule.class)
public interface ApplicationComponent {
    ...
}

Cara yang disarankan untuk menambahkan jenis ke grafik Dagger adalah dengan menggunakan injeksi konstruktor (misalnya dengan anotasi @Inject pada konstruktor class). Terkadang hal ini tidak mungkin dilakukan sehingga Anda harus menggunakan modul Dagger. Salah satu contohnya adalah saat Anda ingin agar Dagger menggunakan hasil komputasi untuk menentukan cara membuat instance sebuah objek. Setiap kali harus menyediakan instance dari jenis tersebut, Dagger menjalankan kode di dalam metode @Provides.

Berikut adalah tampilan grafik Dagger dalam contoh saat ini:

Diagram grafik dependensi LoginActivity

Gambar 2. Representasi grafik dengan LoginActivity yang diinjeksikan oleh Dagger

Titik entri ke grafik tersebut adalah LoginActivity. Karena LoginActivity menginjeksikan LoginViewModel, Dagger membangun grafik yang mengetahui cara menyediakan instance LoginViewModel, dan secara rekursif, dari dependensinya. Dagger mengetahui cara melakukannya karena anotasi @Inject pada konstruktor class.

Di dalam ApplicationComponent yang dibuat oleh Dagger, ada metode jenis factory untuk mendapatkan instance dari semua class yang diketahui cara penyediaannya. Dalam contoh ini, Dagger mendelegasikan ke NetworkModule yang disertakan dalam ApplicationComponent untuk mendapatkan instance LoginRetrofitService.

Cakupan dagger

Cakupan disebutkan pada halaman Dasar-dasar Dagger sebagai cara agar memiliki instance unik dari suatu jenis dalam komponen. Inilah yang dimaksud dengan mencakupkan jenis pada siklus proses komponen.

Karena Anda mungkin ingin menggunakan UserRepository dalam fitur lain aplikasi dan mungkin tidak ingin membuat objek baru setiap kali membutuhkannya, Anda dapat menetapkannya sebagai instance unik untuk seluruh aplikasi. Sama halnya dengan LoginRetrofitService. Pembuatannya mungkin akan mahal, dan sebaiknya instance unik objek tersebut dapat digunakan kembali. Membuat instance UserRemoteDataSource tidak semahal itu, sehingga pencakupannya pada siklus proses komponen tidak diperlukan.

@Singleton adalah satu-satunya anotasi cakupan yang disertakan dengan paket javax.inject. Anda dapat menggunakannya untuk menganotasi ApplicationComponent dan objek yang ingin Anda gunakan kembali di seluruh aplikasi.

Kotlin

@Singleton
@Component(modules = [NetworkModule::class])
interface ApplicationComponent {
    fun inject(activity: LoginActivity)
}

@Singleton
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

@Module
class NetworkModule {
    // Way to scope types inside a Dagger Module
    @Singleton
    @Provides
    fun provideLoginRetrofitService(): LoginRetrofitService { ... }
}

Java

@Singleton
@Component(modules = NetworkModule.class)
public interface ApplicationComponent {
    void inject(LoginActivity loginActivity);
}

@Singleton
public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

@Module
public class NetworkModule {

    @Singleton
    @Provides
    public LoginRetrofitService provideLoginRetrofitService() { ... }
}

Hati-hati agar tidak menimbulkan kebocoran memori saat menerapkan cakupan ke objek. Selama komponen cakupan ada dalam memori, objek yang dibuat juga ada dalam memori. Karena ApplicationComponent dibuat saat aplikasi diluncurkan (dalam class Application), objek juga akan dihancurkan ketika aplikasi dihancurkan. Jadi, instance unik UserRepository selalu ada di memori hingga aplikasi dihancurkan.

Subkomponen Dagger

Jika alur login Anda (yang dikelola oleh satu LoginActivity) terdiri dari beberapa fragmen, Anda harus menggunakan kembali instance LoginViewModel yang sama di semua fragmen. @Singleton tidak dapat menganotasi LoginViewModel untuk menggunakan kembali instance karena alasan berikut:

  1. Instance LoginViewModel akan tetap ada dalam memori setelah alur selesai.

  2. Sebaiknya Anda menggunakan instance yang berbeda dari LoginViewModel untuk setiap alur login. Misalnya, jika pengguna logout, sebaiknya gunakan instance LoginViewModel yang berbeda, bukan instance yang sama seperti saat pengguna login untuk pertama kalinya.

Untuk mencakupkan LoginViewModel ke siklus proses LoginActivity, Anda harus membuat komponen baru (subgrafik baru) untuk alur login dan cakupan yang baru.

Mari kita buat grafik khusus untuk alur login.

Kotlin

@Component
interface LoginComponent {}

Java

@Component
public interface LoginComponent {
}

Sekarang, LoginActivity harus mendapatkan injeksi dari LoginComponent karena memiliki konfigurasi khusus login. Tindakan ini akan menghapus tanggung jawab untuk menginjeksikan LoginActivity dari class ApplicationComponent.

Kotlin

@Component
interface LoginComponent {
    fun inject(activity: LoginActivity)
}

Java

@Component
public interface LoginComponent {
    void inject(LoginActivity loginActivity);
}

LoginComponent harus dapat mengakses objek dari ApplicationComponent karena LoginViewModel bergantung pada UserRepository. Cara memberi tahu Dagger bahwa Anda ingin komponen baru menggunakan bagian komponen lain adalah dengan subkomponen Dagger. Komponen baru harus merupakan subkomponen dari komponen yang berisi resource bersama.

Subkomponen adalah komponen yang mewarisi dan memperluas grafik objek dari komponen induk. Jadi, semua objek yang disediakan di komponen induk juga disediakan dalam subkomponen. Dengan cara ini, objek dari subkomponen dapat bergantung pada objek yang disediakan oleh komponen induk.

Untuk membuat instance subkomponen, Anda memerlukan instance dari komponen induk. Oleh karena itu, objek yang disediakan oleh komponen induk ke subkomponen masih tercakup pada komponen induk.

Dalam contoh ini, Anda harus menentukan LoginComponent sebagai subkomponen dari ApplicationComponent. Untuk melakukannya, anotasikan LoginComponent dengan @Subcomponent:

Kotlin

// @Subcomponent annotation informs Dagger this interface is a Dagger Subcomponent
@Subcomponent
interface LoginComponent {

    // This tells Dagger that LoginActivity requests injection from LoginComponent
    // so that this subcomponent graph needs to satisfy all the dependencies of the
    // fields that LoginActivity is injecting
    fun inject(loginActivity: LoginActivity)
}

Java

// @Subcomponent annotation informs Dagger this interface is a Dagger Subcomponent
@Subcomponent
public interface LoginComponent {

    // This tells Dagger that LoginActivity requests injection from LoginComponent
    // so that this subcomponent graph needs to satisfy all the dependencies of the
    // fields that LoginActivity is injecting
    void inject(LoginActivity loginActivity);
}

Anda juga harus menentukan factory subkomponen di dalam LoginComponent sehingga ApplicationComponent mengetahui cara membuat instance LoginComponent.

Kotlin

@Subcomponent
interface LoginComponent {

    // Factory that is used to create instances of this subcomponent
    @Subcomponent.Factory
    interface Factory {
        fun create(): LoginComponent
    }

    fun inject(loginActivity: LoginActivity)
}

Java

@Subcomponent
public interface LoginComponent {

    // Factory that is used to create instances of this subcomponent
    @Subcomponent.Factory
    interface Factory {
        LoginComponent create();
    }

    void inject(LoginActivity loginActivity);
}

Untuk memberi tahu Dagger bahwa LoginComponent adalah subkomponen ApplicationComponent, Anda harus menunjukkannya dengan:

  1. Membuat modul Dagger baru (misalnya SubcomponentsModule) yang meneruskan class subkomponen ke atribut subcomponents anotasi.

    Kotlin

    // The "subcomponents" attribute in the @Module annotation tells Dagger what
    // Subcomponents are children of the Component this module is included in.
    @Module(subcomponents = LoginComponent::class)
    class SubcomponentsModule {}
    

    Java

    // The "subcomponents" attribute in the @Module annotation tells Dagger what
    // Subcomponents are children of the Component this module is included in.
    @Module(subcomponents = LoginComponent.class)
    public class SubcomponentsModule {
    }
    
  2. Menambahkan modul baru (misalnya SubcomponentsModule) ke ApplicationComponent:

    Kotlin

    // Including SubcomponentsModule, tell ApplicationComponent that
    // LoginComponent is its subcomponent.
    @Singleton
    @Component(modules = [NetworkModule::class, SubcomponentsModule::class])
    interface ApplicationComponent {
    }
    

    Java

    // Including SubcomponentsModule, tell ApplicationComponent that
    // LoginComponent is its subcomponent.
    @Singleton
    @Component(modules = {NetworkModule.class, SubcomponentsModule.class})
    public interface ApplicationComponent {
    }
    

    Perhatikan bahwa ApplicationComponent tidak perlu menginjekikan LoginActivity lagi karena tanggung jawab tersebut sekarang milik LoginComponent, jadi Anda dapat menghapus metode inject() dari ApplicationComponent.

    Konsumen ApplicationComponent perlu mengetahui cara membuat instance LoginComponent. Komponen induk harus menambahkan metode dalam antarmukanya untuk memungkinkan konsumen membuat instance subkomponen dari instance komponen induk:

  3. Ekspos factory yang membuat instance LoginComponent dalam antarmuka:

    Kotlin

    @Singleton
    @Component(modules = [NetworkModule::class, SubcomponentsModule::class])
    interface ApplicationComponent {
    // This function exposes the LoginComponent Factory out of the graph so consumers
    // can use it to obtain new instances of LoginComponent
    fun loginComponent(): LoginComponent.Factory
    }
    

    Java

    @Singleton
    @Component(modules = { NetworkModule.class, SubcomponentsModule.class} )
    public interface ApplicationComponent {
    // This function exposes the LoginComponent Factory out of the graph so consumers
    // can use it to obtain new instances of LoginComponent
    LoginComponent.Factory loginComponent();
    }
    

Menetapkan cakupan ke subkomponen

Jika Anda membuat project, Anda dapat membuat instance dari ApplicationComponent dan LoginComponent. ApplicationComponent disertakan ke siklus proses aplikasi karena Anda ingin menggunakan instance grafik yang sama selama aplikasi tersebut berada dalam memori.

Apa yang dimaksud dengan siklus proses LoginComponent? Salah satu alasan mengapa Anda membutuhkan LoginComponent adalah karena Anda harus berbagi instance LoginViewModel yang sama antar-fragmen yang berkaitan dengan Login. Namun, sebaiknya gunakan instance LoginViewModel yang berbeda setiap kali ada alur login baru. LoginActivity adalah masa aktif yang tepat untuk LoginComponent: untuk setiap aktivitas baru, Anda memerlukan instance LoginComponent baru dan fragmen yang dapat menggunakan instance LoginComponent tersebut.

Karena LoginComponent disertakan pada siklus proses LoginActivity, Anda harus menyimpan referensi ke komponen dalam aktivitas tersebut dengan cara yang sama seperti Anda menyimpan referensi ke applicationComponent dalam class Application. Dengan demikian, fragmen dapat mengaksesnya.

Kotlin

class LoginActivity: Activity() {
    // Reference to the Login graph
    lateinit var loginComponent: LoginComponent
    ...
}

Java

public class LoginActivity extends Activity {

    // Reference to the Login graph
    LoginComponent loginComponent;

    ...
}

Perhatikan bahwa variabel loginComponent tidak dianotasi dengan @Inject karena Anda tidak mengharapkan variabel tersebut disediakan oleh Dagger.

Anda dapat menggunakan ApplicationComponent untuk mendapatkan referensi ke LoginComponent, lalu menginjeksikan LoginActivity seperti berikut ini:

Kotlin

class LoginActivity: Activity() {
    // Reference to the Login graph
    lateinit var loginComponent: LoginComponent

    // Fields that need to be injected by the login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        // Creation of the login graph using the application graph
        loginComponent = (applicationContext as MyDaggerApplication)
                            .appComponent.loginComponent().create()

        // Make Dagger instantiate @Inject fields in LoginActivity
        loginComponent.inject(this)

        // Now loginViewModel is available

        super.onCreate(savedInstanceState)
    }
}

Java

public class LoginActivity extends Activity {

    // Reference to the Login graph
    LoginComponent loginComponent;

    // Fields that need to be injected by the login graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Creation of the login graph using the application graph
        loginComponent = ((MyApplication) getApplicationContext())
                                .appComponent.loginComponent().create();

        // Make Dagger instantiate @Inject fields in LoginActivity
        loginComponent.inject(this);

        // Now loginViewModel is available

        super.onCreate(savedInstanceState);
    }
}

LoginComponent dibuat dalam metode onCreate() aktivitas, dan akan secara implisit dimusnahkan saat aktivitas dihapus.

LoginComponent harus selalu menyediakan instance LoginViewModel yang sama setiap kali diminta. Anda dapat memastikan hal ini dengan membuat cakupan anotasi kustom dan menganotasi LoginComponent dan LoginViewModel dengannya. Perlu diperhatikan bahwa Anda tidak dapat menggunakan anotasi @Singleton karena sudah digunakan oleh komponen induk dan tindakan tersebut akan membuat objek menjadi singleton aplikasi (instance unik untuk seluruh aplikasi). Anda perlu membuat cakupan anotasi yang berbeda.

Dalam hal ini, Anda dapat menyebut cakupan ini @LoginScope, tetapi ini bukan hal yang disarankan. Nama anotasi cakupan tidak boleh secara eksplisit menyatakan tujuan yang harus dipenuhinya. Sebaliknya, anotasi tersebut harus diberi nama sesuai masa aktifnya karena anotasi dapat digunakan kembali oleh komponen yang setara seperti RegistrationComponent dan SettingsComponent. Itu sebabnya Anda harus menyebutnya @ActivityScope, bukan @LoginScope.

Kotlin

// Definition of a custom scope called ActivityScope
@Scope
@Retention(value = AnnotationRetention.RUNTIME)
annotation class ActivityScope

// Classes annotated with @ActivityScope are scoped to the graph and the same
// instance of that type is provided every time the type is requested.
@ActivityScope
@Subcomponent
interface LoginComponent { ... }

// A unique instance of LoginViewModel is provided in Components
// annotated with @ActivityScope
@ActivityScope
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

Java

// Definition of a custom scope called ActivityScope
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityScope {}

// Classes annotated with @ActivityScope are scoped to the graph and the same
// instance of that type is provided every time the type is requested.
@ActivityScope
@Subcomponent
public interface LoginComponent { ... }

// A unique instance of LoginViewModel is provided in Components
// annotated with @ActivityScope
@ActivityScope
public class LoginViewModel {

    private final UserRepository userRepository;

    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

Sekarang, jika Anda memiliki dua fragmen yang memerlukan LoginViewModel, keduanya disediakan dengan instance yang sama. Misalnya, jika Anda memiliki LoginUsernameFragment dan LoginPasswordFragment, keduanya harus diinjeksikan oleh LoginComponent:

Kotlin

@ActivityScope
@Subcomponent
interface LoginComponent {

    @Subcomponent.Factory
    interface Factory {
        fun create(): LoginComponent
    }

    // All LoginActivity, LoginUsernameFragment and LoginPasswordFragment
    // request injection from LoginComponent. The graph needs to satisfy
    // all the dependencies of the fields those classes are injecting
    fun inject(loginActivity: LoginActivity)
    fun inject(usernameFragment: LoginUsernameFragment)
    fun inject(passwordFragment: LoginPasswordFragment)
}

Java

@ActivityScope
@Subcomponent
public interface LoginComponent {

    @Subcomponent.Factory
    interface Factory {
        LoginComponent create();
    }

    // All LoginActivity, LoginUsernameFragment and LoginPasswordFragment
    // request injection from LoginComponent. The graph needs to satisfy
    // all the dependencies of the fields those classes are injecting
    void inject(LoginActivity loginActivity);
    void inject(LoginUsernameFragment loginUsernameFragment);
    void inject(LoginPasswordFragment loginPasswordFragment);
}

Komponen ini mengakses instance komponen yang berada dalam objek LoginActivity. Contoh kode untuk LoginUserNameFragment muncul dalam cuplikan kode berikut:

Kotlin

class LoginUsernameFragment: Fragment() {

    // Fields that need to be injected by the login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onAttach(context: Context) {
        super.onAttach(context)

        // Obtaining the login graph from LoginActivity and instantiate
        // the @Inject fields with objects from the graph
        (activity as LoginActivity).loginComponent.inject(this)

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

Java

public class LoginUsernameFragment extends Fragment {

    // Fields that need to be injected by the login graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);

        // Obtaining the login graph from LoginActivity and instantiate
        // the @Inject fields with objects from the graph
        ((LoginActivity) getActivity()).loginComponent.inject(this);

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

Dan hal yang sama berlaku untuk LoginPasswordFragment:

Kotlin

class LoginPasswordFragment: Fragment() {

    // Fields that need to be injected by the login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onAttach(context: Context) {
        super.onAttach(context)

        (activity as LoginActivity).loginComponent.inject(this)

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

Java

public class LoginPasswordFragment extends Fragment {

    // Fields that need to be injected by the login graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);

        ((LoginActivity) getActivity()).loginComponent.inject(this);

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

Gambar 3 menunjukkan bagaimana grafik Dagger terlihat dengan subkomponen baru. Class dengan titik putih (UserRepository, LoginRetrofitService, dan LoginViewModel) adalah class yang memiliki instance unik dan dicakupkan pada masing-masing komponennya.

Grafik Aplikasi setelah menambahkan subkomponen terakhir

Gambar 3. Representasi grafik yang Anda buat untuk contoh aplikasi Android

Mari kita uraikan bagian grafik tersebut:

  1. NetworkModule (sehingga LoginRetrofitService juga) disertakan dalam ApplicationComponent karena Anda menentukannya dalam komponen.

  2. UserRepository tetap berada di ApplicationComponent karena dicakupkan ke ApplicationComponent. Jika project berkembang, Anda ingin berbagi instance yang sama di berbagai fitur (misalnya Pendaftaran).

    Karena UserRepository adalah bagian dari ApplicationComponent, dependensinya (yaitu UserLocalDataSource dan UserRemoteDataSource) juga harus ada dalam komponen ini agar dapat menyediakan instance UserRepository.

  3. LoginViewModel disertakan dalam LoginComponent karena hanya diwajibkan oleh class yang diinjeksikan oleh LoginComponent. LoginViewModel tidak disertakan dalam ApplicationComponent karena tidak ada dependensi dalam ApplicationComponent yang membutuhkan LoginViewModel.

    Demikian pula, jika Anda belum mencakupkan UserRepository ke ApplicationComponent, Dagger akan secara otomatis menyertakan UserRepository dan dependensinya sebagai bagian dari LoginComponent karena itulah satu-satunya tempat UserRepository digunakan.

Selain mencakupkan objek ke siklus proses yang berbeda, membuat subkomponen adalah praktik yang baik untuk mengenkapsulasi berbagai bagian aplikasi Anda dari satu sama lain.

Membuat struktur aplikasi untuk menciptakan subgrafik Dagger yang berbeda sesuai dengan alur aplikasi Anda dapat membantu aplikasi agar menunjukkan performa yang lebih baik dan terukur dalam hal memori dan waktu startup.

Praktik terbaik saat membuat grafik Dagger

Saat membuat grafik Dagger untuk aplikasi Anda:

  • Saat membuat komponen, Anda harus mempertimbangkan elemen apa yang bertanggung jawab terhadap masa aktif komponen tersebut. Dalam hal ini, class Application bertanggung jawab atas ApplicationComponent dan LoginActivity bertanggung jawab atas LoginComponent.

  • Gunakan cakupan hanya jika memungkinkan. Penggunaan cakupan secara berlebihan dapat berdampak negatif pada performa runtime aplikasi Anda: objek berada dalam memori selama komponen berada dalam memori, dan mendapatkan objek cakupan akan menjadi lebih mahal. Ketika Dagger menyediakan objek, objek tersebut akan menggunakan penguncian DoubleCheck, bukan penyedia jenis factory.

Menguji project yang menggunakan Dagger

Salah satu manfaat menggunakan framework injeksi dependensi seperti Dagger adalah karena Dagger menjadikan pengujian kode lebih mudah.

Pengujian unit

Anda tidak perlu menggunakan Dagger untuk pengujian unit. Saat menguji class yang menggunakan injeksi konstruktor, Anda tidak perlu menggunakan Dagger untuk membuat instance class tersebut. Anda dapat langsung memanggil konstruktor yang meneruskan dependensi tiruan atau palsu secara langsung seperti yang Anda lakukan jika dependensi itu tidak dianotasi.

Misalnya, saat menguji LoginViewModel:

Kotlin

@ActivityScope
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

class LoginViewModelTest {

    @Test
    fun `Happy path`() {
        // You don't need Dagger to create an instance of LoginViewModel
        // You can pass a fake or mock UserRepository
        val viewModel = LoginViewModel(fakeUserRepository)
        assertEquals(...)
    }
}

Java

@ActivityScope
public class LoginViewModel {

    private final UserRepository userRepository;

    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

public class LoginViewModelTest {

    @Test
    public void happyPath() {
        // You don't need Dagger to create an instance of LoginViewModel
        // You can pass a fake or mock UserRepository
        LoginViewModel viewModel = new LoginViewModel(fakeUserRepository);
        assertEquals(...);
    }
}

Pengujian menyeluruh

Untuk pengujian integrasi, praktik yang baik adalah membuat TestApplicationComponent yang dimaksudkan untuk pengujian. Produksi dan pengujian menggunakan konfigurasi komponen yang berbeda.

Hal ini memerlukan desain modul yang lebih canggih dalam aplikasi Anda. Komponen pengujian memperluas komponen produksi dan menginstal serangkaian modul yang berbeda.

Kotlin

// TestApplicationComponent extends from ApplicationComponent to have them both
// with the same interface methods. You need to include the modules of the
// component here as well, and you can replace the ones you want to override.
// This sample uses FakeNetworkModule instead of NetworkModule
@Singleton
@Component(modules = [FakeNetworkModule::class, SubcomponentsModule::class])
interface TestApplicationComponent : ApplicationComponent {
}

Java

// TestApplicationComponent extends from ApplicationComponent to have them both
// with the same interface methods. You need to include the modules of the
// Component here as well, and you can replace the ones you want to override.
// This sample uses FakeNetworkModule instead of NetworkModule
@Singleton
@Component(modules = {FakeNetworkModule.class, SubcomponentsModule.class})
public interface TestApplicationComponent extends ApplicationComponent {
}

FakeNetworkModule memiliki implementasi palsu atas NetworkModule yang asli. Di sana, Anda dapat memberikan contoh palsu atau tiruan dari apa pun yang ingin Anda ganti.

Kotlin

// In the FakeNetworkModule, pass a fake implementation of LoginRetrofitService
// that you can use in your tests.
@Module
class FakeNetworkModule {
    @Provides
    fun provideLoginRetrofitService(): LoginRetrofitService {
        return FakeLoginService()
    }
}

Java

// In the FakeNetworkModule, pass a fake implementation of LoginRetrofitService
// that you can use in your tests.
@Module
public class FakeNetworkModule {

    @Provides
    public LoginRetrofitService provideLoginRetrofitService() {
        return new FakeLoginService();
    }
}

Dalam integrasi atau pengujian menyeluruh, Anda akan menggunakan TestApplication yang membuat TestApplicationComponent, bukan ApplicationComponent.

Kotlin

// Your test application needs an instance of the test graph
class MyTestApplication: MyApplication() {
    override val appComponent = DaggerTestApplicationComponent.create()
}

Java

// Your test application needs an instance of the test graph
public class MyTestApplication extends MyApplication {
    ApplicationComponent appComponent = DaggerTestApplicationComponent.create();
}

Kemudian, aplikasi pengujian ini digunakan dalam TestRunner kustom yang akan Anda gunakan untuk menjalankan pengujian instrumentasi. Untuk informasi selengkapnya tentang hal ini, lihat Menggunakan Dagger di codelab aplikasi Android Anda.

Bekerja dengan modul Dagger

Modul Dagger adalah cara untuk mengenkapsulasi cara menyediakan objek secara semantik. Anda dapat menyertakan modul dalam komponen, tetapi Anda juga dapat menyertakan modul di dalam modul lainnya. Metode ini sangat efektif, tetapi dapat dengan mudah disalahgunakan.

Setelah modul ditambahkan ke komponen atau modul lain, modul tersebut sudah ada di grafik Dagger. Dagger dapat menyediakan objek tersebut dalam komponen itu. Sebelum menambahkan modul, periksa apakah modul tersebut sudah menjadi bagian dari grafik Dagger dengan memeriksa apakah modul telah ditambahkan ke komponen atau dengan mengompilasi project dan melihat apakah Dagger dapat menemukan dependensi yang diperlukan untuk modul tersebut.

Praktik yang baik menentukan bahwa modul hanya boleh dideklarasikan satu kali dalam suatu komponen (di luar kasus penggunaan Dagger tingkat lanjut yang spesifik).

Misalkan grafik Anda dikonfigurasi dengan cara ini. ApplicationComponent mencakup Module1 dan Module2, sementara Module1 mencakup ModuleX.

Kotlin

@Component(modules = [Module1::class, Module2::class])
interface ApplicationComponent { ... }

@Module(includes = [ModuleX::class])
class Module1 { ... }

@Module
class Module2 { ... }

Java

@Component(modules = {Module1.class, Module2.class})
public interface ApplicationComponent { ... }

@Module(includes = {ModuleX.class})
public class Module1 { ... }

@Module
public class Module2 { ... }

Jika sekarang Module2 bergantung pada class yang disediakan oleh ModuleX. Praktik yang buruk termasuk ModuleX dalam Module2 karena ModuleX disertakan dua kali dalam grafik seperti yang terlihat pada cuplikan kode berikut:

Kotlin

// Bad practice: ModuleX is declared multiple times in this Dagger graph
@Component(modules = [Module1::class, Module2::class])
interface ApplicationComponent { ... }

@Module(includes = [ModuleX::class])
class Module1 { ... }

@Module(includes = [ModuleX::class])
class Module2 { ... }

Java

// Bad practice: ModuleX is declared multiple times in this Dagger graph.
@Component(modules = {Module1.class, Module2.class})
public interface ApplicationComponent { ... }

@Module(includes = ModuleX.class)
public class Module1 { ... }

@Module(includes = ModuleX.class)
public class Module2 { ... }

Sebagai gantinya, Anda harus melakukan salah satu hal berikut:

  1. Faktorkan ulang modul dan ekstrak modul umum ke komponen.
  2. Buat modul baru dengan objek yang digunakan bersama oleh kedua modul, lalu ekstrak ke komponen.

Tidak melakukan pemfaktoran ulang seperti ini akan menyebabkan banyak modul, termasuk satu sama lain, tidak memiliki organisasi yang jelas dan membuatnya sulit untuk dilihat dari mana asal dependensi masing-masing.

Praktik yang baik (Opsi 1): ModulX dideklarasikan sekali dalam grafik Dagger.

Kotlin

@Component(modules = [Module1::class, Module2::class, ModuleX::class])
interface ApplicationComponent { ... }

@Module
class Module1 { ... }

@Module
class Module2 { ... }

Java

@Component(modules = {Module1.class, Module2.class, ModuleX.class})
public interface ApplicationComponent { ... }

@Module
public class Module1 { ... }

@Module
public class Module2 { ... }

Praktik yang baik (Opsi 2): Dependensi umum dari Module1 dan Module2 dalam ModuleX diekstrak menjadi modul baru bernama ModuleXCommon yang disertakan dalam komponen. Kemudian, dua modul lain bernama ModuleXWithModule1Dependencies dan ModuleXWithModule2Dependencies dibuat dengan dependensi yang spesifik untuk masing-masing modul. Semua modul dideklarasikan satu kali dalam grafik Dagger.

Kotlin

@Component(modules = [Module1::class, Module2::class, ModuleXCommon::class])
interface ApplicationComponent { ... }

@Module
class ModuleXCommon { ... }

@Module
class ModuleXWithModule1SpecificDependencies { ... }

@Module
class ModuleXWithModule2SpecificDependencies { ... }

@Module(includes = [ModuleXWithModule1SpecificDependencies::class])
class Module1 { ... }

@Module(includes = [ModuleXWithModule2SpecificDependencies::class])
class Module2 { ... }

Java

@Component(modules = {Module1.class, Module2.class, ModuleXCommon.class})
public interface ApplicationComponent { ... }

@Module
public class ModuleXCommon { ... }

@Module
public class ModuleXWithModule1SpecificDependencies { ... }

@Module
public class ModuleXWithModule2SpecificDependencies { ... }

@Module(includes = ModuleXWithModule1SpecificDependencies.class)
public class Module1 { ... }

@Module(includes = ModuleXWithModule2SpecificDependencies.class)
public class Module2 { ... }

Injeksi berbantuan

Injeksi berbantuan adalah pola DI yang digunakan untuk membuat objek. Dengan pola ini, beberapa parameter dapat disediakan oleh framework DI sedangkan parameter lainnya harus diteruskan saat pembuatan oleh pengguna.

Di Android, pola ini umum di layar detail tempat id elemen yang ditampilkan hanya diketahui saat runtime, bukan pada waktu kompilasi saat Dagger menghasilkan grafik DI. Untuk mempelajari lebih lanjut injeksi berbantuan dengan Dagger, lihat dokumentasi Dagger.

Kesimpulan

Jika Anda belum melakukannya, baca bagian praktik terbaik. Untuk melihat cara menggunakan Dagger di aplikasi Android, baca bagian Menggunakan Dagger di codelab aplikasi Android.