又是很久沒有更新掘金了~其實個人我的博客上一直有記筆記記筆記,不過沒有整理到這邊來。最近剛換了份工做,以前也分享過面經兒——兩年Android開發大廠面試經驗,忙於熟悉新業務,本身要學習增強的東西還不少,好好學習,每天向上~java
Dagger 的名字取自有向無環圖 DAG (directed acyclic graph),由於程序裏的依賴關係拼接起來就是一個或者多個有向無環圖。android
首先理解一下什麼是依賴注入。一個類 UserRepository 中有一個 UserRemoteDataSource 類型的屬性, 那 UserRemoteDataSource 即是 UserRepository 的依賴,初始化這個依賴能夠有兩種方法,一種是在類內部本身初始化,另外一種是由外部初始化傳入(便是依賴注入,關鍵在於初始化是誰作的)。git
這種由外部初始化的方式均可以叫作依賴注入,而 Dagger 則爲依賴注入提供了一種更簡單的方式。github
接下來用 Dagger2 實現以下圖所示的依賴關係:web
首先添加 Dagger2 的依賴:面試
apply plugin: 'kotlin-kapt'
dependencies {
implementation 'com.google.dagger:dagger:2.x'
kapt 'com.google.dagger:dagger-compiler:2.x'
}
複製代碼
而後定義 UserRepository 類:markdown
class UserRepository @Inject constructor(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
) {
fun load(): String = "${localDataSource.load()} - ${remoteDataSource.load()}"
}
複製代碼
這裏經過在構造器上添加 @Inject
註解告知 Dagger 如何建立 UserRepository 實例,它的依賴項爲 UserLocalDataSource 和 UserRemoteDataSource, 爲了讓 Dagger 知道如何建立這兩個依賴項實例,也需使用 @Inject
註解:app
interface DataSource {
fun load(): String
}
class UserRemoteDataSource @Inject constructor() : DataSource {
override fun load(): String = "UserRemoteDataSource.load()"
}
class UserLocalDataSource @Inject constructor() : DataSource {
override fun load(): String = "UserLocalDataSource.load()"
}
複製代碼
這樣 Dagger 便會知道如何建立它們。ide
而後在 ViewModel 中也能夠經過這種方式依賴 UserRepository:函數
class LoginViewModel @Inject constructor(private val userRepository: UserRepository) {
fun loadData(): String = userRepository.load()
}
複製代碼
接下來在 Activity 中怎麼引用 LoginViewModel 實例呢?顯然它不能跟上面同樣在構造方法中使用 @Inject
註解了,由於 Activity 實例化是由系統完成的,沒法由 Dagger 去進行實例化,所以這裏不能由構造函數注入 LoginViewModel, 而應該使用屬性注入(注意:注入的屬性不能爲私有屬性):
class LoginActivity : AppCompatActivity() {
@Inject
lateinit var viewModel: LoginViewModel
}
複製代碼
光是這樣顯然是不夠的,咱們還須要告知 Dagger 要求注入依賴項的對象是誰(LoginActivity),在此以前就得先看看 Dagger Component 的概念。
Dagger 能夠在項目中建立一個有向無環圖(依賴關係),而後它能夠從該圖中瞭解在須要這些依賴項時從何處獲取它們。爲了讓 Dagger 執行此操做,咱們須要建立一個接口,並使用 @Component
爲其添加註解,在上面的基礎用法裏,Dagger 須要知道 LoginActivity 中的 LoginViewModel 依賴應該怎麼建立:
對於屬性注入,Dagger 須要讓 LoginActivity 訪問該圖才能提供所需的 LoginViewModel 依賴實例,爲此應在 Component 接口中提供一個函數,讓該函數將請求注入的對象做爲參數。
@Component
interface ApplicationComponent {
fun inject(activity: LoginActivity)
}
複製代碼
@Component
會讓 Dagger 生成一個容器,其中應包含知足其提供的類型所需的全部依賴項,這稱爲 Dagger 組件,它包含一個圖,其中包括 Dagger 知道如何提供的對象及其各自的依賴項。在 build 項目時 Dagger 會生成 ApplicationComponent 接口的實現:DaggerApplicationComponent。Dagger 經過其註解處理器(處理 @Inject
等)建立了一個依賴關係圖,包含了這些依賴之間的關係,而且將 ApplicationComponent 接口中咱們加入的方法(inject 方法,方法名能夠隨意)涉及的依賴做爲入口點。
在聲明瞭 inject 方法後,咱們能夠這樣實現注入:
class LoginActivity : AppCompatActivity() {
@Inject
lateinit var viewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
DaggerApplicationComponent.create().inject(this)
super.onCreate(savedInstanceState)
println(viewModel.loadData())
}
}
複製代碼
在上面的 LoginActivity 中,若是不經過屬性注入 LoginViewModel 依賴的話,還能夠經過構造器注入的方式,這裏須要在 @Component
接口內定義返回所需類(即 LoginViewModel)的實例的函數,這樣 Dagger 就知道怎麼建立它了。
@Component
interface ApplicationComponent {
fun viewModel(): LoginViewModel
}
複製代碼
而後在 LoginActivity 中給 viewModel 賦值:
class LoginActivity : AppCompatActivity() {
private val viewModel: LoginViewModel = DaggerApplicationComponent.create().viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
println(viewModel.loadData())
}
}
複製代碼
除了 @Inject
以外,還有一種方法可告知 Dagger 如何提供類實例 —— @Module
和 @Provides
。
假設將上例中的 UserRemoteDataSource 構造方法改一下,其依賴於 UserService 類:
class UserRemoteDataSource @Inject constructor(private val userService: UserService) : DataSource {
override fun load(): String = userService.load()
}
class UserService private constructor() {
class Builder {
fun build(): UserService = UserService()
}
fun load(): String = "UserService.load()"
}
複製代碼
能夠看到 UserService 對外的建立方法不是經過構造函數,而是經過其 Builder 類來實例化的,那這裏就不能使用 @Inject
來進行注入依賴了,在實際項目中這樣的場景也很多,咱們可使用 @Module
註解來定義 Dagger 模塊,使用 @Provides
註解定義依賴項:
@Module
class NetworkModule {
@Provides
fun provideUserService(): UserService {
return UserService.Builder().build()
}
}
@Component(modules = [NetworkModule::class])
interface ApplicationComponent {
fun viewModel(): LoginViewModel
}
複製代碼
上面的 Module 註解用來告知 Dagger 這是一個 Module 類,而 Provides 註解用來告訴 Dagger 怎麼建立該方法返回的類型的實例。這裏若是要建立的 UserService 實例須要依賴其餘對象,能夠將依賴做爲方法參數傳入,固然該依賴也須要使用 Inject 等註解來告知 Dagger 怎麼去建立它。
在上面經過 DaggerApplicationComponent.create().viewModel()
建立的 ViewModel 實例,同一個 DaggerApplicationComponent 對象調用屢次 viewModel() 方法,Dagger 默認會建立多個不一樣的 ViewModel 實例。能夠驗證:
val component = DaggerApplicationComponent.create()
println(component.viewModel() == component.viewModel()) // false
複製代碼
若是須要讓 Dagger 中同一個 DaggerApplicationComponent 對象始終返回同一 viewModel 實例,可使用做用域註解(@Singleton
)將某個對象的生命週期限定爲其 Component 對象的生命週期,這意味着每次獲取時都會使用該依賴項的同一實例。固然,若是是不一樣的 DaggerApplicationComponent 對象,那麼 viewModel 天然也不同。
@Singleton
@Component
interface ApplicationComponent {
fun viewModel(): LoginViewModel
}
@Singleton
class LoginViewModel @Inject constructor(private val userRepository: UserRepository) {
fun loadData(): String = userRepository.load()
}
val component = DaggerApplicationComponent.create()
println(component.viewModel() == component.viewModel()) // true
複製代碼
@Singleton
是 javax.inject
軟件包隨附的惟一一個做用域註解,也能夠建立並使用自定義做用域註解,使用方式跟 @Singleton
同樣:
@Scope
@MustBeDocumented
@Retention(value = AnnotationRetention.RUNTIME)
annotation class MyCustomScope
複製代碼
也能夠對 Module 中提供的依賴使用與 Component 同樣的做用域註解,這樣 Provides 方法中返回的依賴也就與 Component 對象有了同樣的生命週期了:
@Module
class NetworkModule {
@Singleton
@Provides
fun provideUserService(): UserService {
return UserService.Builder().build()
}
}
複製代碼
注意:使用做用域註解的依賴只能在帶有相同做用域註解的 Component 中使用。
在 Android 中咱們一般會建立一個位於 Application 類中的 Dagger 圖,這樣就將該圖附加到應用的生命週期裏了:
class MainApplication : Application() {
val applicationGraph = DaggerApplicationComponent.create()
}
複製代碼
因而上面與該 ApplicationComponent 使用同一 @Singleton
註解的依賴項也將得到與其一致的生命週期,經過該 ApplicationComponent 對象中的方法獲取到的依賴對象(applicationGraph.viewModel()等),其生命週期將與其一致。
做用域限定規則:
上面咱們在 MainApplication 應用類中建立了一個 DaggerApplicationComponent 實例,當經過這個 applicationGraph 實例去獲取依賴的時候,對於使用 Singleton 註解的依賴(注意 Component 也須要用 Singleton 註解),則其會跟應用生命週期一致(蕾絲於全局單例)。
如需將 LoginViewModel 的做用域限定爲 LoginActivity 的生命週期,咱們能夠在 LoginActivity 中建立 DaggerApplicationComponent 實例屬性,而後經過這個實例去獲取 LoginViewModel 對象,因爲此時這個 DaggerApplicationComponent 實例的生命週期跟 LoginActivity 綁定,則 LoginViewModel 的做用域也會被限定爲 LoginActivity 的生命週期:
class LoginActivity : AppCompatActivity() {
@Inject
lateinit var viewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
DaggerApplicationComponent.create().inject(this)
super.onCreate(savedInstanceState)
println("Current: $viewModel, ${viewModel.loadData()}")
}
}
複製代碼
經過上面這個方式,能夠將 LoginViewModel 的做用域限定爲 LoginActivity 的生命週期,但因爲 DaggerApplicationComponent 被定義爲應用全局的依賴圖,而這裏咱們所需的依賴僅是 LoginActivity 相關的,且又要限制 LoginViewModel 的做用域,爲此又在 LoginActivity 中反覆建立 DaggerApplicationComponent 實例,顯得不那麼好看。並且 LoginViewModel 的依賴應該只有登錄這些場景纔會使用,能夠避免將其放到應用全局的依賴圖裏。所以這裏能夠考慮使用子組件(子依賴圖),應用組件 DaggerApplicationComponent 中只放一些共有的依賴圖,不一樣的應用場景考慮增長子圖,子圖存放特有應用/業務場景的依賴,如 LoginViewModle 就放在登錄子圖 LoginComponent 中。
子組件是繼承並擴展父組件的對象圖的組件,所以,父組件中提供的全部對象也將在子組件中提供,這樣子組件中的對象就能夠依賴於父組件提供的對象(共有),父組件向子組件提供的對象的做用域仍限定爲父組件的生命週期。
一、定義子組件
@Subcomponent
interface LoginComponent {
// 定義子組件 Factory 使得 ApplicationComponent 知道如何建立 LoginComponent 的實例
@Subcomponent.Factory
interface Factory {
fun create(): LoginComponent
}
fun inject(loginActivity: LoginActivity)
}
複製代碼
二、建立子組件的模塊
@Module(subcomponents = [LoginComponent::class])
class SubcomponentsModule {
}
複製代碼
三、將聲明子組件的模塊添加到 ApplicationComponent 中
@Singleton
@Component(modules = [NetworkModule::class, SubcomponentsModule::class])
interface ApplicationComponent {
// 告知 Dagger 如何建立 LoginComponent.Factory
// 而 LoginComponent.Factory 會用來建立子組件 LoginComponent 實例
fun loginComponent(): LoginComponent.Factory
// 將 UserRepository 依賴放入 ApplicationComponent 圖中
fun repository(): UserRepository
}
複製代碼
注意,這裏 ApplicationComponent 以前的 inject 方法刪除了,由於 inject 放到了專門的登錄子組件中。增長了一個返回 UserRepository 實例的 repository() 方法,若是不加這個方法且 UserRepository 沒有跟 ApplicationComponent 同樣用 Singleton 註解標識的話,UserRepository 就會被包含到 LoginComponent 依賴圖中,由於它被 LoginViewModel 引用了,這是目前使用它的惟一位置,而 LoginViewModel 是 LoginComponent 子組件中的依賴。如今的依賴關係以下圖:
四、使用
class LoginActivity : AppCompatActivity() {
@Inject
lateinit var viewModel: LoginViewModel
lateinit var loginComponent: LoginComponent
override fun onCreate(savedInstanceState: Bundle?) {
loginComponent = (application as MainApplication).applicationGraph.loginComponent().create()
loginComponent.inject(this)
super.onCreate(savedInstanceState)
println("Current: $viewModel, ${viewModel.loadData()}")
}
}
複製代碼
LoginComponent 是在 Activity 的 onCreate() 方法中建立的,將隨着 Activity 的銷燬而被隱式銷燬。這樣它就具有了跟 LoginActivity 一致的生命週期了。
能夠建立自定義做用域註解,並使用該做用域爲 LoginComponent 和 LoginViewModel 添加註解。此時若是存在兩個須要 LoginViewModel 依賴的 Fragment 頁面,則咱們在這兩個 Fragment 中注入的 LoginViewModel 都是同一個實例(注意不能使用 Singleton 註解,由於其已經被父組件佔用了)。
能夠將該註解命名爲 ActivityScope, 這裏不使用 LoginScope, 由於該註解也能夠被其餘蕾絲的場景(同級組件)使用。
@Scope
@Retention(value = AnnotationRetention.RUNTIME)
annotation class ActivityScope
@ActivityScope
@Subcomponent
interface LoginComponent {
// 定義子組件 Factory 使得 ApplicationComponent 知道如何建立 LoginComponent 的實例
@Subcomponent.Factory
interface Factory {
fun create(): LoginComponent
}
fun inject(loginActivity: LoginActivity)
fun inject(fragment: LoginAFragment)
fun inject(fragment: LoginBFragment)
}
@ActivityScope
class LoginViewModel @Inject constructor(private val userRepository: UserRepository) { ... }
複製代碼
而後在這兩個 Fragment 中能夠注入 LoginViewModel 實例:
class LoginAFragment: Fragment() {
@Inject
lateinit var loginViewModel: LoginViewModel
override fun onAttach(context: Context) {
super.onAttach(context)
(activity as LoginActivity).loginComponent.inject(this)
}
}
複製代碼
一個 Component 能夠經過設置 dependencies 依賴另外一個 Component 組件,而後它即可獲取到所依賴的 Component 中暴露的依賴。
咱們首先建立一個零件組件:
@Singleton
class Memory @Inject constructor() {
override fun toString(): String = "Memory"
}
@Singleton
class Cpu @Inject constructor() {
override fun toString(): String = "Cup"
}
@Singleton
@Component
interface PartComponent {
fun cup(): Cpu
fun memory(): Memory
}
複製代碼
這裏使用 Singleton 註解將 Memory 和 Cpu 限制到 PartComponent 零件組件的做用域裏。
接着假設一個 Computer 類須要依賴 Cpu 和 Memory:
@ActivityScope // 看實際場景,可加可不加
class Computer @Inject constructor(
private val cpu: Cpu,
private val memory: Memory
) {
override fun toString(): String = "$cpu & $memory"
}
複製代碼
而後定義一個依賴 PartComponent 組件的 ComputerComponent 組件:
// 須要加做用域,在新版本 Dagger 中沒加做用域的話,依賴的 PartComponent 組件若是有做用域註解則會編譯報錯
// ComputerComponent (unscoped) cannot depend on scoped components
@ActivityScope
@Component(dependencies = [PartComponent::class])
interface ComputerComponent {
fun inject(activity: MainActivity)
}
複製代碼
在這裏 ComputerComponent 依賴 PartComponent 組件,所以它可獲取到 PartComponent 接口中暴露的依賴:
class MainActivity : AppCompatActivity() {
@Inject
lateinit var computer: Computer
override fun onCreate(savedInstanceState: Bundle?) {
val partComponent = DaggerPartComponent.create()
val computerComponent = DaggerComputerComponent.builder().partComponent(partComponent).build()
computerComponent.inject(this)
super.onCreate(savedInstanceState)
println(computer) // Cup & Memory
}
}
複製代碼
這裏咱們若是將 PartComponent 接口中暴露依賴的兩個方法刪掉,那麼 ComputerComponent 便不能訪問到 Cpu 和 Memory 了,編譯報錯,由於它會直接嘗試去找 Computer 的兩個依賴,而後發現這兩個依賴被 Singleton 註解標識了,而 ComputerComponent 自身是 ActivityScope 做用域的,所以編譯報錯(以前咱們說依賴若是有做用域註解的話,其做用域註解要和組件的一致)。
關於組件依賴還可舉例,依舊用上面的 Cpu 和 Memory 類:
class Cpu {
override fun toString(): String = "Cup"
}
class Memory {
override fun toString(): String = "Memory"
}
@Module
class PartModule {
@Provides
fun provideCpu(): Cpu = Cpu()
@Provides
fun provideMemory(): Memory = Memory()
}
複製代碼
這裏咱們經過 Module 的方式提供依賴。接着在 PartComponent 組件中聲明模塊,並暴露出獲取依賴的方法。
@Component(modules = [PartModule::class])
interface PartComponent {
fun cup(): Cpu
fun memory(): Memory
}
複製代碼
而後跟上面同樣讓 ComputerComponent 去依賴 PartComponent 並注入 Computer,這裏能夠視狀況增長做用域限定。
Google 在 Jetpack 包中增長了 Hilt 組件,它是專門針對 Android 平臺作的一個依賴注入庫,底層依舊是基於 Dagger, 所以學習 Dagger 仍是挺有必要的。Hilt 並非對 Dagger 作了優化,而是針對 Android 開發制定了一套場景化的規則,剛學習了 Dagger 的一些用法,後續有時間接着學習一下 Hilt 及其實現原理。
Dagger 的用法會比較複雜一點,這篇文章結合了我本身的一些理解和用例,講解了一下 Dagger 的基礎用法,但願對有須要的同窗能有所幫助。
文中內容若有錯誤歡迎指出,共同進步!以爲不錯的留個贊再走哈~
參考: