寫在前面
這裡是使用Kotlin構建MVVM應用程式—第四部分:依賴注入Dagger2
在前面的一系列文章中,我們瞭解了在MVVM架構中是如何提供和處理資料的。
//////model
val remote=Retrofit.Builder()
.baseUrl(Constants.HOST_API)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build().create(PaoService::class.java)
val local=AppDatabase.getInstance(applicationContext).paoDao()
val repo = PaoRepo(remote, local)
複製程式碼
為了得到給ViewModel層提供資料的倉庫repo,我們需要有remote
(由Retrofit提供來自伺服器的資料)和local
(由Room提供來自本地的資料)。
由於一個應用程式必定有多個不同的viewmodel
,所以就必須為其提供多個repo
,那就需要提供多個remote
和local
。而麻煩的便是提供remote
和local
的寫法都差不了多少,但你卻又不得不寫。
真正的開發者都不會想做沒有效率的事情。
因此,省時省力的依賴注入思想就得到了很多開發者的推崇,在android開發中,那當然就是Dagger2了。
什麼是dagger2?
A fast dependency injector for Android and Java.
一個適用於Android和Java的快速的依賴注入工具。
那什麼又是依賴注入呢?
我們可以先來看一個例子:我們在寫物件導向程式時,往往會用到組合,即在一個類中引用另一個類,從而可以呼叫引用的類的方法完成某些功能,就像下面這樣:
public class ClassA {
...
ClassB b;
...
public ClassA() {
b = new ClassB();
}
public void do() {
...
b.doSomething();
...
}
}
複製程式碼
這個時候就產生了依賴問題,ClassA依賴於ClassB,必須藉助ClassB的方法,才能完成一些功能。這樣看好像並沒有什麼問題,但是我們在ClassA的構造方法裡面直接建立了ClassB的例項,問題就出現在這,在ClassA裡直接建立ClassB例項,違背了單一職責原則,ClassB例項的建立不應由ClassA來完成;其次耦合度增加,擴充套件性差,如果我們想在例項化ClassB的時候傳入引數,那麼不得不改動ClassA的構造方法,不符合開閉原則。
注:
單一職責原則:一個類,只有一個引起它變化的原因。應該只有一個職責。每一個職責都是變化的一個軸線,如果一個類有一個以上的職責,這些職責就耦合在了一起。這會導致脆弱的設計。當一個職責發生變化時,可能會影響其它的職責。另外,多個職責耦合在一起,會影響複用性
開閉原則:一個軟體實體如類,模組和函式應該對擴充套件開放,對修改關閉。
因此我們需要一種注入方式,將依賴注入到宿主類(或者叫目標類)中,從而解決上面所述的問題。
引入Dagger2
// Add Dagger dependencies 當前版本2.16
apply plugin: `kotlin-kapt`
dependencies {
implementation `com.google.dagger:dagger:2.16`
kapt `com.google.dagger:dagger-compiler:2.16`
}
複製程式碼
注:
kapt 即 Kotlin Annotation Processing,就是服務於Kotlin的註解處理器。可在編譯時期獲取相關注解資料,然後動態生成.java原始檔(讓機器幫我們寫程式碼),通常是自動產生一些有規律性的重複程式碼,解決了手工編寫重複程式碼的問題,大大提升編碼效率。
Dagger2便藉助了註解處理器生成了許多必須的程式碼。經過編譯之後,這些程式碼可以在
***app/build/generated/source/kapt/..***目錄下找到
剛接觸Dagger2的人不明白為什麼使用了幾個註解之後就不需要再一直new Class了,只是覺得太酷了,Magic。
這樣便很容易遇到瓶頸,遇到需要稍加變通的情況,便會手足無措,只好放棄。
這也是許多開發者從入門到放棄的原因—知其然而不知其所以然。
Dagger2並沒有那麼神祕,在我們平常開發看不見的角落(build資料夾),它做了許多額外的工作。當你一不留神注意到那個角落的時候,就會恍然大悟。
如何依賴注入?
在具體使用Dagger2之前,我們先來思考一下如何將
//////model
val remote=Retrofit.Builder()
.baseUrl(Constants.HOST_API)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build().create(PaoService::class.java)
val local=AppDatabase.getInstance(applicationContext).paoDao()
val repo = PaoRepo(remote, local)
/////viewmodel
mViewModel=PaoViewModel(repo)
複製程式碼
這些東西提取出來然後進行統一注入?
假設這些依賴都存在於一個類中,我們把它記為Module:
//提供依賴
class Module constructor(val applicationContext:Context){
//////model
val remote=Retrofit.Builder()
.baseUrl(Constants.HOST_API)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build().create(PaoService::class.java)
val local=AppDatabase.getInstance(applicationContext).paoDao()
val repo = PaoRepo(remote, local)
/////viewmodel
val viewmodel=PaoViewModel(repo)
}
複製程式碼
現在我們需要把Provider
中的viewmodel
賦值給PaoActivity
中的mViewModel
這裡我們為Provider和Activity搭建一座橋樑Component
,並提供一個inject
方法用於注入這些依賴
//注入依賴
class Component constructor(val module:Module){
//注入
fun inject(activity:PaoActivity){
activity.mViewModel=module.viewmodel
}
}
複製程式碼
最後在View層只需要呼叫一下inject
方法便可以進行注入
class PaoActivity : RxAppCompatActivity() {
lateinit var mViewModel : PaoViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//依賴
val module=Module(applicationContext)
//注入
Component(module).inject(this)
}
}
複製程式碼
Dagger2便是應用的這一套邏輯,不過Dagger2在通過annotationProcessor
在編譯時期對註解進行了處理,自動生成了上面描述的程式碼邏輯。
應用Dagger2
首先我們需要了解Dagger2裡的幾個註解
-
@Inject
它用於標識哪些應該被注入,被標識的可以是
public
屬性或者constructor
建構函式 -
@Component
這裡用於標識依賴和待注入物件之間的橋樑
-
@Module
帶有此註解的類,用來提供依賴,裡面定義一些用@Provides註解的以provide開頭的方法,這些方法就是所提供的依賴,Dagger2會在該類中尋找例項化某個類所需要的依賴
Dagger2通過處理這幾個註解之後,便會自動生成我們需要的前文中的程式碼。
而開發者需要做的不過是根據實際的需要合理運用這幾種註解即可。
首先,我們新建我們需要的檔案
AppModule.kt:
@Module
class AppModule(val applicationContext: Context){
//提供 Retrofit 例項
@Provides @Singleton
fun provideRemoteClient(): Retrofit = Retrofit.Builder()
.baseUrl(Constants.HOST_API)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build()
//提供 PaoService 例項
@Provides @Singleton
fun providePaoService(client:Retrofit) =client.create(PaoService::class.java)
//提供 資料庫 例項
@Provides @Singleton
fun provideAppDataBase():AppDatabase = AppDatabase.getInstance(applicationContext)
//提供PaoDao 例項
@Provides @Singleton
fun providePaoDao(dataBase:AppDatabase)=dataBase.paoDao()
}
複製程式碼
AppComponent.kt:
@Singleton
@Component(modules = arrayOf(AppModule::class))
interface AppComponent{
fun inject(activity: PaoActivity)
}
複製程式碼
modules 表明了哪些依賴可以被提供。
我們我需要使用@Inject標識哪些需要被注入
PaoActivity.kt
class PaoActivity : RxAppCompatActivity() {
//標識mViewModel需要被注入
@Inject
lateinit var mViewModel : PaoViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//di
getComponent().inject(this)
}
fun getComponent()=DaggerAppComponent.builder()
.appModule(AppModule(applicationContext)).build()
}
複製程式碼
PaoViewModel.kt
//@Inject 可用於建構函式,表示建構函式中的引數是被自動注入的
class PaoViewModel @Inject constructor(private val repo: PaoRepo)class PaoViewModel @Inject constructor(private val repo: PaoRepo)
複製程式碼
PaoRepo.kt
//@Inject 可用於建構函式,表示建構函式中的引數是被自動注入的
class PaoRepo @Inject constructor(private val remote:PaoService, private val local :PaoDao)
複製程式碼
整個過程可以簡單看作是一個交易
Component就類似於商店一樣,AppModule是供應商,提供各種商品給商店,PaoActivity可以看作顧客
AppModule:我把貨都給你了
AppComponent:好嘞,收到
過了一會兒,PaoActivity來採購了
PaoActivity:我需要一個PaoViewModel,有賣的沒?
AppComponent:稍等 ,我幫您看看
PaoActivity:嗯哼
AppComponent:一個PaoViewModel需要一個PaoRepo,一個PaoRepo需要有PaoService和PaoDao。嗯,都有,可以成交
然後便愉快的完成了這單交易
編譯之後,通過處理註解會生成以下檔案
附上編譯生成的DaggerAppComponent.kt
檔案
public final class DaggerAppComponent implements AppComponent {
private Provider<Retrofit> provideRemoteClientProvider;
private Provider<PaoService> providePaoServiceProvider;
private Provider<AppDatabase> provideAppDataBaseProvider;
private Provider<PaoDao> providePaoDaoProvider;
private DaggerAppComponent(Builder builder) {
initialize(builder);
}
public static Builder builder() {
return new Builder();
}
private PaoRepo getPaoRepo() {
return new PaoRepo(providePaoServiceProvider.get(), providePaoDaoProvider.get());
}
private PaoViewModel getPaoViewModel() {
return new PaoViewModel(getPaoRepo());
}
@SuppressWarnings("unchecked")
private void initialize(final Builder builder) {
this.provideRemoteClientProvider =
DoubleCheck.provider(AppModule_ProvideRemoteClientFactory.create(builder.appModule));
this.providePaoServiceProvider =
DoubleCheck.provider(
AppModule_ProvidePaoServiceFactory.create(
builder.appModule, provideRemoteClientProvider));
this.provideAppDataBaseProvider =
DoubleCheck.provider(AppModule_ProvideAppDataBaseFactory.create(builder.appModule));
this.providePaoDaoProvider =
DoubleCheck.provider(
AppModule_ProvidePaoDaoFactory.create(builder.appModule, provideAppDataBaseProvider));
}
@Override
public void inject(PaoActivity activity) {
injectPaoActivity(activity);
}
private PaoActivity injectPaoActivity(PaoActivity instance) {
PaoActivity_MembersInjector.injectMViewModel(instance, getPaoViewModel());
return instance;
}
public static final class Builder {
private AppModule appModule;
private Builder() {}
public AppComponent build() {
if (appModule == null) {
throw new IllegalStateException(AppModule.class.getCanonicalName() + " must be set");
}
return new DaggerAppComponent(this);
}
public Builder appModule(AppModule appModule) {
this.appModule = Preconditions.checkNotNull(appModule);
return this;
}
}
}
複製程式碼
到此,本篇的文章就結束了。
本專案的github地址:github.com/ditclear/MV…
更多的例子可以檢視:github.com/ditclear/Pa…
寫在最後
其實Dagger2理解起來並不難,只要去看看生成的檔案,便很容易明白。但是很多android開發者都不喜歡問為什麼,更不喜歡探究為什麼,不看原始碼,只懂使用,導致在技術上止步不前,才有那麼多次的從入門到放棄。
多看書,多寫程式碼,多讀原始碼,路才能走寬。