[译]手动依赖注入

568 阅读8分钟

Android推荐的应用程序体系结构鼓励您将代码划分为多个类,以从关注点分离中受益,该原则是层次结构中的每个类都有一个定义的职责。这导致需要将更多的较小的类连接在一起,以实现彼此的依赖性。

Android应用程序的应用程序图模型

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

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

依赖注入有助于建立这些连接,并使您可以换出实施进行测试。例如,当测试依赖于存储库的ViewModel时,可以通过伪造或模拟传递存储库的不同实现,以测试不同的情况。

手动依赖注入基础

这部分包括如何在一个真正的Android应用手动依赖注入。它通过迭代方法逐步介绍了如何开始在应用程序中使用依赖项注入。。该方法会不断改进,直到达到与Dagger自动为您生成的代码相似为止。有关Dagger的更多信息,请阅读Dagger基础知识。

将流程视为应用程序中与功能相对应的一组屏幕。登录,注册和签出都是流程的示例。

典型Android应用程序的登录流程中,LoginActivity取决于LoginViewModel,而后者又取决于UserRepository。然后,UserRepository依赖于UserLocalDataSource和UserRemoteDataSource,后者又依赖于Retrofit。

LoginActivity是登录流程的入口点,用户与Activity进行交互。因此,LoginActivity需要创建具有所有依赖项的LoginViewModel。

该流程的Repository和DataSource类如下所示:

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代码如下:

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);
    }
}

使用这种方法存在如下问题:

  • 有大量的模板代码。如果要在代码的另一部分中创建LoginViewModel的另一个实例,则需要重复代码。
  • 依赖关系必须按顺序声明。您必须在LoginViewModel创建之前实例化UserRepository
  • 重用对象很困难。如果要跨多个功能复用UserRepository,则必须使其遵循单例模式。单例模式使测试更加困难,因为所有测试共享相同的单例实例。

使用容器管理依赖项

要解决重用对象的问题,您可以创建自己的依赖项容器类,用于获取依赖项。此容器提供的所有实例可以是公共的。在该示例中,由于您仅需要UserRepository的一个实例,因此可以在需要提供它们的依赖项的情况下将其依赖项设为私有,并可以选择在以后将其公开:

// 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类。

// 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只是一个常规类,在放置在您的Application类中的应用程序之间共享唯一实例。但是,AppContainer没有遵循单例模式;在Kotlin中,它不是对象,在Java中,典型的Singleton.getInstance()方法无法访问它。

现在你可以从application中获取AppContainer的实例并且获取UserRepository的共享实例。

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中共享一个AppContainerActivity包含来自图的对象,并创建其他类可以使用的那些对象的实例。 如果需要在应用程序的更多位置使用LoginViewModel,那么在一个集中的位置创建LoginViewModel实例是有意义的。您可以将LoginViewModel的创建移动到容器中,并为工厂提供该类型的新对象。 LoginViewModelFactory的代码如下所示:

// 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使用它:

// 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(LoginUsernameFragment和LoginPasswordFragment)组成的登录流程。这些视图想要:

  1. 访问需要共享的相同LoginUserData实例,直到登录流程完成。

  2. 当流程再次开始时,创建一个LoginUserData的新实例。

您可以使用登录流容器来实现。登录流开始时需要创建此容器,登录结束时需要将其从内存中删除。

让我们在示例代码中添加一个LoginContainer。您希望能够在应用程序中创建LoginContainer的多个实例,因此不要将其设为单例,而应使其成为具有AppContainer中登录流程需要的依赖项的类。

// 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()中将其删除。

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实例。

因为在这种情况下,您要处理视图生命周期逻辑,所以使用生命周期观察是有意义的。

注意:如果您需要容器来抵抗配置更改,请遵循“保存UI状态”指南。您需要像处理进程终止一样处理它。否则,您的应用可能会在内存较少的设备上丢失状态。

总结

依赖注入是用于创建可扩展且可测试的Android应用程序的好技术。使用容器作为在应用程序不同部分中共享类实例的一种方式,并用作使用工厂创建类实例的集中位置。

当您的应用程序变大时,您将开始看到您编写了很多样板代码(例如工厂),这很容易出错。您还必须自己管理容器的范围和生命周期,优化和丢弃不再需要的容器以释放内存。错误地执行此操作可能会导致应用程序中的细微错误和内存泄漏。

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