Android アプリの推奨アーキテクチャでは、コードをクラスに分割して、関心の分離を活用することを推奨しています。関心の分離とは、階層の各クラスが定義された単一の責任を持つという原則です。 これにより、より多くの小さなクラスが互いに接続して互いの依存関係を満たす必要が生じます。
クラス間の依存関係はグラフで表すことができ、各クラスは依存するクラスに接続されます。すべてのクラスとその依存関係をあらわすことでアプリグラフが構成されます。
図 1 に、アプリグラフの概要を示します。
クラス A(ViewModel
)がクラス B(Repository
)に依存している場合、A から B への線はその依存関係を表しています。
依存関係挿入により、これらの接続を確立し、テストのために実装を入れ替えることが可能になります。たとえば、リポジトリに依存する ViewModel
をテストする場合、Repository
の異なる実装をフェイクやモックで渡して、それぞれのケースをテストできます。
手動依存関係挿入の基本
このセクションでは、実際の Android アプリのシナリオで手動依存関係挿入を適用する方法について説明します。ここでは、アプリで依存関係挿入を開始する反復的アプローチを説明します。このアプローチは、Dagger が自動的に生成するものと非常によく似たポイントに達するまで改善されていきます。Dagger の詳細については、Dagger の基本をご覧ください。
フローは、機能に対応するアプリ内の画面のグループと考えてください。ログイン、登録、決済はすべてフローの例です。
一般的な Android アプリのログインフローをカバーする場合、LoginActivity
は LoginViewModel
に依存し、さらに UserRepository
に依存します。
この場合、UserRepository
は UserLocalDataSource
と
UserRemoteDataSource
は Retrofit
に依存します。
あります。
LoginActivity
はログインフローへのエントリ ポイントであり、ユーザーはアクティビティを操作します。したがって、LoginActivity
はすべての依存関係が含まれる LoginViewModel
を作成する必要があります。
フローの Repository
と DataSource
のクラスは次のようになります。
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); } }
この方法には次のような問題があります。
ボイラープレート コードが多い。コードの別の部分で
LoginViewModel
の別のインスタンスを作成する場合は、コードが重複してしまいます。依存関係は順番に宣言する必要があります。作成するには、
LoginViewModel
の前にUserRepository
をインスタンス化する必要があります。オブジェクトを再利用するのは困難です。複数の機能で
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
が使用できるように LoginViewModelFactory
を AppContainer
に含めます。
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(); } }
この方法は前の方法よりも優れていますが、次の点も考慮してください。
AppContainer
を自分で管理し、すべての依存関係のインスタンスを手動で作成する必要があります。他にも多くのボイラープレート コードがあります。オブジェクトを再利用するかどうかに応じて、ファクトリやパラメータを手動で作成する必要があります。
アプリフローでの依存関係の管理
AppContainer
は、プロジェクトに機能を追加しようとすると複雑になります。アプリの規模が大きくなり、さまざまな機能フローを導入するようになると、さらに多くの問題が発生します。
異なるフローがある場合は、オブジェクトをそのフローの範囲内で動作させることができます。たとえば、
LoginUserData
(ログインフローでのみ使用されるユーザー名とパスワード)を作成する場合、別のユーザーの古いログインフローのデータは保持しないことも可能です。新しいフローごとに新しいインスタンスが必要です。次のコード例で示すように、AppContainer
内にFlowContainer
オブジェクトを作成することで、これを実現できます。アプリグラフとフローコンテナの最適化も難しい場合があります。 現在のフローに応じて、不要なインスタンスを削除する必要があります。
1 つのアクティビティ(LoginActivity
)と複数のフラグメント(LoginUsernameFragment
と LoginPasswordFragment
)で構成されるログインフローがあるとします。これらのビューでは、次のことができます。
ログインフローが終了するまで、共有する必要がある同じ
LoginUserData
インスタンスにアクセスします。フローが再開されると、
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
)に自己完結しているため、アクティビティはそのコンテナのライフサイクルを管理します。LoginActivity
は onCreate()
でインスタンスを作成し、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 を使用してこのプロセスを自動化し、手動で記述した場合と同じコードを生成する方法を説明します。