手動による依存関係挿入

Android アプリの推奨アーキテクチャでは、コードをクラスに分割して、関心の分離を活用することを推奨しています。関心の分離とは、階層の各クラスが定義された単一の責任を持つという原則です。 これにより、より多くの小さなクラスが互いに接続して互いの依存関係を満たす必要が生じます。

通常、Android アプリは多数のクラスで構成され、一部のクラスは互いに依存しています。
図 1. Android アプリのアプリグラフのモデル

クラス間の依存関係はグラフで表すことができ、各クラスは依存するクラスに接続されます。すべてのクラスとその依存関係をあらわすことでアプリグラフが構成されます。 図 1 に、アプリグラフの概要を示します。 クラス A(ViewModel)がクラス B(Repository)に依存している場合、A から B への線はその依存関係を表しています。

依存関係挿入により、これらの接続を確立し、テストのために実装を入れ替えることが可能になります。たとえば、リポジトリに依存する ViewModel をテストする場合、Repository の異なる実装をフェイクやモックで渡して、それぞれのケースをテストできます。

手動依存関係挿入の基本

このセクションでは、実際の Android アプリのシナリオで手動依存関係挿入を適用する方法について説明します。ここでは、アプリで依存関係挿入を開始する反復的アプローチを説明します。このアプローチは、Dagger が自動的に生成するものと非常によく似たポイントに達するまで改善されていきます。Dagger の詳細については、Dagger の基本をご覧ください。

フローは、機能に対応するアプリ内の画面のグループと考えてください。ログイン、登録、決済はすべてフローの例です。

一般的な Android アプリのログインフローをカバーする場合、LoginActivityLoginViewModel に依存し、さらに UserRepository に依存します。 この場合、UserRepositoryUserLocalDataSourceUserRemoteDataSource に依存し、さらに、これらは Retrofit サービスに依存します。

LoginActivity はログインフローへのエントリ ポイントであり、ユーザーはアクティビティを操作します。したがって、LoginActivity はすべての依存関係が含まれる LoginViewModel を作成する必要があります。

フローの RepositoryDataSource のクラスは次のようになります。

Kotlin

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

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

Java

class UserLocalDataSource {
    public UserLocalDataSource() { }
    ...
}

class UserRemoteDataSource {

    private final Retrofit retrofit;

    public UserRemoteDataSource(Retrofit retrofit) {
        this.retrofit = retrofit;
    }

    ...
}

class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

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

    ...
}

LoginActivity は次のように表示されます。

Kotlin

class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // In order to satisfy the dependencies of LoginViewModel, you have to also
        // satisfy the dependencies of all of its dependencies recursively.
        // First, create retrofit which is the dependency of UserRemoteDataSource
        val retrofit = Retrofit.Builder()
            .baseUrl("https://example.com")
            .build()
            .create(LoginService::class.java)

        // Then, satisfy the dependencies of UserRepository
        val remoteDataSource = UserRemoteDataSource(retrofit)
        val localDataSource = UserLocalDataSource()

        // Now you can create an instance of UserRepository that LoginViewModel needs
        val userRepository = UserRepository(localDataSource, remoteDataSource)

        // Lastly, create an instance of LoginViewModel with userRepository
        loginViewModel = LoginViewModel(userRepository)
    }
}

Java

public class MainActivity extends Activity {

    private LoginViewModel loginViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // In order to satisfy the dependencies of LoginViewModel, you have to also
        // satisfy the dependencies of all of its dependencies recursively.
        // First, create retrofit which is the dependency of UserRemoteDataSource
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://example.com")
                .build()
                .create(LoginService.class);

        // Then, satisfy the dependencies of UserRepository
        UserRemoteDataSource remoteDataSource = new UserRemoteDataSource(retrofit);
        UserLocalDataSource localDataSource = new UserLocalDataSource();

        // Now you can create an instance of UserRepository that LoginViewModel needs
        UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource);

        // Lastly, create an instance of LoginViewModel with userRepository
        loginViewModel = new LoginViewModel(userRepository);
    }
}

この方法には次のような問題があります。

  1. ボイラープレート コードが多い。コードの別の部分で LoginViewModel の別のインスタンスを作成する場合は、コードが重複してしまいます。

  2. 依存関係は順番に宣言する必要があります。作成するには、LoginViewModel の前に UserRepository をインスタンス化する必要があります。

  3. オブジェクトを再利用するのは困難です。複数の機能で UserRepository を再利用する場合は、シングルトン パターンに従う必要があります。 すべてのテストが同じシングルトン インスタンスを共有するため、シングルトン パターンによりテストはより困難になります。

コンテナを使用した依存関係の管理

オブジェクトの再利用に関する問題を解決するには、依存関係の取得に使用する独自の依存関係コンテナクラスを作成します。このコンテナで提供されるすべてのインスタンスを公開できます。この例では、UserRepository のインスタンスのみが必要なため、依存関係を非公開にしておき、その後必要になった場合に一般公開にすることができます。

Kotlin

// Container of objects shared across the whole app
class AppContainer {

    // Since you want to expose userRepository out of the container, you need to satisfy
    // its dependencies as you did before
    private val retrofit = Retrofit.Builder()
                            .baseUrl("https://example.com")
                            .build()
                            .create(LoginService::class.java)

    private val remoteDataSource = UserRemoteDataSource(retrofit)
    private val localDataSource = UserLocalDataSource()

    // userRepository is not private; it'll be exposed
    val userRepository = UserRepository(localDataSource, remoteDataSource)
}

Java

// Container of objects shared across the whole app
public class AppContainer {

    // Since you want to expose userRepository out of the container, you need to satisfy
    // its dependencies as you did before
    private Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("https://example.com")
            .build()
            .create(LoginService.class);

    private UserRemoteDataSource remoteDataSource = new UserRemoteDataSource(retrofit);
    private UserLocalDataSource localDataSource = new UserLocalDataSource();

    // userRepository is not private; it'll be exposed
    public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource);
}

これらの依存関係はアプリ全体で使用されるため、すべてのアクティビティが使用できる共通の場所、つまり Application クラスに配置する必要があります。AppContainer インスタンスを含むカスタム Application クラスを作成します。

Kotlin

// Custom Application class that needs to be specified
// in the AndroidManifest.xml file
class MyApplication : Application() {

    // Instance of AppContainer that will be used by all the Activities of the app
    val appContainer = AppContainer()
}

Java

// Custom Application class that needs to be specified
// in the AndroidManifest.xml file
public class MyApplication extends Application {

    // Instance of AppContainer that will be used by all the Activities of the app
    public AppContainer appContainer = new AppContainer();
}

これで、アプリから AppContainer のインスタンスを取得し、UserRepository インスタンスの共有を取得できます。

Kotlin

class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Gets userRepository from the instance of AppContainer in Application
        val appContainer = (application as MyApplication).appContainer
        loginViewModel = LoginViewModel(appContainer.userRepository)
    }
}

Java

public class MainActivity extends Activity {

    private LoginViewModel loginViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Gets userRepository from the instance of AppContainer in Application
        AppContainer appContainer = ((MyApplication) getApplication()).appContainer;
        loginViewModel = new LoginViewModel(appContainer.userRepository);
    }
}

このように、シングルトンの UserRepository は存在しません。代わりに、グラフのオブジェクトを含むすべてのアクティビティで AppContainer を共有し、他のクラスが使用できるオブジェクトのインスタンスを作成します。

LoginViewModel がアプリ内のより多くの場所で必要な場合は、LoginViewModel のインスタンスを作成する一元化された場所が必要です。LoginViewModel の作成をコンテナに移動し、そのタイプの新しいオブジェクトをファクトリで指定できます。LoginViewModelFactory のコードは次のようになります。

Kotlin

// Definition of a Factory interface with a function to create objects of a type
interface Factory<T> {
    fun create(): T
}

// Factory for LoginViewModel.
// Since LoginViewModel depends on UserRepository, in order to create instances of
// LoginViewModel, you need an instance of UserRepository that you pass as a parameter.
class LoginViewModelFactory(private val userRepository: UserRepository) : Factory {
    override fun create(): LoginViewModel {
        return LoginViewModel(userRepository)
    }
}

Java

// Definition of a Factory interface with a function to create objects of a type
public interface Factory<T> {
    T create();
}

// Factory for LoginViewModel.
// Since LoginViewModel depends on UserRepository, in order to create instances of
// LoginViewModel, you need an instance of UserRepository that you pass as a parameter.
class LoginViewModelFactory implements Factory {

    private final UserRepository userRepository;

    public LoginViewModelFactory(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public LoginViewModel create() {
        return new LoginViewModel(userRepository);
    }
}

LoginActivity が使用できるように LoginViewModelFactoryAppContainer に含めます。

Kotlin

// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory
class AppContainer {
    ...
    val userRepository = UserRepository(localDataSource, remoteDataSource)

    val loginViewModelFactory = LoginViewModelFactory(userRepository)
}

class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Gets LoginViewModelFactory from the application instance of AppContainer
        // to create a new LoginViewModel instance
        val appContainer = (application as MyApplication).appContainer
        loginViewModel = appContainer.loginViewModelFactory.create()
    }
}

Java

// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory
public class AppContainer {
    ...

    public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource);

    public LoginViewModelFactory loginViewModelFactory = new LoginViewModelFactory(userRepository);
}

public class MainActivity extends Activity {

    private LoginViewModel loginViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Gets LoginViewModelFactory from the application instance of AppContainer
        // to create a new LoginViewModel instance
        AppContainer appContainer = ((MyApplication) getApplication()).appContainer;
        loginViewModel = appContainer.loginViewModelFactory.create();
    }
}

この方法は前の方法よりも優れていますが、次の点も考慮してください。

  1. AppContainer を自分で管理し、すべての依存関係のインスタンスを手動で作成する必要があります。

  2. 他にも多くのボイラープレート コードがあります。オブジェクトを再利用するかどうかに応じて、ファクトリやパラメータを手動で作成する必要があります。

アプリフローでの依存関係の管理

AppContainer は、プロジェクトに機能を追加しようとすると複雑になります。アプリの規模が大きくなり、さまざまな機能フローを導入するようになると、さらに多くの問題が発生します。

  1. 異なるフローがある場合は、オブジェクトをそのフローの範囲内で動作させることができます。たとえば、LoginUserData(ログインフローでのみ使用されるユーザー名とパスワード)を作成する場合、別のユーザーの古いログインフローのデータは保持しないことも可能です。新しいフローごとに新しいインスタンスが必要です。次のコード例で示すように、AppContainer 内に FlowContainer オブジェクトを作成することで、これを実現できます。

  2. アプリグラフとフローコンテナの最適化も難しい場合があります。 現在のフローに応じて、不要なインスタンスを削除する必要があります。

1 つのアクティビティ(LoginActivity)と複数のフラグメント(LoginUsernameFragmentLoginPasswordFragment)で構成されるログインフローがあるとします。これらのビューでは、次のことができます。

  1. ログインフローが終了するまで、共有する必要がある同じ LoginUserData インスタンスにアクセスします。

  2. フローが再開されると、LoginUserData の新しいインスタンスを作成します。

これを行うには、ログインフロー コンテナを使用します。このコンテナは、ログインフローの開始時に作成し、フローの終了時にメモリから削除する必要があります。

サンプルコードに LoginContainer を追加しましょう。アプリで LoginContainer のインスタンスを複数作成できるようにする場合、シングルトンにするのではなく、AppContainer からのログインフローに必要な依存関係を持つクラスにします。

Kotlin

class LoginContainer(val userRepository: UserRepository) {

    val loginData = LoginUserData()

    val loginViewModelFactory = LoginViewModelFactory(userRepository)
}

// AppContainer contains LoginContainer now
class AppContainer {
    ...
    val userRepository = UserRepository(localDataSource, remoteDataSource)

    // LoginContainer will be null when the user is NOT in the login flow
    var loginContainer: LoginContainer? = null
}

Java

// Container with Login-specific dependencies
class LoginContainer {

    private final UserRepository userRepository;

    public LoginContainer(UserRepository userRepository) {
        this.userRepository = userRepository;
        loginViewModelFactory = new LoginViewModelFactory(userRepository);
    }

    public LoginUserData loginData = new LoginUserData();

    public LoginViewModelFactory loginViewModelFactory;
}

// AppContainer contains LoginContainer now
public class AppContainer {
    ...
    public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource);

    // LoginContainer will be null when the user is NOT in the login flow
    public LoginContainer loginContainer;
}

フローに固有のコンテナを作成したら、コンテナ インスタンスを作成して削除するタイミングを決定する必要があります。ログインフローはアクティビティ(LoginActivity)に自己完結しているため、アクティビティはそのコンテナのライフサイクルを管理します。LoginActivityonCreate() でインスタンスを作成し、onDestroy() で削除します。

Kotlin

class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel
    private lateinit var loginData: LoginUserData
    private lateinit var appContainer: AppContainer


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        appContainer = (application as MyApplication).appContainer

        // Login flow has started. Populate loginContainer in AppContainer
        appContainer.loginContainer = LoginContainer(appContainer.userRepository)

        loginViewModel = appContainer.loginContainer.loginViewModelFactory.create()
        loginData = appContainer.loginContainer.loginData
    }

    override fun onDestroy() {
        // Login flow is finishing
        // Removing the instance of loginContainer in the AppContainer
        appContainer.loginContainer = null
        super.onDestroy()
    }
}

Java

public class LoginActivity extends Activity {

    private LoginViewModel loginViewModel;
    private LoginData loginData;
    private AppContainer appContainer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        appContainer = ((MyApplication) getApplication()).appContainer;

        // Login flow has started. Populate loginContainer in AppContainer
        appContainer.loginContainer = new LoginContainer(appContainer.userRepository);

        loginViewModel = appContainer.loginContainer.loginViewModelFactory.create();
        loginData = appContainer.loginContainer.loginData;
    }

    @Override
    protected void onDestroy() {
        // Login flow is finishing
        // Removing the instance of loginContainer in the AppContainer
        appContainer.loginContainer = null;

        super.onDestroy();
    }
}

LoginActivity と同様に、ログイン フラグメントは AppContainer から LoginContainer にアクセスし、共有の LoginUserData インスタンスを使用できます。

この場合はビューのライフサイクル ロジックを扱うため、ライフサイクルの観察を使用するのが理にかなっています。

おわりに

依存関係挿入は、スケーラブルでテスト可能な Android アプリを作成するための優れた手法です。コンテナは、アプリのさまざまな部分でクラスのインスタンスを共有する方法として、また、ファクトリを使用してクラスのインスタンスを作成する一元化された場所として使用します。

アプリの規模が大きくなると、大量のボイラープレート コード(ファクトリなど)を作成することになり、エラーが発生しやすくなります。また、コンテナのスコープとライフサイクルを自分で管理し、メモリを解放するためにコンテナを最適化して、不要になったコンテナを破棄する必要があります。 この処理を誤ると、アプリで小さなバグやメモリリークが発生する可能性があります。

Dagger セクションでは、Dagger を使用してこのプロセスを自動化し、手動で記述した場合と同じコードを生成する方法を説明します。