iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 20
0
Mobile Development

Android 十全大補系列 第 20

[Android 十全大補] Dagger

  • 分享至 

  • xImage
  •  

前二章我們談了架構分層以後,相信大家對一般程式開發所該有的結構都有了初步的了解,架構的分層是為了隔離依賴,讓程式碼的依賴關係提升變成模組間的關係,那我們該怎麼管裡或提供程式碼間的依賴呢?

Dependency Injection

在 SOLID Principle 這章我們談到 Dependency Inversion Principle 的時候有提到,高階物件不應該依賴於低階物件,二者最好都依賴於抽象,而抽象的實體由外部提供,這好處是當外面 dependency 變化的時候我們不用改既有程式碼,同一份程式碼也可以在多個情境下重用。
這種依賴的實體由外部提供的行為就叫做 Dependency Injection,有以下幾種 inject 的方式:

  1. Constructor injection:
    在物件建立的時候由外部直接提供,好處是單純不容易出錯,缺點是沒有彈性,而且 Constructor 可能越變越龐大。
  2. Setter injection:
    開一個 setXXX 的 function 讓外部呼叫直接設定 dependency,比起 Constructor 的方式較彈性,但可能要自己掌控呼叫順序的時間點。
  3. Interface injection:
    需要 dependency 的 class 必須實作統一的 interface,然後一樣由外部提供 dependency。

雖然很複雜但精神上都是一樣的,那就是 dependency 應該由外部注入,有了這個概念後我們就一起來看個例子吧:

interface Wood

interface Water

interface Ink

interface Paper

interface Printer

class MyWood : Wood

class MyWater : Water

class MyInk(val water: Water) : Ink

class MyPaper(val wood: Wood) : Paper

class MyPrinter(val paper: Paper, val ink: Ink) : Printer

Printer 是由 InkPaper 組成,而 InkWater 組成、PaperWood 組成,所以我們會有個地方需要產生所有 dependency 如下:

val printer = MyPrinter(MyInk(MyWater()), MyPaper(MyWood()))

非常的醜,如果分成很多行的話:

val water = MyWater()
val wood = MyWood()
val ink = MyInk(water)
val paper = MyPaper(wood)
val printer = MyPrinter(ink, paper)

還是非常的雜亂,有沒有辦法優雅的完成 dependency injection 呢?

Dagger is here to the rescue!

Dagger

由於由外部提供 dependency 這件事情非常繁瑣且較無生產力,dagger 透過 annotation processing 的方式幫我們在 compile 之後動態生成這些程式碼。我們要做的事情就是標註 annotation 跟簡單設定而已,而在 dagger 裡需要以下四大重要的 annotation:

  1. @Provide : 標注在 function 上,代表這個 function 會提供 dependency。
  2. @Module : 標注在 class 上,代表這個 class 包含著一到多個標註了 @Provide 的 function。
  3. @Inject : 這其實是 Java 的物件,代表物件會被外部設定。
  4. @Component : @Module 跟需要 dependency 的物件中間的橋樑。

如果你沒看過 annotation processing 的介紹可以參考:
https://ithelp.ithome.com.tw/articles/10222408

起手式一樣是 dependency 的宣告:

dependencies {
  implementation 'com.google.dagger:dagger:2.21'
  kapt 'com.google.dagger:dagger-compiler:2.21'
}

建立一份 PrinterModule 的程式碼來提供 Printer 相關的 dependency 如下:

@Module
class PrinterModule {
    @Provides
    fun providePrinter(paper: Paper, ink: Ink): Printer {
        return MyPrinter(paper, ink)
    }

    @Provides
    fun providePaper(wood: Wood): Paper {
        return MyPaper(wood)
    }

    @Provides
    fun provideWood(): Wood {
        return MyWood()
    }

    @Provides
    fun provideInk(water: Water): Ink {
        return MyInk(water)
    }

    @Provides
    fun provideWater(): Water {
        return MyWater()
    }
}

dagger 可以可以透過參數的方式幫我們完成遞迴的取得 dependency,比如說 providePrinter 裡需要 PaperInk,我們可以把它定義成參數假設他會由外部傳進來:

@Provides
fun providePrinter(paper: Paper, ink: Ink): Printer {
    return MyPrinter(paper, ink)
}

然後 dagger 會自動去找回傳型別相同的 providePaperprovideInk 提供給 providePrinter 當 function 的參數。

@Provides
fun providePaper(wood: Wood): Paper {
    return MyPaper(wood)
}

@Provides
fun provideInk(water: Water): Ink {
    return MyInk(water)
}

值得注意的是 function 名字並不重要,重點在於回傳的型別,而 WoodWater 如果有需要的話,也可以一層一層遞迴地去尋找正確的 dependency,如果尋找的過程有一個地方失敗的話,會導致 compile fail ,所以我們的使用者不會遇到問題,這也是 annotation processing 的一個蠻好的優點。

另一個值得一提的是我們在這個檔案裡的 function 的參數與回傳都只有指定 interface 型別,將來我們要替換的時候就會非常方便。

第二步驟就是要在我們需要 dependency 的物件上,標上 @Inject 讓 dagger 知道我們需要 inject 的物件位置與名稱是什麼,假設我們需要在 MainActivity 使用 Printer 的話,範例如下:

class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var printer: Printer
}

有了整個 object graph 之後,第三步就是要靠 @Component 來串接 module 跟需要 dependency 的物件,我們建立 PrinterComponent 如下:

@Component(modules = [PrinterModule::class])
interface PrinterComponent {

    fun inject(mainActivity: MainActivity)
}

PrinterComponent 透過 modules 來指定連結的 module class,而 inject function 代表著 dependency 的需求者可以透過呼叫這個 function 讓 dagger 提供 dependency。

特別注意 PrinterComponent 是個 interface,compile 之後 dagger 會幫我們建立真正的 DaggerPrinterComponent 類別。
接下來回到 MainActivity 把 component 的實體建立起來如下:

class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var printer: Printer

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        DaggerPrinterComponent.create().inject(this)
    }
}

呼叫了 inject 之後,printer 就會自動被設值了,而且 MainActivity 並不知道 Printer 的具體 class 在哪,所有依賴都建構在 interface 之下,這樣的程式是不是乾淨許多呢?

當我們哪天需要換 dependency 的時候,比如 MyPrinter 要換成 EpsonPrinter 或是 MyPaper 要換成 DoubleAPaper,都只要在 PrinterModule 改建構實體的程式碼即可,整個 graph 都會自動更新,所有的程式還是會依賴同樣的 interface 來運作,維護起來是不是也很方便呢?

@Provides
fun providePrinter(paper: Paper, ink: Ink): Printer {
    // return MyPrinter(paper, ink)
    return EpsonPrinter(paper, ink)
}

以上就是今天的全部內容了,
如果有興趣的話可以接著看 Dagger as a Pro
希望對大家都有幫助!

Android 十全大補已經正式出書上架囉!
有興趣的讀者歡迎參考:
https://www.tenlong.com.tw/products/9789864345786


上一篇
[Android 十全大補] Clean Architecture
下一篇
[Android 十全大補] Dagger as a Pro
系列文
Android 十全大補30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言