Menggunakan Dagger di aplikasi multi-modul

Project yang memiliki beberapa modul Gradle dikenal sebagai project multi-modul. Dalam project multi-modul yang dikirim sebagai APK tunggal tanpa modul fitur, memiliki modul app yang dapat bergantung pada sebagian besar modul project Anda dan modul base atau core yang biasa diandalkan oleh modul lainnya merupakan hal yang umum. Modul app biasanya berisi class Application, sedangkan modul base berisi semua class umum yang yang digunakan bersama di semua modul dalam project Anda.

Modul app adalah tempat yang cocok untuk mendeklarasikan komponen aplikasi Anda (misalnya, ApplicationComponent pada gambar di bawah ini) yang dapat menyediakan objek yang mungkin diperlukan komponen lain serta singleton aplikasi Anda. Sebagai contoh, class seperti OkHttpClient, parser JSON, pengakses untuk database Anda, atau objek SharedPreferences yang dapat ditentukan dalam modul core, akan disediakan oleh ApplicationComponent yang ditentukan dalam modul app.

Dalam modul app, Anda juga dapat memiliki komponen lain dengan masa aktif yang lebih pendek. Contohnya dapat berupa UserComponent dengan konfigurasi khusus pengguna (seperti UserSession) setelah login.

Di berbagai modul project, Anda dapat menentukan setidaknya satu subkomponen yang memiliki logika yang khusus untuk modul tersebut seperti yang ditunjukkan dalam gambar 1.

Gambar 1. Contoh grafik Dagger dalam project multi-modul

Misalnya, dalam modul login, Anda dapat memiliki LoginComponent yang dicakupkan dengan anotasi @ModuleScope kustom yang dapat menyediakan objek yang umum untuk fitur tersebut seperti LoginRepository. Di dalam modul tersebut, Anda juga dapat memiliki komponen lain yang bergantung pada LoginComponent dengan cakupan kustom yang berbeda. Misalnya, @FeatureScope untuk LoginActivityComponent atau TermsAndConditionsComponent, yang mana Anda dapat mencakup logika yang lebih mengedepankan fitur seperti objek ViewModel.

Untuk modul lainnya seperti Registration, Anda akan memiliki konfigurasi yang serupa.

Aturan umum untuk project multi-modul adalah bahwa modul di tingkat yang sama tidak boleh saling bergantung satu sama lain. Namun, jika modul saling bergantung, pertimbangkan apakah logika yang digunakan bersama oleh modul (dependensi antara modul tersebut) harus menjadi bagian dari modul induk. Jika demikian, lakukan pemfaktoran ulang untuk memindahkan class ke modul induk; jika tidak, buat modul baru yang memperluas modul induk dan buat kedua modul asli memperluas modul baru.

Sebagai praktik terbaik, Anda umumnya akan membuat komponen di modul dalam kasus berikut:

  • Anda harus melakukan injeksi kolom, seperti pada LoginActivityComponent.

  • Anda harus mencakup objek, seperti pada LoginComponent.

Jika kedua kasus tersebut tidak berlaku dan Anda perlu memberi tahu Dagger cara menyediakan objek dari modul tersebut, buat dan tampilkan modul Dagger dengan metode @Provides atau @Binds jika injeksi konstruksi tidak memungkinkan untuk class tersebut.

Implementasi dengan subkomponen Dagger

Halaman dokumen Menggunakan Dagger di aplikasi Android membahas cara membuat dan menggunakan subkomponen. Namun, Anda tidak dapat menggunakan kode yang sama karena modul fitur tidak mengetahui modul app. Sebagai contoh, jika Anda mempertimbangkan alur Login standar dan kode yang kami miliki di halaman sebelumnya, kode tersebut tidak akan dikompilasi lagi:

Kotlin

class LoginActivity: Activity() {
  ...

  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)
    ...
  }
}

Java

public class LoginActivity extends Activity {
    ...

    @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);

        ...
    }
}

Alasannya adalah karena modul login tidak mengetahui MyApplication atau appComponent. Agar berfungsi, Anda harus menentukan antarmuka dalam modul fitur yang menyediakan FeatureComponent yang perlu diimplementasikan oleh MyApplication.

Pada contoh berikut, Anda dapat menentukan antarmuka LoginComponentProvider yang menyediakan LoginComponent dalam modul login untuk alur Login:

Kotlin

interface LoginComponentProvider {
    fun provideLoginComponent(): LoginComponent
}

Java

public interface LoginComponentProvider {
   public LoginComponent provideLoginComponent();
}

Sekarang, LoginActivity akan menggunakan antarmuka tersebut, bukan cuplikan kode yang ditentukan di atas:

Kotlin

class LoginActivity: Activity() {
  ...

  override fun onCreate(savedInstanceState: Bundle?) {
    loginComponent = (applicationContext as LoginComponentProvider)
                        .provideLoginComponent()

    loginComponent.inject(this)
    ...
  }
}

Java

public class LoginActivity extends Activity {
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        loginComponent = ((LoginComponentProvider) getApplicationContext())
                                .provideLoginComponent();

        loginComponent.inject(this);

        ...
    }
}

Sekarang, MyApplication perlu menerapkan antarmuka tersebut dan menerapkan metode yang diperlukan:

Kotlin

class MyApplication: Application(), LoginComponentProvider {
  // Reference to the application graph that is used across the whole app
  val appComponent = DaggerApplicationComponent.create()

  override fun provideLoginComponent(): LoginComponent {
    return appComponent.loginComponent().create()
  }
}

Java

public class MyApplication extends Application implements LoginComponentProvider {
  // Reference to the application graph that is used across the whole app
  ApplicationComponent appComponent = DaggerApplicationComponent.create();

  @Override
  public LoginComponent provideLoginComponent() {
    return appComponent.loginComponent.create();
  }
}

Berikut cara menggunakan subkomponen Dagger dalam project multi-modul. Dengan modul fitur, solusinya berbeda karena cara modul bergantung satu sama lain.

Dependensi komponen dengan modul fitur

Dengan modul fitur, cara umum modul bergantung satu sama lain akan dibalik. Modul app bukan menyertakan modul fitur, tetapi modul fitur bergantung pada modul app. Lihat gambar 2 untuk representasi cara penyusunan modul.

Gambar 2. Contoh grafik Dagger dalam project yang berisi modul fitur

Dalam Dagger, komponen harus mengetahui subkomponennya. Informasi ini disertakan dalam modul Dagger yang ditambahkan ke komponen induk (seperti modul SubcomponentsModule yang ada di halaman Menggunakan Dagger di aplikasi Android).

Sayangnya, dengan dependensi terbalik antara aplikasi dan modul fitur, subkomponen tidak terlihat dari modul app karena tidak berada dalam jalur build. Sebagai contoh, LoginComponent yang ditentukan dalam modul fitur login tidak boleh menjadi subkomponen dari ApplicationComponent yang ditentukan dalam modul app.

Dagger memiliki mekanisme yang disebut dependensi komponen yang dapat Anda gunakan untuk menyelesaikan masalah ini. Alih-alih komponen turunan menjadi subkomponen dari komponen induk, komponen turunan akan bergantung pada komponen induk. Dengan demikian, tidak ada hubungan induk-turunan; komponen kini bergantung pada komponen lain untuk mendapatkan dependensi tertentu. Komponen harus menampilkan jenis dari grafik agar komponen dependen dapat memakainya.

Misalnya: modul fitur yang disebut login ingin membuat LoginComponent yang bergantung pada AppComponent yang tersedia di modul Gradle app.

Berikut adalah definisi untuk class dan AppComponent yang merupakan bagian dari modul Gradle app:

Kotlin

// UserRepository's dependencies
class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor() { ... }

// UserRepository is scoped to AppComponent
@Singleton
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

@Singleton
@Component
interface AppComponent { ... }

Java

// UserRepository's dependencies
public class UserLocalDataSource {

    @Inject
    public UserLocalDataSource() {}
}

public class UserRemoteDataSource {

    @Inject
    public UserRemoteDataSource() { }
}

// UserRepository is scoped to AppComponent
@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;
    }
}

@Singleton
@Component
public interface ApplicationComponent { ... }

Dalam modul gradle login yang menyertakan modul gradle app, Anda memiliki LoginActivity yang memerlukan instance LoginViewModel untuk diinjeksi:

Kotlin

// LoginViewModel depends on UserRepository that is scoped to AppComponent
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

Java

// LoginViewModel depends on UserRepository that is scoped to AppComponent
public class LoginViewModel {

    private final UserRepository userRepository;

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

LoginViewModel memiliki dependensi pada UserRepository yang tersedia dan tercakup di AppComponent. Mari kita buat LoginComponent yang bergantung pada AppComponent untuk menginjeksi LoginActivity:

Kotlin

// Use the dependencies attribute in the Component annotation to specify the
// dependencies of this Component
@Component(dependencies = [AppComponent::class])
interface LoginComponent {
    fun inject(activity: LoginActivity)
}

Java

// Use the dependencies attribute in the Component annotation to specify the
// dependencies of this Component
@Component(dependencies = AppComponent.class)
public interface LoginComponent {

    void inject(LoginActivity loginActivity);
}

LoginComponent menentukan dependensi pada AppComponent dengan menambahkannya ke parameter dependensi anotasi komponen. Karena LoginActivity akan diinjeksi oleh Dagger, tambahkan metode inject() ke antarmuka.

Saat membuat LoginComponent, instance AppComponent harus diteruskan. Gunakan factory komponen untuk melakukannya:

Kotlin

@Component(dependencies = [AppComponent::class])
interface LoginComponent {

    @Component.Factory
    interface Factory {
        // Takes an instance of AppComponent when creating
        // an instance of LoginComponent
        fun create(appComponent: AppComponent): LoginComponent
    }

    fun inject(activity: LoginActivity)
}

Java

@Component(dependencies = AppComponent.class)
public interface LoginComponent {

    @Component.Factory
    interface Factory {
        // Takes an instance of AppComponent when creating
        // an instance of LoginComponent
        LoginComponent create(AppComponent appComponent);
    }

    void inject(LoginActivity loginActivity);
}

Sekarang, LoginActivity dapat membuat instance LoginComponent dan memanggil metode inject().

Kotlin

class LoginActivity: Activity() {

    // You want Dagger to provide an instance of LoginViewModel from the Login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        // Gets appComponent from MyApplication available in the base Gradle module
        val appComponent = (applicationContext as MyApplication).appComponent

        // Creates a new instance of LoginComponent
        // Injects the component to populate the @Inject fields
        DaggerLoginComponent.factory().create(appComponent).inject(this)

        super.onCreate(savedInstanceState)

        // Now you can access loginViewModel
    }
}

Java

public class LoginActivity extends Activity {

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Gets appComponent from MyApplication available in the base Gradle module
        AppComponent appComponent = ((MyApplication) getApplicationContext()).appComponent;

        // Creates a new instance of LoginComponent
        // Injects the component to populate the @Inject fields
        DaggerLoginComponent.factory().create(appComponent).inject(this);

        // Now you can access loginViewModel
    }
}

LoginViewModel bergantung pada UserRepository; dan agar LoginComponent dapat mengaksesnya dari AppComponent, AppComponent harus menampilkannya dalam antarmukanya:

Kotlin

@Singleton
@Component
interface AppComponent {
    fun userRepository(): UserRepository
}

Java

@Singleton
@Component
public interface AppComponent {
    UserRepository userRepository();
}

Aturan pembatasan pada komponen dependen berfungsi dengan cara yang sama seperti pada subkomponen. Karena menggunakan instance AppComponent, LoginComponent tidak dapat menggunakan anotasi cakupan yang sama.

Jika ingin LoginViewModel tercakup di LoginComponent, Anda akan melakukannya seperti yang Anda lakukan sebelumnya menggunakan anotasi @ActivityScope kustom.

Kotlin

@ActivityScope
@Component(dependencies = [AppComponent::class])
interface LoginComponent { ... }

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

Java

@ActivityScope
@Component(dependencies = AppComponent.class)
public interface LoginComponent { ... }

@ActivityScope
public class LoginViewModel {

    private final UserRepository userRepository;

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

Praktik terbaik

  • ApplicationComponent harus selalu berada dalam modul app.

  • Buat komponen Dagger di modul jika Anda perlu melakukan injeksi kolom dalam modul tersebut atau Anda harus mencakup objek untuk alur tertentu dari aplikasi Anda.

  • Untuk modul Gradle yang dimaksudkan untuk menjadi utilitas atau helper dan tidak perlu membuat grafik (karena itulah Anda memerlukan komponen Dagger), buat dan tampilkan modul Dagger umum dengan metode @Provides dan @Binds class tersebut yang tidak mendukung injeksi konstruktor.

  • Untuk menggunakan Dagger di aplikasi Android dengan modul fitur, gunakan dependensi komponen agar dapat mengakses dependensi yang disediakan oleh ApplicationComponent yang ditentukan dalam modul app.