手動による依存関係インジェクション(View)

コンセプトと Jetpack Compose の実装

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<LoginViewModel> {
   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<LoginViewModel> {

   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 インスタンスを使用できます。

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