Android依賴注入框架-Hilt詳解剪决,官方基于Dagger封裝適配Android而開發(fā)灵汪,史上最詳細解析
記得2年前檀训,我發(fā)布過一篇關于Android依賴注入框架的文章,Dagger在Android開發(fā)中上手難度較高识虚,當時就給大家推薦了Koin,感興趣的同學可以去看看那篇文章肢扯。http://www.reibang.com/p/bccb93a78cee。(Koin與Hilt各有優(yōu)勢)
現(xiàn)在担锤,Android有了官方的依賴注入工具蔚晨,那就是Hilt,基于Dagger上的封裝,所以Hilt跟Dagger很相似肛循,谷歌官方也有專門的Hilt中文文檔铭腕,不過版本比較老,是2.28版本多糠,地址為:https://developer.android.google.cn/training/dependency-injection/hilt-android累舷,感興趣的同學可以去看看。本例是基于當前最新的版本:2.40.5版本所寫夹孔,最新的版本跟老的版本有很多的差別被盈,方法和參數(shù)都變動挺大,老的文檔可能不適用于最新的版本搭伤。正所謂用新不用舊只怎,如果想了解最新用法少走歪路的,可以看我這篇文章怜俐,算是比較詳細的吧身堡。
依賴注入是什么,依賴注入有什么好處拍鲤,這些就不多講了贴谎,直接去看我上面以前寫的文章,我們直接進入Hilt的用法講解季稳。本篇的例子在我的github上擅这,地址為:https://github.com/CaesarShao/CaesarHit,感興趣的可以對照著理解景鼠。
本文目錄
依賴方式
開始使用
Activity作用域--最基礎調用
全局作用域及單例模式
ViewModel中調用
Fragment作用域使用
Service作用域使用
自定義View作用域
嵌套使用
重名的用法
接口未指明用法
其他類中獲取方式
依賴方式
首先在根級的build.gradle中加入
buildscript {
...
dependencies {
...
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.40.5'
}
}
然后在app/build.gradle中加入
...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
dependencies {
implementation "com.google.dagger:hilt-android:2.40.5"
kapt "com.google.dagger:hilt-compiler:2.40.5"
implementation "androidx.activity:activity-ktx:1.2.3"http://在activity中可以便捷獲取ViewModel
implementation 'androidx.fragment:fragment-ktx:1.3.4'//在fragment中可以便捷獲取ViewModel
}
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
在文檔中有寫這么一段話:Hilt 使用 Java 8 功能仲翎。如需在項目中啟用 Java 8,請將以下代碼添加到 app/build.gradle
文件中莲蜘。不過目前我創(chuàng)建項目的時候谭确,好像會自動增加這個。
開始使用
首先在我們項目的Application中加上@HiltAndroidApp注釋
@HiltAndroidApp
class MyApp : Application() {
}
接下來就可以開始使用依賴注入了票渠。
我們先來總體講解下逐哈,后面會有詳細的例子說明。依賴注入的方式有2種问顷,一種就是我們自己創(chuàng)建的類昂秃,只要在構造函數(shù)中加上@Inject這個標簽注釋禀梳,就可以在我們需要注入的界面通過@Inject注釋來調用。第二種就是專門給第三方類使用的肠骆,因為第三方的類無法用@Inject標簽注釋算途,我們就需要在項目中添加@Module注解塊,然后在對應的@Module中將需要用到的提供對象new出來蚀腿,用@Provides注釋來標識出嘴瓤,之后我們就可以在其他注入的界面調用。
Hilt的依賴注入也有作用域的概念莉钙,就是你在注入一個對象的時候廓脆,已經(jīng)規(guī)定好了,這個對象在哪里可以被調用磁玉,然后當這個被調用的區(qū)域被銷毀了停忿,那作用域中被注入的對象也會自動銷毀,是不是很方便蚊伞。
Hilt目前提供了以下幾個作用域席赂,并且對應使用的地方我也一并寫出來
Singleton -- SingletonComponent -> Application (這個作用域看著有點像單例,但是就是應用全局作用域时迫,我們單例的對象颅停,必須要在這個作用域中注入才能使用)
ActivityRetainedScoped -- ActivityRetainedComponent ->跟Activity生命周期有聯(lián)系的,例如viewmodel或者其他關聯(lián)的
ActivityScoped -- ActivityComponent -> Activity
ViewModelScoped -- ViewModelComponent -> ViewModel
FragmentScoped -- FragmentComponent -> Fragment
ViewScoped -- ViewWithFragmentComponent -> View(從名字上看别垮,應該是自定義view與fragment的生命周期相關)
ViewScoped -- ViewComponent -> View(一般用于自定義view便监,不過從里面的繼承關系來看扎谎,應該是view與activity的生命周期相關)
ServiceScoped -- ServiceComponent -> Service
好碳想,所有的作用域都已經(jīng)寫出來了,接下來我針對不同的作用域毁靶,來講解胧奔,其實用法都很簡單,每個作用域预吆,我都會用2種注入方法來寫龙填,代表著自己寫的類和第三方類的使用。(其中帶有Global字樣的bean都可以理解為是第三方類來使用)
Activity作用域--最基礎調用
@ActivityScoped
class SimpleData @Inject constructor(){
init {
CaesarHitLog.I("SimpleData類的構造函數(shù)被調用了")
}
fun deal(){
CaesarHitLog.I("SimpleData調用了方法")
}
}
第一個SimpleData類中拐叉,用@ActivityScoped標簽注釋代表是Activity的作用域岩遗,如果什么都不寫就代表是全局的作用域,在構造函數(shù)中用@Inject標簽注入凤瘦,就完成了一個簡單類的注入宿礁。
class SimpleGlobalData {
init {
CaesarHitLog.I("SimpleGlobalData類的構造函數(shù)被調用了")
}
fun deal(){
CaesarHitLog.I("SimpleGlobalData調用了方法")
}
}
@InstallIn(ActivityComponent::class)
@Module
class MyActModule {
@Provides
fun providerSimpleGlobal(): SimpleGlobalData {
return SimpleGlobalData()
}
}
第二個可以理解是第三方類的注入,我們創(chuàng)建一個MyActModule蔬芥,用@Module標簽注釋梆靖,然后再加上@InstallIn(ActivityComponent::class)注解控汉,代表這個作用域是Activity的,最后在里面寫上provider的方法返吻,要用@Provides注釋姑子,記住方法名不能重復,然后返回你需要的對象就可以了测僵,跟Dagger一毛一樣街佑。
如何在Activity中調用呢,也非常簡單捍靠。
@AndroidEntryPoint
class SimpleActivity : AppCompatActivity() {
@Inject
lateinit var simpleData1: SimpleData
@Inject
lateinit var simpleData2: SimpleData
@Inject
lateinit var simpleGlobalData1: SimpleGlobalData
@Inject
lateinit var simpleGlobalData2: SimpleGlobalData
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_simple)
simpleData1.deal()
CaesarHitLog.I("simpleData1的地址:" + simpleData1.toString())
CaesarHitLog.I("simpleData2的地址:" + simpleData2.toString())
simpleGlobalData1.deal()
CaesarHitLog.I("simpleGlobalData1的地址:" + simpleGlobalData1.toString())
CaesarHitLog.I("simpleGlobalData2的地址:" + simpleGlobalData2.toString())
}
}
在Activity中加上@AndroidEntryPoint注解舆乔,然后用@Inject注解獲取對象就可以了,我這邊打印了4個對象的地址剂公,都是不同的希俩,說明獲取成功,并且每次注入獲取都是一個新的對象纲辽。
全局作用域及單例模式
接著就是全局的使用和單例的使用
@Singleton
class SingleData @Inject constructor(){
init {
CaesarHitLog.I("SingleData類的構造函數(shù)被調用了")
}
}
上面的代碼有個@Singleton標簽颜武,代表是全局單例的作用域,如果單單想要全局非單例拖吼,那就去掉@Singleton作用域就可以了鳞上。
class SingleGlobalData {
init {
CaesarHitLog.I("SingleData類的構造函數(shù)被調用了")
}
}
@InstallIn(SingletonComponent::class)
@Module
class MyAppModule {
@Provides
@Singleton
fun providerSingleGlobal(): SingleGlobalData {
return SingleGlobalData()
}
}
第二個就是全局的另外一種寫法,其他的大同小異吊档,就改了一個作用域SingletonComponent篙议,然后,如果你是要單例的怠硼,就要加上@Singleton鬼贱,如果不是,就去掉即可香璃。如果你去看過官方的文檔这难,就會發(fā)現(xiàn),跟他的不同葡秒,因為我這個是最新的版本姻乓,官方上的版本還是比較老的,很多的方法和類都變了眯牧。最后使用的方法都是一樣的蹋岩,就不多說了。
...
@Inject
lateinit var singleData: SingleData
@Inject
lateinit var singleGlobalData: SingleGlobalData
override fun onCreate(savedInstanceState: Bundle?) {
CaesarHitLog.I("singleData地址:"+singleData)
CaesarHitLog.I("singleGlobalData地址:"+singleGlobalData)
}
ViewModel中調用
這個作用域是ViewModelScoped及ViewModelComponent学少,用起來也很簡單剪个。
@ViewModelScoped
class VMData @Inject constructor(){
init {
CaesarHitLog.I("VMData類的構造函數(shù)被調用了")
}
}
class VMGlobalData {
init {
CaesarHitLog.I("VMGlobalData類的構造函數(shù)被調用了")
}
}
@InstallIn(ViewModelComponent::class)
@Module
class MyModelModule {
@Provides
fun providerVM(): VMGlobalData {
return VMGlobalData()
}
}
然后在viewmodel中調用如下:
@HiltViewModel
class MyViewModel @Inject constructor() :ViewModel() {
@Inject
lateinit var vm: VMData
@Inject
lateinit var vmg: VMGlobalData
fun check(){
CaesarHitLog.I("VMData地址:"+vm)
CaesarHitLog.I("VMGlobalData:"+vmg)
}
}
這邊ViewModel要用@HiltViewModel標簽注釋,另外要注意旱易,在ViewModel的構造方法中要加一個@Inject標簽禁偎,不然會報錯腿堤。還有這邊ViewModel官方已經(jīng)給我們注入過了,所以我們可以用便捷的方式來獲取ViewModel如暖。這邊要注意笆檀,不能在Activity中用@Inject的方式來獲取,這種獲取的方式會將ViewModel變成一個普通的類盒至,會失去它的生命周期酗洒。
@AndroidEntryPoint
class MyViewModelActivity : AppCompatActivity() {
// @Inject
// lateinit var viewmodel: MyViewModel 錯誤的獲取方式
// val viewmodel: MyViewModel by lazy {
// ViewModelProvider(this).get(MyViewModel::class.java)
// }//這個是老的方式
val viewmodel by viewModels<MyViewModel>()//這個是注入獲取的方式
當然了,用便捷的方式獲取的話還需要另外2個依賴包
implementation "androidx.activity:activity-ktx:1.2.3"http://在activity中獲取
implementation 'androidx.fragment:fragment-ktx:1.3.4'//在fragment中獲取
Fragment作用域使用
@FragmentScoped
class FragmentData @Inject constructor(){
init {
CaesarHitLog.I("FragmentData類的構造函數(shù)被調用了")
}
}
class FragmentGlobalData {
init {
CaesarHitLog.I("FragmentGlobalData類的構造函數(shù)被調用了")
}
}
@InstallIn(FragmentComponent::class)
@Module
class MyFragmentModule {
@Provides
fun providerFrag(): FragmentGlobalData {
return FragmentGlobalData()
}
}
@AndroidEntryPoint
class BlankFragment : Fragment() {
@Inject
lateinit var fragData: FragmentData
@Inject
lateinit var fragData2: FragmentGlobalData
}
在fragment中也超級簡單枷遂。用FragmentScoped作用域和FragmentComponent樱衷,記得在碎片中,要加上@AndroidEntryPoint注釋酒唉。
Service作用域使用
@ServiceScoped
class ServiceData @Inject constructor(){
init {
CaesarHitLog.I("ServiceData類的構造函數(shù)被調用了")
}
}
class ServiceGlobalData {
init {
CaesarHitLog.I("ServiceGlobalData類的構造函數(shù)被調用了")
}
}
@InstallIn(ServiceComponent::class)
@Module
class MyServiceModule {
@Provides
fun providerSerFrag(): ServiceGlobalData {
return ServiceGlobalData()
}
}
@AndroidEntryPoint
class MyService:Service(){
@Inject
lateinit var data: ServiceData
@Inject
lateinit var data2: ServiceGlobalData
}
在Service中矩桂,別忘記加上@AndroidEntryPoint注釋
自定義View作用域
在自定義view中,有時我們也需要獲取對象使用痪伦。只要指定viewScoped作用域即可
@ViewScoped
class CusViewData @Inject constructor(){
init {
CaesarHitLog.I("CusViewData類的構造函數(shù)被調用了")
}
}
class CusViewGlobalData {
init {
CaesarHitLog.I("CusViewGlobalData類的構造函數(shù)被調用了")
}
}
@AndroidEntryPoint
class CusView :View {
@Inject
lateinit var data: CusViewData
@Inject
lateinit var data2: CusViewGlobalData
}
調用跟上面都是大同小異侄榴。
嵌套使用
我們平常使用的時候,不單單一個類會這么簡單网沾,他肯定構造函數(shù)中包含了其他的類癞蚕,那這種情況應該怎么使用,下面也給出2種注入方式
class MoreDate @Inject constructor(val simpleData: SimpleData,@ActivityContext val context: Context){
init {
CaesarHitLog.I("MoreDate類的構造函數(shù)被調用了,context是否為空:"+(context==null))
}
}
第一種就是自己寫的方法辉哥,MoreDate的構造函數(shù)中桦山,有2個參數(shù),一個是SimpleData醋旦,這個類我們在剛才已經(jīng)注入過了恒水,所以在這邊就可以直接使用,系統(tǒng)會自動為我們在構造函數(shù)中注入浑度。另外一個是Context上下文寇窑,其中Context我們用了@ActivityContext來修飾鸦概,Hilt已經(jīng)為我們注入提供了2種上下文箩张,另外一個是ApplicationContext,我們也能直接使用窗市。接下來我們來看看另外一個注入方法:
class MoreGlobalData(val singleData: SingleData, var context: Context){
init {
CaesarHitLog.I("MoreGlobalData類的構造函數(shù)被調用了,context是否為空:"+(context==null))
}
}
@Provides
fun providerMoreGlobal( singleData: SingleData,@ApplicationContext context: Context): MoreGlobalData {
return MoreGlobalData(singleData,context)
}
@AndroidEntryPoint
class MoreActivity : AppCompatActivity() {
@Inject
lateinit var fragData: MoreDate
@Inject
lateinit var fragData2: MoreGlobalData
}
在Provides中先慷,MoreGlobalData需要SingleData和Context對象,Hilt會自動為我們找對象咨察,我們不需要去主動賦值论熙,只需要在方法參數(shù)中有定義即可。
重名的用法
有時候我們要創(chuàng)建2個相同的對象摄狱,那我們應該如何去區(qū)分他們脓诡?
class UserNameGlobalBean constructor(val name: String) {
init {
CaesarHitLog.I("UserNameBean構造了")
}
}
@Named("name1")
@Provides
fun providerUserName1(): UserNameGlobalBean {
return UserNameGlobalBean("111")
}
@Named("name2")
@Provides
fun providerUserName2(): UserNameGlobalBean {
return UserNameGlobalBean("222")
}
@AndroidEntryPoint
class NamesActivity : AppCompatActivity() {
@Inject
@Named("name1")
lateinit var nameOne: UserNameGlobalBean
@Inject
@Named("name2")
lateinit var nameTwo: UserNameGlobalBean
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_names)
CaesarHitLog.I("名字1為:"+nameOne.name)
CaesarHitLog.I("名字2為:"+nameTwo.name)
}
}
通過加入@Named限定符无午,就可以指定我們需要對象,調用的時候祝谚,也指定一下即可宪迟。
接口未指明用法
這個跟上面的有點不太一樣,先看例子吧交惯。
interface ICallBack {
fun onData()
fun onDes()
}
class CallBackImpl @Inject constructor(@ActivityContext var context: Context):ICallBack{
override fun onData() {
Toast.makeText(context,"onData調用了",Toast.LENGTH_SHORT).show()
CaesarHitLog.I("數(shù)據(jù)1調用額")
}
override fun onDes() {
CaesarHitLog.I("數(shù)據(jù)2調用額")
}
}
我們有一個接口ICallBack次泽,然后有一個實現(xiàn)該接口的類CallBackImpl,這邊注意席爽,這個類的構造函數(shù)中要加上@Inject注釋意荤。然后接口類的注入也有點不同,因為是抽象的只锻,所以在綁定的時候玖像,類也要是抽象類。然后這邊不再是provider標簽了齐饮,要用Binds標簽御铃。
@InstallIn(ActivityComponent::class)
@Module
abstract class MyAbsModule {
@Binds
abstract fun provideCallback(callback: CallBackImpl):ICallBack
}
@AndroidEntryPoint
class InterActivity : AppCompatActivity() {
@Inject
lateinit var calBack: ICallBack
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_inter)
calBack.onData()
calBack.onDes()
}
}
在綁定的時候,我們的參數(shù)是一個實現(xiàn)類沈矿,返回是一個接口上真。所以最后,我們獲取調用的時候羹膳,可以用它的接口方法睡互,然后在實現(xiàn)類中,就會自動調用了對應的方法陵像。
其他非注入的類中獲取
Android應用有4大組件就珠,目前Hilt只支持了其中2個的依賴注入,那剩余2個的組件甚至于其他不直接支持注入的類中難道就不能使用了么醒颖,當然可以妻怎,接下來我這邊再舉一個在廣播中獲取注入的方式,注冊廣播和發(fā)送就簡略了泞歉,直接上核心代碼:
class MyBroadReceiver:BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if ("com.caesar.hit.normal.broad" == intent?.action){
val singletom = EntryPointAccessors.fromApplication<MySingleTom>(context!!)
CaesarHitLog.I("收到了和廣播:"+singletom.getSingleData())
}
}
@EntryPoint
@InstallIn(SingletonComponent::class)
interface MySingleTom {
fun getSingleData(): SingleData
}
}
這邊我們用EntryPoint限定符創(chuàng)建一個入口逼侦,在里面定義好你要獲取的那個類,當然了腰耙,那個類是已經(jīng)被注入好的榛丢。然后我們通過EntryPointAccessors這個類的方法,它有4種獲取的方法挺庞,對應4種作用域晰赞,分別為fromApplication,fromActivity,fromFragment掖鱼,fromView然走,獲取的類型就是下面定義的接口MySingleTom,然后通過里面的方法戏挡,就可以獲取到你想要的對象了丰刊。通過這種辦法,就可以在其他你想要的任何類中來獲取注入的對象了增拥。
至此啄巧,HIlt基本講完了它的基礎用法。不過我看Hilt每次更新的變動挺大的掌栅,目前應該還不是它的完全版本秩仆,不過Hilt相對于Dagger是大大簡化了難度,還是強烈推薦大家去使用猾封。
轉載請標明出處澄耍。