Google 致力于为黑人社区推动种族平等。查看具体举措

手动依赖项注入

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 是登录流程的入口点,用户与 Activity 进行交互。因此,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);
    }
    

由于这些依赖项在整个应用中使用,因此需要将它们放置在所有 Activity 都可以使用的通用位置:应用类。创建一个包含 AppContainer 实例的自定义应用类。

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。相反,您可以在所有 Activity 中共享 AppContainer,其包含图表中的对象并创建其他类可以使用的对象实例。

如果需要在应用的更多位置使用 LoginViewModel,则具有一个可创建 LoginViewModel 实例的集中位置是有必要的。您可以将 LoginViewModel 的创建移至容器,并为工厂提供该类型的新对象。LoginViewModelFactory 的代码如下所示:

Kotlin

    // Definition of a Factory interface with a function to create objects of a type
    interface Factory {
        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 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. 对应用图表和流程容器进行优化可能也非常困难。您需要记住删除不需要的实例,具体取决于您所处的流程。

假设您的登录流程由一个 Activity (LoginActivity) 和多个 Fragment(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;
    }
    

拥有某个流程专用的容器后,必须决定何时创建和删除容器实例。由于您的登录流程在 Activity (LoginActivity) 中是独立的,因此该 Activity 是管理该容器生命周期的 Activity。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 一样,登录 Fragment 可以从 AppContainer 访问 LoginContainer 并使用共享的 LoginUserData 实例。

因为在这种情况下,您需要处理视图生命周期逻辑,因此使用生命周期观察较为合理。

总结

依赖项注入对于创建可扩展且可测试的 Android 应用而言是一项适合的技术。将容器作为在应用的不同部分共享各个类实例的一种方式,以及使用工厂创建各个类实例的集中位置。

当应用变大时,您会发现您编写了大量样板代码(例如工厂),这可能容易出错。您还必须自行管理容器的范围和生命周期,优化并舍弃不再需要的容器以释放内存。如果操作不当,可能会导致应用出现微小错误和内存泄露。

下一页中,您将学习如何使用 Dagger 自动执行该过程,并生成与手动编写相同的代码。