手動插入依附元件

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

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

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

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

手動插入依附元件基礎

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

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

在一般 Android 應用程式的登入流程中,LoginActivity 會依附 LoginViewModel,而後者則依附 UserRepository。其後,UserRepository 依附 UserLocalDataSourceUserRemoteDataSource,而後者依附 Retrofit 課程中也會快速介紹 Memorystore 這是 Google Cloud 的全代管 Redis 服務

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

您可以在 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 例項。

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

結語

建立可擴充且可測試的 Android 應用程式時,依附元件插入是一項好用的功能。利用容器在應用程式不同部分共用類別的例項,並使用工廠集中一處建立類別的例項。

隨著應用程式規模變大,您會發現自己編寫出許多容易出錯的樣板程式碼 (例如工廠)。您也必須自行管理容器的範圍和生命週期,最佳化及捨棄不再需要的容器來釋出記憶體。如果出錯,可能會導致應用程式出現細微錯誤和記憶體流失。

您可以參閱「Dagger」相關章節,瞭解如何使用 Dagger 自動執行這項程序,並透過其他方式產生以手動方式編寫的相同程式碼。