複数の Gradle モジュールがあるプロジェクトは、マルチモジュール プロジェクトと呼ばれます。
機能モジュールのない単一の APK としてリリースされるマルチモジュール プロジェクトには、多くの場合、プロジェクトのほとんどのモジュールが依存する app
モジュールと、通常、残りのモジュールが依存する base
モジュールまたは core
モジュールが含まれます。app
モジュールには通常 Application
クラスが含まれ、base
モジュールにはプロジェクト内のすべてのモジュールで共有される一般的なクラスが含まれます。
app
モジュールは、他のコンポーネントで必要なオブジェクトやアプリのシングルトンを提供するアプリケーション コンポーネント(例: 下図の ApplicationComponent
)を宣言するのに適した場所です。たとえば、core
モジュールで定義される OkHttpClient
のようなクラス、JSON パーサー、データベースのアクセサー、SharedPreferences
オブジェクトは、app
で定義される ApplicationComponent
によって提供されます。
app
モジュールには、存続期間がより短い他のコンポーネントを持たせることもできます。
たとえば、ログイン後のユーザー固有の設定(UserSession
など)を持つ UserComponent
などです。
プロジェクトの各モジュールには、図 1 に示すように、そのモジュールに固有のロジックを持つサブコンポーネントを少なくとも 1 つ定義できます。
たとえば、login
モジュールに、機能に共通するオブジェクト(LoginRepository
など)を提供し、カスタムの @ModuleScope
アノテーションでスコープ指定された LoginComponent
を持たせることができます。このモジュールには、LoginComponent
に依存し、別のカスタム スコープを持つコンポーネントを持たせることもできます。たとえば、LoginActivityComponent
や TermsAndConditionsComponent
に @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
モジュールが MyApplication
と appComponent
を知らないためです。コンパイルできるようにするには、機能モジュールにインターフェースを定義して、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 に示す、モジュールが作る構造をご覧ください。
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 モジュールをインクルードする場合、次のように、LoginActivity
に LoginViewModel
インスタンスを注入する必要があります。
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); }
これで、LoginActivity
は LoginComponent
のインスタンスを作成し、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
は UserRepository
に依存します。LoginComponent
が AppComponent
からアクセスできるようにするには、次のように、AppComponent
をそのインターフェースで公開する必要があります。
Kotlin
@Singleton @Component interface AppComponent { fun userRepository(): UserRepository }
Java
@Singleton @Component public interface AppComponent { UserRepository userRepository(); }
依存するコンポーネントのスコープ指定のルールは、サブコンポーネントと同様に適用されます。LoginComponent
は AppComponent
のインスタンスを使用するため、同じスコープ アノテーションを使用することはできません。
LoginViewModel
を LoginComponent
にスコープ設定する場合は、以前行ったようにカスタムの @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
が提供する依存関係にアクセスできるようにする。