พื้นฐาน Dagger2-101 - DI Concept & Scope

Nutron
3 min readSep 18, 2020

บทความนี้เป็นอีกหนึ่งบทความที่ตั้งใจจะเขียนมานานแต่ไม่ได้เริ่มสักที จนกระทั่งเพิ่งมีเวลามานั่งเขียนและเรียบเรียงเพราะรู้สึกว่ามันเป็นสิ่งที่ Dev ทุกคนควรจะรู้ เนื่องจากประสบการณ์การทำงาน (ทั้งในประเทศและต่างประเทศรวมถึงการคุยกับ Dev หลายๆคน) Dagger2 ซึ่งต่อไปจะเรียกว่า Dagger นั้นแทบจะเป็น Library พื้นฐานที่นิยมใช้ในการทำ Dependency Injection ซึ่ง Developer อย่างเราๆควรจะรู้ไว้ แต่เนื่องจาก Dagger มันค่อนข้างซับซ้อนและเข้าใจยาก อีกทั้ง Document ของ Dagger เองอ่านแล้วก็ยังมึนๆงงๆ ด้วยเหตุนี้จึงอยากเขียนพื้นฐานการใช้ Dagger ขึ้นมา โดยพยายามจะสรุปเนื้อหาที่สำคัญๆ และการใช้งาน Dagger หวังว่ามันจะเป็นประโยชน์กับ Dev ทุกคนที่เข้ามาอ่านหรือคนที่กำลังคิดจะใช้ Dagger โดยเนื้อหาจะแบ่งออกเป็นหัวข้อดังนี้

  1. Dependency Injection Concept & Scope
  2. Component & Component Builder & @BindInstance
  3. Component dependencies & Qualified types
  4. Subcomponent & SubComponent Builder
  5. Dagger Android Injection

Dependency Injection Concept

Dependency Injection เป็นเทคนิคหนึ่งที่ช่วยลดการผูกมัดกันของโค้ด โดยแทนที่ class A จะสร้าง Object B ขึ้นมาใช้งานภายใน class เอง ก็เปลี่ยนมาเป็นการส่ง ObjectB ผ่านเข้ามาทาง constructor หรือ method parameter แทน ซึ่งจะเห็นได้ว่า A ไม่จำเป็นต้องรู้ว่า B ถูกสร้างขึ้นมาอย่างไร รู้เพียงว่าจะใช้ B อย่างไรก็พอ ซึ่งประโยชน์ของการทำ DI ที่เห็นได้ชัดคือ มันช่วยให้เราทำ Unit Test ได้ง่ายขึ้นและลดการผูกมัดกันระหว่าง Classes

Dagger คือหนึ่งใน Library ที่เข้ามาช่วยในการทำ Dependency Injection ซึ่งหากใครที่เคยใช้ ButterKnife มาก่อนมันก็จะคล้ายๆกัน โดย Dagger จะทำหน้าที่สร้างและ reference ถึง objects/fields ที่เราต้องการ เพียงแค่เราใส่ @Inject ไปที่ objects/fields นั้นๆ แล้ว เรียกฟังก์ชั่น Inject() เราก็จะสามารถอ้างถึงและใช้งาน objects/fields เหล่านั้นใน Activty หรือ Fragment ของเราได้

แต่ก่อนที่เราจะสามารถเรียกใช้งานแบบด้านบนได้ เราต้องเตรียมอะไรบางอย่างเพื่อให้ Dagger รู้ว่า เมื่อมีการ Inject objects/fields นี้ มันต้องไปหาจากที่ไหน โดย Dagger จะสร้าง Dependency graph ขึ้นมาจากที่เราบอก Dagger ว่า การจะได้มาซึ่ง objects/fields เหล่านั้นต้องทำอย่างไร ต้องสร้างตัวไหนก่อน ตัวไหนหลัง และแต่ละตัวมีความสัมพันธ์กันอย่างไร รูปด้านล่างแสดงถึงตัวอย่างของ Dependency graph อย่างง่าย

Scope

ก่อนที่เราจะไปพูดถึงการใช้งาน Dagger อยากจะขอพูดถึง Scope ก่อน เพราะเจ้า Scope นี้จะถูกพูดถึงในหัวข้อถัดๆไป

“Scope” หรือ “ขอบเขต” เป็นคอนเซปต์ที่ที่มีอยู่ใน Dagger2 มันคือวิธีการที่เราระบุ lifecycle ให้กับ objects/fields เหล่านั้นว่าจะยังคงอยู่ได้ตราบเท่าที่ Scope นั้นยังอยู่ ตัวอย่างเช่น หากเราระบุให้ objects/fields เหล่านั้นให้อยู่ในระดับ Application Scope มันก็จะอยู่ไปตลอดจนกว่า Application ของเราจะตาย หรือบาง objects/fields ที่เราอยากให้มันอยู่แค่ในระดับ Activity lifecycle หรือ Fragment lifecycle เราก็สามารถระบุ Scope ให้มันอยู่ได้แค่จนกว่า Activty หรือ Fragment นั้นจะตายเช่นกัน

นอกจากนี้การใช้ Scope ยังเป็นการบอก Dagger ว่าให้เก็บรักษา objects/fields ตัวนั้นเอาไว้ตราบเท่าที่ Scope นี้ยังอยู่ (local Singleton) นั่นหมายความว่าหากมีการอ้างถึง objects/fields นี้ใน scope เดียวกัน เราจะได้ objects/fields ตัวเดิมเสมอ อันนี้สำคัญนะ! เพราะมันจะมีประโยชน์มากหากเราต้องการที่จะแชร์ state ของ objects/fields นั้นไปยังตัวอื่นๆ

จากภาพด้านบนเราจะเห็นว่า Context, NetworkUtils และ Repository ออปเจ็คเหล่านี้จะคงอยู่ตลอดจนกว่า Application จะตายหรืออาจพูดได้ว่ามันอยู่ในระดับ Application Scope นอกจากนี้จะสังเกตเห็นว่ามีการแชร์ Interactor ระหว่างสอง Activties/Views เพื่อจุดประสงค์บางอย่าง เช่น ต้องการเก็บ State หรือค่าบางค่าไว้ เพื่อใช้ในอีก Activties/Views หนึ่ง ซึ่งหาก Interactor นั้นถูกสร้างขึ้นมาใหม่โปรแกรมก็อาจทำงานผิดพลาดได้ เนื่องจาก State หรือค่าที่ต้องการจะหายไปด้วย ดังนั้น การใช้ Scope จะช่วยให้ Dagger ยังคงเก็บรักษา Interactor ไว้จนกว่า Scope นั้นจะถูกทำลายหรือตายไป

ใน Dagger2 มี Scope พื้นฐานอย่าง @Singleton ให้เราใช้ โดยเจ้า @Singleton นี้ จะเป็นตัวบอก Dagger ว่า objects/fields จะถูกสร้างแค่ครั้งเดียวใน Application นั้น และจะคงอยู่ตลอดจนกว่า Application จะตาย (Global singleton) ให้ลองนึกจินตนาการภาพตามว่า เรามี Application Scope ซึ่งการที่เราใส่ @Singleton จะทำให้ objects/fields ถูกสร้างแค่ครั้งเดียวใน Application lifecycle โดยทุกครั้งที่มีการอ้างถึง object ตัวนี้ใน Application เราจะได้ object ตัวเดิมเสมอ

Note: จากที่บอกว่า @Singleton คือ scope ที่จะคงอยู่ตลอดตราบเท่าที่ Application ยังอยู่ จริงๆแล้วมันไม่ได้จริงซะทีเดียว เนื่องจาก @Singleton เป็นแค่ตัวบอก Dagger ว่า objects/fields จะถูกสร้างแค่ครั้งเดียวภายใต้ขอบเขตของ Component นั้นๆ และจะคงเก็บรักษา objects/fields นั้นไว้ จนกว่า Component นั้นจะถูกทำลาย ซึ่งโดยปกติแล้ว Component จะมาคู่กับ Scope ดังนั้นเมื่อ Component ถูกทำลาย Scope ก็จะหายไปด้วย

เรามักใช้ @Singleton กับ Application Component (Application Scope) เพื่อที่จะให้ objects/fields เหล่านั้นเป็น Global singleton ส่วน Component หรือ Scope ที่ถัดลงมาเรามักจะใช้ Custom Scope ที่เราสร้างขึ้นมาเป็นตัวกำหนดขอบเขตของ objects/fields แทน

***ถึงจุดนี้หลายคนคงสงสัยว่า Component คืออะไร เดี๋ยวจะมาอธิบายให้ในบทความถัดไป*** 😎

@Module
class DataModule {

@Provides
@Singleton
fun providePreferences(): ValuePreference = ValuePreference()

}

จากตัวอย่างด้านบน ยังไม่ต้องสนใจอย่างอื่น สนใจแค่ @Singleton ก็พอ ซึ่งโค้ดส่วนนี้จะเป็นการบอก Dagger ว่า ValuePreference จะถูกสร้างแค่ครั้งเดียว ดังนั้นหากมีการอ้างถึง ValuePreference ใน objects/fields หรือ Activity/Fragment ภายใต้ Scope เดียวกัน มันจะได้ ValuePreference ตัวเดิมเสมอ

Custom Scope

อย่างที่ได้กล่าวไปตอนต้น จริงๆแล้ว @Singleton ก็เป็นแค่ Scope หนึ่งที่ Dagger เตรียมไว้ให้เราดังโค้ดด้านล่าง

@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Singleton {
}

ซึ่งเราเองก็สามารถสร้าง Scope ขึ้นมาใช้ตามที่เราเห็นสมควรได้ ตัวอย่างด้านล่างคือวิธีการสร้าง Scope ที่มีชื่อว่า ViewScope

//Kotlin
@Scope
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
annotation class ViewScope
//Java
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewScope {
}

เราควรสร้าง Custom Scope เมื่อไหร่?

หากเราอนุมานว่า @Singleton คือการทำ Global Singleton แล้ว (ซึ่งจริงๆแล้วอาจจะใช่หรือไม่ใช่ก็ได้ขึ้นอยู่กับการใช้งาน) การสร้าง Custom Scope ก็เหมือนกับการทำ local Singleton ให้กับ objects/fields ใน Component นั้นๆ ซึ่งทุกครั้งที่มีการอ้างถึง objects/fields นั้นใน component เดียวกัน เราจะได้ objects/fields ตัวเดิมเสมอ ในทางกลับกัน หากเราไม่ได้กำหนด Scope ให้กับ objects/fields มันก็จะถูกสร้างใหม่ขึ้นมาเสมอเช่นกัน นอกจากนี้ประโยชน์ของการกำหนด Scope จะช่วยให้เราเห็นภาพ lifecycle ของ component หรือ objects/fields ได้ชัดเจนยื่งขึ้น โค้ดด้านล่างคือตัวอย่างการนำ Custom Scope ไปใช้

@Module
class ViewModelModule {
@Provides
@ViewScope // custom scope here
fun provideViewModel(): RandomViewModel = RandomViewModelImpl()
}

Conclusion

สาระสำคัญของบทความนี้คืออยากให้รู้ว่า DI คืออะไร ประโยชน์ของการทำ DI คืออะไร อีกประเด็นที่สำคัญคือเรื่องของ Scope ว่ามันคืออะไร มีประโยชน์อย่างไร เราควรใช้มันเมื่อไหร่และอย่างไร โดยเฉพาะอย่างยิ่งเรื่องของ Global Singleton และ Local Singleton

และเพื่อไม่ให้บทความนี้ยาวเกินไป คงขอตัดจบแค่เพียงเท่านี้ก่อน ในบทความหน้า เราจะมาพูดถึงการใช้งาน Dagger2 รวมถึงวิธีการสร้าง Component ต่างๆเพื่อให้ Dagger2 สามารถจัดเตรียม dependencies ให้กับเราได้

สุดท้าย ถ้าเห็นว่า Blog นี้มีประโยชน์ ช่วยสนับสนุนด้วยการฝากกด ❤️ กด Share ให้กันด้วยนะจ๊ะ จะได้มีกำลังใจในการเขียนบทความต่อๆไป ขอบคุณครับ

--

--