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.
Article directory
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:
The arrow in the figure above is unidirectional, which means that one end of the arrow depends on the other end. For example, Activity/Fragment
depends on ViewModel
, and ViewModel
depends on Repository
. In Android's MVVM architecture, dependency injection means injecting ViewModel
instances of (instance) into Activity/Fragment
classes, and for the same reason, Repository
injecting instances of into ViewModel
classes. By analogy, instances of Model
and also need to be injected into the class.RemoteDataSource
Repository
In fact, what we usually do is to Activity/Fragment
directly 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/Fragment
and 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:
Here, LoginActivity
depends on LoginViewModel
, LoginViewModel
depends on UserRepository
, UserRepository
depends on UserLocalDataSource
and UserRemoteDataSource
. UserRemoteDataSource
depends on Retrofit
.
When not using the idea of dependency injection, LoginActivity
it 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 LoginViewModel
the instantiation of , we need to pass in an UserRepository
instance parameter of userRepository
. This doesn't end there, because UserRepository
in turn depends on UserLocalDataSource
as well UserRemoteDataSource
. One ring after another. So, if we LoginActivity
write 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 LoginActivity
the class, we create instances of all related dependencies. But as we originally envisioned, LoginActivity
as long as there are objects in the class loginViewModel
, the others do not need to appear in LoginActivity
the 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 AppContainer
class 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 AppContainer
class cannot be LoginActivity
used in the class, because AppContainer
these "dependencies" in the class need to be used globally in the entire application (application), so the AppContainer
instance of the class needs to be placed Application()
in the subclass of :
We create a MyApplication
new class that inherits from Application()
:
class MyApplication : Application() {
<!-- -->
val appContainer = AppContainer()
}
In this way, LoginActivity
it 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, LoginActivity
the coding style of dependency injection can be seen in the class.
The object we implement here loginViewModel
uses the dependency injection method of Constructor Injection: loginViewModel = LoginViewModel(appContainer.userRepository)
.
Furthermore, if in our Android application, in addition to LoginActivity
the class, other classes also need LoginViewModel
instance objects of the class, then we cannot create new instance objects of LoginActivity
the class . LoginViewModel
It's still the old way, as one of the "dependencies", we need to put the implemented LoginViewModel
instance objects into "containers" (containers). Here, you need to use the design pattern of the factory pattern, create a new Factory
interface, then LoginViewModelFactory
implement this interface in the class, and return an LoginViewModel
instance 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 LoginViewModelFactory
factory 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 LoginActivity
it can be used directly in the class (instead of creating a new LoginViewModel
instance 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 LoginActivity
adding 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
LoginUserData
class instance object.
At this time, we need to add a LoginContainer
class to store LoginUserData
the instance objects of the class and LoginViewModelFactory
the 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 LoginContainer
apply 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 LoginActivity
the class, we need to get the instance object through in the stage LoginActivity
of the class onCreate()
(user login) , and release the corresponding resources in the stage (user exit):loginContainer
LoginUserData
onDestroy()
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
AppContainer
class and putLoginViewModel
all 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 patternAppContainer
to implement anloginViewModelFactory
object in the class container; - Finally, in order to realize the login and logout of different users, we created a new
LoginContainer
class container and put itAppContainer
in the class container, and then inLoginActivity
the canonCreate()
get the user login information inloginData
, and after the user logs out, it is inLoginActivity
theonDestroy()
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.