手动依赖注入

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

您可以将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. 优化应用程序图和流程容器也可能很困难。您需要记住删除不需要的实例,具体取决于您所在的流程。

假设您有一个登录流程,它由一个活动(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 自动执行此过程并生成您本来需要手动编写的相同代码。