手动依赖注入

Android 的推荐应用架构 鼓励将您的代码划分为不同的类,以从关注点分离中获益,关注点分离原则要求层次结构中的每个类都具有一个定义明确的责任。这会导致更多更小的类需要连接在一起以满足彼此的依赖关系。

Android apps are usually made up of many classes, and some of them
    depend on each other.
图 1. Android 应用的应用图模型

类之间的依赖关系可以用图表示,其中每个类都与其依赖的类相连。所有类及其依赖关系的表示构成了应用图。在图 1 中,您可以看到应用图的抽象。当类 A (ViewModel) 依赖于类 B (Repository) 时,有一条从 A 指向 B 的线代表该依赖关系。

依赖注入有助于建立这些连接,并使您能够交换实现以进行测试。例如,在测试依赖于存储库的ViewModel时,您可以传递不同的Repository实现,这些实现使用模拟或存根,以测试不同的情况。

手动依赖注入的基础知识

本节介绍如何在真实的 Android 应用场景中应用手动依赖注入。它将逐步介绍如何在应用中开始使用依赖注入的方法。这种方法会不断改进,直到达到非常类似于 Dagger 自动为您生成的程度。有关 Dagger 的更多信息,请阅读Dagger 基础知识

流程视为应用中的一组屏幕,这些屏幕对应于某个功能。登录、注册和结账都是流程的例子。

在介绍典型 Android 应用的登录流程时,LoginActivity 依赖于 LoginViewModel,而 LoginViewModel 又依赖于 UserRepository。然后,UserRepository 依赖于 UserLocalDataSourceUserRemoteDataSource,而 UserRemoteDataSource 又依赖于 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 自动执行此过程,并生成你原本需要手动编写的相同代码。