手動插入依附元件 (檢視區塊)

概念和 Jetpack Compose 實作

Android 建議的應用程式架構建議將程式碼分割為多個類別,這麼做可享有關注點分離原則帶來的好處;這項原則是指階層的每個類別都具有已定義的單一責任。這麼做需要將更多較小的類別互相連結,成為彼此的依附元件。

Android 應用程式通常由許多類別組成,其中有些類別互為依附元件。
圖 1. 某個 Android 應用程式的應用程式圖表模型

類別之間的依附元件能以圖表形式呈現,其中每個類別都會分別連結至依附的類別。所有類別及其依附元件的呈現方式可構成「應用程式圖表」。圖 1 顯示應用程式圖表的摘要。當類別 A (ViewModel) 依附類別 B (Repository) 時,從 A 指向 B 的線條就代表該依附元件。

依附元件插入功能可以建立這類連結,並替換用於測試的實作內容。舉例來說,測試依附存放區的 ViewModel 時,您可以透過假的或模擬的方式,傳遞不同的 Repository 實作內容來測試各種情況。

手動插入依附元件基礎

本節說明如何在 Android 應用程式的實際情境中套用手動依附元件插入功能,並逐步說明如何透過疊代的做法,在應用程式中使用依附元件插入功能。這種做法會不斷改進,直到達到接近 Dagger 自動產生的效果。如要進一步瞭解 Dagger,請參閱「Dagger 基本概念」。

假設流程為應用程式中對應至特定功能的一組畫面。登入、註冊和結帳都是流程的範例。

在一般 Android 應用程式的登入流程中,LoginActivity 會依附 LoginViewModel,而後者則依附 UserRepository。其後,UserRepository 依附 UserLocalDataSourceUserRemoteDataSource,而後者則依附 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. 必須依序宣告依附元件。您必須先建立 UserRepository 的例項,才能建立 LoginViewModel

  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);
   }
}

您可以在 AppContainer 中加入 LoginViewModelFactory,供 LoginActivity 使用:

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. 最佳化應用程式圖表和流程容器也很困難。您需要記得根據個別流程,刪除不必要的例項。

假設有一個登入流程包含一項活動 (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) 中具有獨立性,因此會由活動管理該容器的生命週期。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 例項。

在本範例中,您要處理檢視區塊生命週期邏輯,因此適合使用生命週期觀測功能。