手动依赖注入

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,后者又依赖于 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. 依赖项必须按顺序声明。您必须先实例化 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);
}

由于这些依赖项在整个应用中都会用到,因此需要将其放置在所有 Activity 都可以使用的公共位置: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。相反,您有一个在所有 Activity 之间共享的 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);
    }
}

您可以将 LoginViewModelFactory 包含在 AppContainer 中,并让 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 负责管理该容器的生命周期。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 部分中,您将学习如何使用 Dagger 自动化此过程,并生成您原本需要手动编写的相同代码。