Dagger の基本

Android アプリにおいて、手動による依存関係インジェクションやサービス ロケータは、プロジェクトの規模によっては問題になることがあります。Dagger を使用すれば、依存関係を管理することで、プロジェクトの複雑さを抑えられます。

Dagger は、通常は手書きするコードを模倣してコードを自動的に生成します。このコードはコンパイル時に生成されるため、Guice などのリフレクション ベースのソリューションよりも、トレース可能でパフォーマンスが高いものになります。

Dagger を使用するメリット

Dagger は、次のようにして、退屈でエラーが発生しがちなボイラープレート コードの作成からプログラマーを解放します。

  • 手動 DI セクションに手動で実装していた AppContainer コード(アプリケーション グラフ)を生成します。

  • アプリケーション グラフで使用可能なクラスの Factory を作成します。これにより、依存関係が内部的に満たされます。

  • スコープを使用した型の設定に応じて、依存関係の再利用、またはその型の新しいインスタンスの作成を行います。

  • Dagger サブコンポーネントを使用して、前のセクションで扱ったログインフローと同じように、特定のフローのコンテナを作成します。これにより、メモリ内の不要になったオブジェクトを解放できるため、アプリのパフォーマンスが向上します。

Dagger は、アノテーションを使用して、クラスの依存関係を宣言し、依存関係を満たす方法を指定している限り、上記の処理をビルド時に自動的に行います。Dagger は、手動で記述するのと同じようなコードを生成します。内部的には、参照する可能性のあるオブジェクトのグラフを作成して、クラスのインスタンスを提供する方法を見つけます。グラフ内のすべてのクラスに対して、インスタンスを取得するために内部で使用する Factory 型のクラスを生成します。

Dagger は、ビルド時に、コードを参照しながら、次のような処理を行います。

  • 依存関係グラフを作成して、次のことを確認します。

    • オブジェクトのすべての依存関係を満たすことができる。これにより、実行時の例外は発生しません。
    • 依存関係の循環が存在しない。これにより、無限ループは発生しません。
  • 実際のオブジェクトとその依存関係を作成するのに実行時に使用するクラスを生成します。

Dagger の簡単な使用例: ファクトリの生成

Dagger の使用方法を説明するために、次の図に示す UserRepository クラスの簡単な Factory を作成します。

UserRepository を次のように定義します。

Kotlin

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

Java

public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

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

    ...
}

@Inject アノテーションを UserRepository コンストラクタに追加して、Dagger が UserRepository の作成方法を認識できるようにします。

Kotlin

// @Inject lets Dagger know how to create instances of this object
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

Java

public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    // @Inject lets Dagger know how to create instances of this object
    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

上のコード スニペットでは、Dagger に次のことを認識させています。

  1. @Inject アノテーションが付けられたコンストラクタを使用して UserRepository のインスタンスを作成する方法。

  2. その依存関係が UserLocalDataSourceUserRemoteDataSource であること。

こうして Dagger は、UserRepository のインスタンスの作成方法を認識するようになりますが、その依存関係を作成する方法は認識しません。次のように他のクラスにもアノテーションを付ければ、その作成方法を認識するようになります。

Kotlin

// @Inject lets Dagger know how to create instances of these objects
class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor() { ... }

Java

public class UserLocalDataSource {
    @Inject
    public UserLocalDataSource() { }
}

public class UserRemoteDataSource {
    @Inject
    public UserRemoteDataSource() { }
}

Dagger コンポーネント

Dagger は、プロジェクト内に依存関係のグラフを作成します。これを使用して、依存関係が必要なときにどこで取得すべきかを知ることができます。そのためには、インターフェースを作成して、それに @Component アノテーションを付ける必要があります。手動による依存関係インジェクションで行うのと同じように、Dagger がコンテナを作成します。

@Component インターフェースの内部では、必要なクラス(UserRepository)のインスタンスを返す関数を定義します。@Component は、Dagger に対して、公開する型が満たす必要がある依存関係をすべて持ったコンテナの生成を指示します。これは Dagger コンポーネントと呼ばれます。これには、Dagger が提供方法を認識しているオブジェクトと、その依存関係からなるグラフが含まれています。

Kotlin

// @Component makes Dagger create a graph of dependencies
@Component
interface ApplicationGraph {
    // The return type  of functions inside the component interface is
    // what can be provided from the container
    fun repository(): UserRepository
}

Java

// @Component makes Dagger create a graph of dependencies
@Component
public interface ApplicationGraph {
    // The return type  of functions inside the component interface is
    // what can be consumed from the graph
    UserRepository userRepository();
}

プロジェクトをビルドすると、Dagger によって ApplicationGraph インターフェースの実装である DaggerApplicationGraph が生成されます。Dagger は、そのアノテーション プロセッサを使用して、UserRepository インスタンスを取得する 1 つのエントリ ポイントだけを持った、3 つのクラス(UserRepositoryUserLocalDatasourceUserRemoteDataSource)間の関係からなる依存関係グラフを作成します。これは次のように使用できます。

Kotlin

// Create an instance of the application graph
val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()
// Grab an instance of UserRepository from the application graph
val userRepository: UserRepository = applicationGraph.repository()

Java

// Create an instance of the application graph
ApplicationGraph applicationGraph = DaggerApplicationGraph.create();

// Grab an instance of UserRepository from the application graph
UserRepository userRepository = applicationGraph.userRepository();

要求されるたびに UserRepository の新しいインスタンスが作成されます。

Kotlin

val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()

val userRepository: UserRepository = applicationGraph.repository()
val userRepository2: UserRepository = applicationGraph.repository()

assert(userRepository != userRepository2)

Java

ApplicationGraph applicationGraph = DaggerApplicationGraph.create();

UserRepository userRepository = applicationGraph.userRepository();
UserRepository userRepository2 = applicationGraph.userRepository();

assert(userRepository != userRepository2)

場合によっては、コンテナ内に依存関係の一意のインスタンスが必要になります。これには次のような理由があります。

  1. この型を依存関係として持つ他の型に、同じインスタンスを共有させたい(ログインフローの複数の ViewModel オブジェクトが同じ LoginUserData を使用している、など)。

  2. オブジェクトを作成するコストが高く、依存関係として宣言されるたびに新しいインスタンスを作成したくない(JSON パーサーなど)。

このサンプルでは、グラフ内に UserRepository の一意のインスタンスを持って、UserRepository を要求するたびに常に同じインスタンスが取得されるようにすることが推奨されます。より複雑なアプリケーション グラフを持った実際のアプリケーションでは、UserRepository に依存する複数の ViewModel オブジェクトがあり、UserRepository の提供が必要となるたびに UserLocalDataSourceUserRemoteDataSource の新しいインスタンスを作成するのは好ましくないため、このようにする価値があります。

手動による依存関係インジェクションの場合は、UserRepository の同じインスタンスを ViewModel クラスのコンストラクタに渡すことによってこれを実現します。しかし Dagger の場合は、コードを手動で記述しないため、同じインスタンスを使用することを Dagger に認識させる必要があります。これは、スコープ アノテーションで実現できます。

Dagger を使用したスコープ設定

スコープ アノテーションを使用すると、オブジェクトの存続期間をそのコンポーネントの存続期間に制限できます。つまり、型を提供する必要があるたびに依存関係の同じインスタンスが使用されます。

ApplicationGraph のリポジトリを要求するときに UserRepository の一意のインスタンスを作成するには、@Component インターフェースと UserRepository に同じスコープ アノテーションを使用します。次のように、Dagger が使用する javax.inject パッケージにすでに用意されている @Singleton アノテーションが使用できます。

Kotlin

// Scope annotations on a @Component interface informs Dagger that classes annotated
// with this annotation (i.e. @Singleton) are bound to the life of the graph and so
// the same instance of that type is provided every time the type is requested.
@Singleton
@Component
interface ApplicationGraph {
    fun repository(): UserRepository
}

// Scope this class to a component using @Singleton scope (i.e. ApplicationGraph)
@Singleton
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

Java

// Scope annotations on a @Component interface informs Dagger that classes annotated
// with this annotation (i.e. @Singleton) are scoped to the graph and the same
// instance of that type is provided every time the type is requested.
@Singleton
@Component
public interface ApplicationGraph {
    UserRepository userRepository();
}

// Scope this class to a component using @Singleton scope (i.e. ApplicationGraph)
@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;
    }
}

また、カスタムのスコープ アノテーションを作成して使用することもできます。スコープ アノテーションは、次のようにして作成できます。

Kotlin

// Creates MyCustomScope
@Scope
@MustBeDocumented
@Retention(value = AnnotationRetention.RUNTIME)
annotation class MyCustomScope

Java

// Creates MyCustomScope
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCustomScope {}

こうすると、次のように前の例と同じように使用できます。

Kotlin

@MyCustomScope
@Component
interface ApplicationGraph {
    fun repository(): UserRepository
}

@MyCustomScope
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val service: UserService
) { ... }

Java

@MyCustomScope
@Component
public interface ApplicationGraph {
    UserRepository userRepository();
}

@MyCustomScope
public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

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

どちらの場合も、同じスコープが @Component インターフェースに対するアノテーション付けに使用されて、オブジェクトが提供されます。したがって、applicationGraph.repository() を呼び出すたびに、UserRepository の同じインスタンスが取得されます。

Kotlin

val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()

val userRepository: UserRepository = applicationGraph.repository()
val userRepository2: UserRepository = applicationGraph.repository()

assert(userRepository == userRepository2)

Java

ApplicationGraph applicationGraph = DaggerApplicationGraph.create();

UserRepository userRepository = applicationGraph.userRepository();
UserRepository userRepository2 = applicationGraph.userRepository();

assert(userRepository == userRepository2)

まとめ

より複雑なシナリオで Dagger を使用する前に、Dagger の利点とその動作の基本を理解することが重要です。

次のページでは、Android アプリに Dagger を追加する方法を説明します。