手動插入依附元件

透過集合功能整理內容 你可以依據偏好儲存及分類內容。

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. 依附元件必須依序宣告。您必須在 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);
    }
}

您可以在 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 自動執行這項程序,並產生您以手動方式編寫的程式碼。