Kotlin 39. Dependency Injection and the use of Hilt in Kotlin, Series 2: Manual Dependency Injection

Let's learn Kotlin together: Concept: 26. Dependency Injection dependency injection and the use of Hilt in Kotlin, Series 2: Manual dependency injection

In this series of blogs, we will mainly introduce:

  • Dependency Injection(Dependency Injection) Concept introduction. I have read many introductions about DI on the Internet, but I am in a fog. Here, we introduce it in an easy-to-understand way.
  • An introduction to manual dependency injection. In order to make it easier for everyone to understand Hilt, we first introduce how to achieve dependency injection effects manually.
  • Hilt annotations (annotations) introduction and use cases
  • How to use Hilt in MVVM case

This blog focuses on manual dependency injection.



1 review

In the first blog of the series, we introduced the concept of dependency injection and why it is needed.

In simple terms, in a scenario such as object A needs (depends on) another instance of object B, transferring the task of creating objects to others and directly using dependencies is called dependency injection (DI). In the previous blog, we also took the example of instantiating the Car class to explain how to prevent the coupling between this car object and various objects it depends on, such as wheels and engines.

For example, in the following example, when the Car instance is created, a new Wheel instance will be created, which is coupling. The purpose of DI is to prevent this from happening.

class Car {
    
    <!-- -->
    private val wheelA = Wheel()

    fun start() {
    
    <!-- -->
        engine.start()
    }
}

fun main(args: Array) {
    
    <!-- -->
    val car = Car()
    car.start()
}

In addition, we also listed three implementations of dependency injection:

  • constructor injection (class constructor injection): dependencies are provided through the class constructor.
  • setter injection (class field injection): The client exposes a setter method that the injector uses to inject dependencies.
  • interface injection: Dependency provides an injector method that can inject dependencies into any clients that pass it. Clients must implement an interface that exposes a setter method that accepts dependencies.

2 Definition of Hilt

Hilt provides a standard way to use DI in your application by providing containers for every Android class in your project and managing their lifecycles automatically.

From the definition of Hilt, we can conclude that Hilt mainly accomplishes two things:

  • Provides "containers" (containers) to hold various dependencies;
  • Automatically manage the "lifecycles" of these "containers" (containers).

In the following chapters, we will explain the meaning and significance of the above two things through manual dependency injection.

3 Manual dependency injection: provide a container to manage various dependencies

Like the previous blog, we also use some examples to explain the concept of manual dependency injection.

The following figure is a basic architecture of MVVM:

Please add a picture description

The arrow in the figure above is unidirectional, which means that one end of the arrow depends on the other end. For example, Activity/Fragmentdepends on ViewModel, and ViewModeldepends on Repository. In Android's MVVM architecture, dependency injection means injecting ViewModelinstances of (instance) into Activity/Fragmentclasses, and for the same reason, Repositoryinjecting instances of into ViewModelclasses. By analogy, instances of Modeland also need to be injected into the class.RemoteDataSourceRepository

In fact, what we usually do is to Activity/Fragmentdirectly create a new one in it ViewModel. It seems convenient, but in fact, isn't this very similar to the coupling example above? If we only have one Activity/Fragmentand one dependent ViewModel, that's fine, but if the relationship is complicated, the advantages of dependency injection are obvious.

For example, when it is necessary to implement a user login function, the MVVM architecture should be as follows:

Please add a picture description

Here, LoginActivitydepends on LoginViewModel, LoginViewModeldepends on UserRepository, UserRepositorydepends on UserLocalDataSourceand UserRemoteDataSource. UserRemoteDataSourcedepends on Retrofit.

When not using the idea of ​​​​dependency injection, LoginActivityit should roughly look like this:

class LoginActivity: Activity() {
    
    <!-- -->

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
    
    <!-- -->
        super.onCreate(savedInstanceState)
        // 实例化LoginViewModel
        loginViewModel = LoginViewModel(userRepository)
    }
}

In order to satisfy LoginViewModelthe instantiation of , we need to pass in an UserRepositoryinstance parameter of userRepository. This doesn't end there, because UserRepositoryin turn depends on UserLocalDataSourceas well UserRemoteDataSource. One ring after another. So, if we LoginActivitywrite it completely, it will roughly look like this:

class LoginActivity: Activity() {
    
    <!-- -->

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
    
    <!-- -->
        super.onCreate(savedInstanceState)

        /******新增代码 Begin ******/
        val retrofit = Retrofit.Builder()
            .baseUrl("https://example.com")
            .build()
            .create(LoginService::class.java)

        val remoteDataSource = UserRemoteDataSource(retrofit)
        val localDataSource = UserLocalDataSource()

        val userRepository = UserRepository(localDataSource, remoteDataSource)
        /******新增代码 End ******/
       
        loginViewModel = LoginViewModel(userRepository)
    }
}

The "new code" section in the above code contains the interlocking relationship between each dependency. So, in LoginActivitythe class, we create instances of all related dependencies. But as we originally envisioned, LoginActivityas long as there are objects in the class loginViewModel, the others do not need to appear in LoginActivitythe class. What we need is something like this clean code:

class LoginActivity: Activity() {
    
    <!-- -->

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
    
    <!-- -->
        super.onCreate(savedInstanceState)
        // 获取 loginViewModel 对象
        loginViewModel = XXXX
    }
}

What we can do is to create a new AppContainerclass and throw all the newly added code into it:

class AppContainer {
    
    <!-- -->

    private val retrofit = Retrofit.Builder()
                            .baseUrl("https://example.com")
                            .build()
                            .create(LoginService::class.java)

    private val remoteDataSource = UserRemoteDataSource(retrofit)
    private val localDataSource = UserLocalDataSource()

    val userRepository = UserRepository(localDataSource, remoteDataSource)
}

The newly created AppContainerclass cannot be LoginActivityused in the class, because AppContainerthese "dependencies" in the class need to be used globally in the entire application (application), so the AppContainerinstance of the class needs to be placed Application()in the subclass of :

We create a MyApplicationnew class that inherits from Application():

class MyApplication : Application() {
    
    <!-- -->
    val appContainer = AppContainer()
}

In this way, LoginActivityit can be used like this in :

class LoginActivity: Activity() {
    
    <!-- -->

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
    
    <!-- -->
        super.onCreate(savedInstanceState)
        // 获取 loginViewModel 对象
        // loginViewModel = XXXX 改成下面代码:
        val appContainer = (application as MyApplication).appContainer
        loginViewModel = LoginViewModel(appContainer.userRepository)        
    }
}

So far, LoginActivitythe coding style of dependency injection can be seen in the class.

The object we implement here loginViewModeluses the dependency injection method of Constructor Injection: loginViewModel = LoginViewModel(appContainer.userRepository).

Furthermore, if in our Android application, in addition to LoginActivitythe class, other classes also need LoginViewModelinstance objects of the class, then we cannot create new instance objects of LoginActivitythe class . LoginViewModelIt's still the old way, as one of the "dependencies", we need to put the implemented LoginViewModelinstance objects into "containers" (containers). Here, you need to use the design pattern of the factory pattern, create a new Factoryinterface, then LoginViewModelFactoryimplement this interface in the class, and return an LoginViewModelinstance of :

interface Factory<T> {
    
    <!-- -->
    fun create(): T
}

class LoginViewModelFactory(private val userRepository: UserRepository) : Factory {
    
    <!-- -->
    override fun create(): LoginViewModel {
    
    <!-- -->
        return LoginViewModel(userRepository)
    }
}

The next thing is simple, we put this new LoginViewModelFactoryfactory class in AppContainer:

class AppContainer {
    
    <!-- -->

    private val retrofit = Retrofit.Builder()
                            .baseUrl("https://example.com")
                            .build()
                            .create(LoginService::class.java)

    private val remoteDataSource = UserRemoteDataSource(retrofit)
    private val localDataSource = UserLocalDataSource()

    val userRepository = UserRepository(localDataSource, remoteDataSource)

    // 新建一个 loginViewModelFactory 对象,在整个application范围内都可以使用
    val loginViewModelFactory = LoginViewModelFactory(userRepository)  
}

Then LoginActivityit can be used directly in the class (instead of creating a new LoginViewModelinstance object of the as before):

class LoginActivity: Activity() {
    
    <!-- -->

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
    
    <!-- -->
        super.onCreate(savedInstanceState)
        // 获取 loginViewModel 对象
        // loginViewModel = XXXX 改成下面代码:
        val appContainer = (application as MyApplication).appContainer
        //loginViewModel = LoginViewModel(appContainer.userRepository) 替换成下面代码:  
        loginViewModel = appContainer.loginViewModelFactory.create()    
    }
}

4 Manual Dependency Injection: Manual Lifecycle Management

Now many Android applications support multiple users, so we need to expand the above application functions: record different user login information. This requires LoginActivityadding new functions to achieve the following goals:

  • Keep access to the class instance object during the user's login LoginUserData, and release the resource after logging out;
  • When a new user logs in, create a new LoginUserDataclass instance object.

At this time, we need to add a LoginContainerclass to store LoginUserDatathe instance objects of the class and LoginViewModelFactorythe instance objects of the class. (TIPS: The dependencies are placed in the container, that is, the instance objects of various classes)

class LoginContainer(val userRepository: UserRepository) {
    
    <!-- -->

    val loginData = LoginUserData()

    val loginViewModelFactory = LoginViewModelFactory(userRepository)
}

Then LoginContainerapply to the previous AppContainer:

class AppContainer {
    
    <!-- -->

    private val retrofit = Retrofit.Builder()
                            .baseUrl("https://example.com")
                            .build()
                            .create(LoginService::class.java)

    private val remoteDataSource = UserRemoteDataSource(retrofit)
    private val localDataSource = UserLocalDataSource()

    val userRepository = UserRepository(localDataSource, remoteDataSource)
   
    //val loginViewModelFactory = LoginViewModelFactory(userRepository)
    // loginViewModelFactory 的实现已经放到LoginContainer中,此处不再需要
   
    // 新建一个loginContainer变量,类型是LoginContainer,初始值是null
    // 因为当用户退出时,其值时null
    var loginContainer: LoginContainer? = null 
}

Next, back to LoginActivitythe class, we need to get the instance object through in the stage LoginActivityof the class onCreate()(user login) , and release the corresponding resources in the stage (user exit):loginContainerLoginUserDataonDestroy()

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)
        // 获取 loginViewModel 对象
        // loginViewModel = XXXX 改成下面代码:
        appContainer = (application as MyApplication).appContainer
        //loginViewModel = LoginViewModel(appContainer.userRepository) 替换成下面代码:  
        //loginViewModel = appContainer.loginViewModelFactory.create() 替换成下面代码:  

        // 用户登录,实例化LoginContainer,得到appContainer中的loginContainer 对象
        appContainer.loginContainer = LoginContainer(appContainer.userRepository)

        // loginViewModel 对象的获取比原来多了一层 loginContainer 对象
        loginViewModel = appContainer.loginContainer.loginViewModelFactory.create()
       
        loginData = appContainer.loginContainer.loginData
    }

    override fun onDestroy() {
    
    <!-- -->
        // 用户退出,释放资源
        appContainer.loginContainer = null
        super.onDestroy()
    }
}

5 summary

Here, we summarize what manual dependency injection has done:

  • First, create a new AppContainerclass and put LoginViewModelall the dependencies needed into it;
  • In order to use instances of in other places in the application (except LoginActivity) LoginViewModel, we use the factory class design pattern AppContainerto implement an loginViewModelFactoryobject in the class container;
  • Finally, in order to realize the login and logout of different users, we created a new LoginContainerclass container and put it AppContainerin the class container, and then in LoginActivitythe can onCreate()get the user login information in loginData, and after the user logs out, it is in LoginActivitythe onDestroy()Release resources.

It is foreseeable that when our application functions become more and more complex (now it is only one of the login functions), manual dependency injection will become unmaintainable. This is why Hilt is used.

Guess you like

Origin blog.csdn.net/zyctimes/article/details/129218481