使用Kotlin構建MVVM應用程式—第四部分:依賴注入Dagger2

ditclear發表於2019-02-26

使用Kotlin構建MVVM應用程式—第四部分:依賴注入Dagger2

寫在前面

這裡是使用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,那就需要提供多個remotelocal。而麻煩的便是提供remotelocal的寫法都差不了多少,但你卻又不得不寫。

真正的開發者都不會想做沒有效率的事情。

因此,省時省力的依賴注入思想就得到了很多開發者的推崇,在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裡的幾個註解

  1. @Inject

    它用於標識哪些應該被注入,被標識的可以是public屬性或者constructor建構函式

  2. @Component

    這裡用於標識依賴和待注入物件之間的橋樑

  3. @Module

    帶有此註解的類,用來提供依賴,裡面定義一些用@Provides註解的以provide開頭的方法,這些方法就是所提供的依賴,Dagger2會在該類中尋找例項化某個類所需要的依賴

Dagger2通過處理這幾個註解之後,便會自動生成我們需要的前文中的程式碼。

而開發者需要做的不過是根據實際的需要合理運用這幾種註解即可。

首先,我們新建我們需要的檔案

di

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。嗯,都有,可以成交

然後便愉快的完成了這單交易

編譯之後,通過處理註解會生成以下檔案

build

附上編譯生成的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開發者都不喜歡問為什麼,更不喜歡探究為什麼,不看原始碼,只懂使用,導致在技術上止步不前,才有那麼多次的從入門到放棄。

多看書,多寫程式碼,多讀原始碼,路才能走寬。

參考資料

相關文章