マルチモジュール アプリで Dagger を使用する

複数の Gradle モジュールがあるプロジェクトは、マルチモジュール プロジェクトと呼ばれます。 機能モジュールのない単一の APK としてリリースされるマルチモジュール プロジェクトには、多くの場合、プロジェクトのほとんどのモジュールが依存する app モジュールと、通常、残りのモジュールが依存する base モジュールまたは core モジュールが含まれます。app モジュールには通常 Application クラスが含まれ、base モジュールにはプロジェクト内のすべてのモジュールで共有される一般的なクラスが含まれます。

app モジュールは、他のコンポーネントで必要なオブジェクトやアプリのシングルトンを提供するアプリケーション コンポーネント(例: 下図の ApplicationComponent)を宣言するのに適した場所です。たとえば、core モジュールで定義される OkHttpClient のようなクラス、JSON パーサー、データベースのアクセサー、SharedPreferences オブジェクトは、app で定義される ApplicationComponent によって提供されます。

app モジュールには、存続期間がより短い他のコンポーネントを持たせることもできます。 たとえば、ログイン後のユーザー固有の設定(UserSession など)を持つ UserComponent などです。

プロジェクトの各モジュールには、図 1 に示すように、そのモジュールに固有のロジックを持つサブコンポーネントを少なくとも 1 つ定義できます。

図 1. マルチモジュール プロジェクトの Dagger グラフの例

たとえば、login モジュールに、機能に共通するオブジェクト(LoginRepository など)を提供し、カスタムの @ModuleScope アノテーションでスコープ指定された LoginComponent を持たせることができます。このモジュールには、LoginComponent に依存し、別のカスタム スコープを持つコンポーネントを持たせることもできます。たとえば、LoginActivityComponentTermsAndConditionsComponent@FeatureScope を指定するなど、より機能に特化したロジック(ViewModel オブジェクトなど)をスコープに指定できます。

Registration などのモジュールにも、同様の設定を行うかもしれません。

一般的なルールとして、マルチモジュール プロジェクトでは、同じレベルのモジュールが互いに依存することは許されません。互いに依存する場合は、共有のロジック(それらの間の依存関係)を親モジュールの一部とすべきかどうかを検討してください。そうすべきであれば、リファクタリングしてそれらのクラスを親モジュールに移動します。そうすべきでない場合は、親モジュールを拡張する新しいモジュールを作成し、元のモジュールの両方が新しいモジュールを拡張するようにします。

次の場合は、通常、モジュール内にコンポーネントを作成するのがおすすめの方法です。

  • フィールド注入の実行が必要(LoginActivityComponent と同様)。

  • オブジェクトのスコープ指定が必要(LoginComponent と同様)。

どちらにも当てはまらず、そのモジュールからオブジェクトを提供する方法を Dagger に指定する必要があり、そのクラスに対するコンストラクタ注入が不可能な場合は、@Provides メソッドまたは @Binds メソッドを持つ Dagger モジュールを作成して公開します。

Dagger サブコンポーネントを使用する実装

サブコンポーネントの作成方法と使用方法については、Android アプリで Dagger を使用するをご覧ください。ただし、機能モジュールは app モジュールを知らないため、同じコードを使用できません。たとえば、一般的なログインフローと、前のページに示した次のようなコードを考えた場合、コンパイルがエラーになります。

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

        ...
    }
}

これは、login モジュールが MyApplicationappComponent を知らないためです。コンパイルできるようにするには、機能モジュールにインターフェースを定義して、MyApplication が実装する必要がある FeatureComponent を提供する必要があります。

次の例では、Login フローの login モジュールで、LoginComponent を提供する LoginComponentProvider インターフェースを定義しています。

Kotlin

interface LoginComponentProvider {
    fun provideLoginComponent(): LoginComponent
}

Java

public interface LoginComponentProvider {
   public LoginComponent provideLoginComponent();
}

LoginActivity は、上で定義したコード スニペットとは異なり、次のようにインターフェースを使用します。

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

        ...
    }
}

MyApplication は、次のようにインターフェースを実装し、必要なメソッドを実装する必要があります。

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

以上が、マルチモジュール プロジェクトで Dagger サブコンポーネントを使用する方法です。 機能モジュールでは、モジュール同士の依存の仕方が異なるため、解決方法も異なります。

機能モジュールによるコンポーネントの依存関係

機能モジュールでは、モジュール間の通常の依存の仕方が逆になります。app モジュールが機能モジュールを含むのではなく、機能モジュールが app モジュールに依存します。図 2 に示す、モジュールが作る構造をご覧ください。

図 2. 機能モジュールを含むプロジェクトの Dagger グラフの例

Dagger では、コンポーネントはそのサブコンポーネントについて知っている必要があります。この情報は、親コンポーネントに追加された Dagger モジュール(例: Android アプリでの Dagger の使用SubcomponentsModule モジュール)に含まれます。

残念ながら、アプリと機能モジュールの依存関係が逆になっているため、サブコンポーネントはビルドパスになく、app モジュールからは見えません。たとえば、login 機能モジュールで定義された LoginComponent を、app モジュールで定義された ApplicationComponent のサブコンポーネントにはできません。

Dagger では、この問題をコンポーネント依存関係というメカニズムで解決できます。子コンポーネントが親コンポーネントのサブコンポーネントなのではなく、子コンポーネントが親コンポーネントに依存します。これにより、親子関係はなくなり、コンポーネントは、なんらかの依存関係を得るために他のコンポーネントに依存するようになります。コンポーネントは、依存するコンポーネントが利用する型をグラフから公開する必要があります。

たとえば、login という機能モジュールが、app Gradle モジュールで利用可能な AppComponent に依存する LoginComponent を作成するとします。

以下は、app Gradle モジュールに含まれるクラスと AppComponent の定義です。

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

login gradle モジュールに app gradle モジュールをインクルードする場合、次のように、LoginActivityLoginViewModel インスタンスを注入する必要があります。

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 は、利用可能範囲とスコープを AppComponent に指定された UserRepository に依存しています。LoginActivity を注入するために、次のように、AppComponent に依存する LoginComponent を作成します。

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 は、コンポーネント アノテーションの dependencies パラメータに追加することにより、AppComponent への依存関係を指定します。LoginActivity は Dagger によって注入されるため、inject() メソッドをインターフェースに追加します。

LoginComponent を作成するときは、AppComponent のインスタンスを渡す必要があります。これには、次のようにコンポーネント ファクトリを使用します。

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

これで、LoginActivityLoginComponent のインスタンスを作成し、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
    }
}

LoginViewModelUserRepository に依存します。LoginComponentAppComponent からアクセスできるようにするには、次のように、AppComponent をそのインターフェースで公開する必要があります。

Kotlin

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

Java

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

依存するコンポーネントのスコープ指定のルールは、サブコンポーネントと同様に適用されます。LoginComponentAppComponent のインスタンスを使用するため、同じスコープ アノテーションを使用することはできません。

LoginViewModelLoginComponent にスコープ設定する場合は、以前行ったようにカスタムの @ActivityScope アノテーションを使用します。

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

おすすめの方法

  • ApplicationComponent を常に app モジュールに入れる。

  • モジュールでフィールド注入を行う必要がある場合や、アプリケーションの特定のフローでオブジェクトをスコープ指定する必要がある場合は、そのモジュールに Dagger コンポーネントを作成する。

  • ユーティリティやヘルパーにするために、グラフを作成する必要がない Gradle モジュールの場合(Dagger コンポーネントを必要とするのはこのため)、Dagger モジュールを作成して公開する。その際、コンストラクタ注入をサポートしないクラスで @Provides メソッドと @Binds メソッドを指定する。

  • 機能モジュールがある Android アプリで Dagger を使用するには、コンポーネント依存関係を使用し、app モジュールで定義された ApplicationComponent が提供する依存関係にアクセスできるようにする。