พื้นฐาน Dagger2 - 104 - Subcomponent & SubComponent Builder

Nutron
4 min readSep 28, 2020

บทความที่แล้วเราพูดถึง Component Dependencies รวมถึงการใช้ @Component.Builder และ @Named กันไปแล้ว ในบนความนี้เราจะมาลองใช้อีกหนึ่งวิธีที่ช่วยจัดการกับการแชร์ Object Dependencies ระหว่าง Components โดยบทความนี้เป็นส่วนหนึ่งของชุดบทความที่มีเนื้อหาแบ่งออกเป็นหัวข้อดังนี้

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

Subcomponent

Subcomponent คืออีกรูปแบบหนึ่งของการแชร์ Object Dependencies ระหว่าง Components เช่นเดียวเดียวกับ Component with Dependencies ทั้ง Subcomponent และ Component with Dependencies เองมี Lifecycle เป็นของตัวเอง นั่นคือ หากไม่มี Component หรือ Object ไหนอ้างถึง (reference) มันก็จะตายไปทำให้ Object Dependencies ทีมันถืออยู่ก็หายไปด้วย เราสามารถใช้ Scope กับ SubComponent เพื่อช่วยในการทำ local Singleton ได้เช่นกัน การใช้ Subcomponent มีข้อแตกต่างจากการใช้ Component with Dependencies ดังนี้

  • เราไม่จำเป็นต้องประกาศ Object dependencies ที่จะถูกใช้ใน Subcomponent ใน Parent Component เหมือนกับ Component Dependencies แต่ Parent component ต้องประกาศว่ามี Subcomponent อะไรบ้าง
  • Subcomponent สามารถเข้าถึงได้ทุก Object dependencies ใน Parent Component ซึ่งรวมถึง Parent ของ Parent Component ด้วย
  • Subcomponent มี Parent Component ได้แค่ตัวเดียว

จาก Diagram ด้านบนจะเห็นว่า ViewModel ที่อยู่ใน ViewComponent สามารถอ้างถึง NetworkUtils ที่อยู่ใน AppComponent ได้ โดยที่ ViewComponent เป็น Subcomponent ของ DomainComponent

จาก Diagram ด้านบน เราสามารถสร้าง Component ออกมาได้ดังนี้

AppComponent

AppModule.kt
DataModule.kt
NetworkModule.kt
AppComponent.kt

จากโค้ดด้านบนเราประกาศฟังก์ชั่นที่จะคืนค่ากลับไปเป็น DomainComponent ไว้ใน Interface ของ AppComponent เพื่อบอกให้ Dagger รู้ว่า DomainComponent จะเป็น SubComponent ของ AppComponent ส่วนสำคัญที่อยากให้สังเกตคือ Return Type จะต้องเป็น Component และ Parameter ที่รับเป็น Module ของ SubComponent นั้น

DomainComponent

DomainModule.kt
DomainComponent.kt

จากโค้ดด้านบน เราประกาศให้ DomainComponent เป็น Subcomponent โดยใช้คำสั่ง @Subcomponent จะสังเกตเห็นว่าเราไม่ต้องระบุ Parent Component เหมือนการทำ Component with Dependencies จากนั้นเราประกาศฟังก์ชั่นที่จะคืนค่ากลับไปเป็น ViewComponent ไว้ใน Interface ของ DomainComponent เพื่อบอกให้ Dagger รู้ว่า ViewComponent จะเป็น Subcomponent ของ DomainComponent

ViewComponent

ViewModelModule.kt
ViewComponent.kt

จากโค้ดด้านบน เราประกาศให้ ViewComponent เป็น Subcomponent ด้วย @Subcomponent และใน ViewModelModule มีการอ้างถึง NetworlUtils ซึ่งเป็น Object Dependencies ของ AppComponent ทั้งๆที่ ViewComponent ไม่ได้เป็น Subcomponent โดยตรงของ AppComponent แต่เป็น Subcomponent ของ DomainComponent

เมื่อเราเตรียม Component ทั้งหมดแล้ว ก็ได้เวลาเรียกใช้งาน ซึ่งการเรียกใช้งานเป็นไปตามโค้ดด้านล่างดังนี้

จากโค้ดด้านบน ViewComponent ถูกสร้างโดย DomainComponent และ DomainComponent ถูกสร้างโดย AppComponent โดยเรียกฟังก์ชั่นที่ประกาศไว้ในแต่ละ Interface ของ Component

Note(1): ตัวอย่างด้านบนเป็นเพียงการสาธิตการเรียกใช้งาน Subcomponent ซึ่งจริงๆแล้ว instance ของ AppComponent ไม่ควรถูกสร้างทุกครั้งที่เรียกใช้งาน แต่ควรถูกสร้างแค่ครั้งเดียวใน Application เพื่อให้มันคงอยู่ตลอดในระดับ Application Scope (หรือ Global Scope)

Note(2): ทุกครั้งที่เรียก addDomainComponent() หรือ addViewComponent() นั้น คือการสร้าง instance ของ Component เหล่านั้นขึ้นมาใหม่ ดังนั้นควรระมัดระวังในการใช้

Note(3): อย่างที่กล่าวไว้ตอนต้น ทั้ง Component with Dependencies และ Subcomponent มี Lifecycle เป็นของตัวเอง ดังนั้นหากเราไม่ต้องการใช้มันแล้วหรือ ต้องการทำลายมันเพื่อจัดการกับ memmory เราสามารถทำได้ด้วยการกำหนดค่า null ให้กับ component เหล่านั้น ตัวอย่างเช่น viewComponent = null

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

@Subcomponent.Builder

บทความที่แล้วเราได้เรียนรู้การใช้งาน @Component.Builder คู่กับ Component กันไปแล้ว แน่นอนว่า Subcomponent เองก็มี @Subcomponent.Builder ให้ใช้เหมือนกัน แล้วการใช้ Subcomponent Builder มันดีอย่างไร?

จากตัวอย่างการสร้าง Subcomponent ด้านบนจะเห็นว่า Parent Component ต้องประกาศ Subcomponent ทุกตัวใน Interface ทำให้ Parent Component ผูกมัด (coupled) กับ Subcomponent โดยตรง การใช้ Subcomponent ฺBuilder จึงเป็นวิธีที่มาช่วยลดการผูกมัด (de-coupled) ระหว่าง Parent Component และ Subcomponent โดยไม่จำเป็นต้องประกาศ Subcomponents ใน Parent Component อีกต่อไป

ทีนี้เรามาลองเปลี่ยนตัวอย่างด้านบนให้มาใช้ @Subcomponent.Builder กัน เช่นเดียวกับ @Component.Builder ตัว Builder ต้องมีฟังก์ชันที่คืนค่าเป็น Component นั้นเสมอ ซึ่งในที่นี่คือ build() ดังนั้นเพื่อความสะดวก เราจะสร้าง Base Interface ที่จัดเตรียมฟังก์ชั่น build() ไว้ให้แล้วดังนี้

interface SubComponentBuilder<out V> {
fun build(): V
}

ต่อไปเราจะแก้ไข DomainComponent ให้มาใช้ @Subcomponent.Builder ดังนี้

จากโค้ด เราสร้าง Interface ที่ชื่อว่า Builder โดยมี @Subcomponent.Builder เป็นตัวกำหนด ซึ่ง Builder จะ implement SubComponentBuilder ที่เป็น base interface ที่เราสร้างขึ้น

ถัดไปเราจะสร้าง Binder ขึ้นมา ซึ่งเจ้า Binder นี้จะถูกกำหนดให้กับ Parent Component เพื่อใช้ในการสร้าง Dependencies graph ในฝั่งของ Parent Component โดยหน้าตาของ Binder จะเป็นดังนี้

จากโค้ดด้านบนใน บรรทัดที่ 9–12 เป็นการสร้าง Annotation ใหม่ขึ้นมา โดยให้ชื่อว่า SubConponentKey โดยจะรับ parameter เป็น Class Type ซึ่งใช้ในการ Map ระหว่างชื่อ Builder กับ Builder Object

ถัดมาเราจะสร้างคลาส ApplicationBinderModule ซึ่งเป็น abstract class โดยมี @Module ที่รับ parameter เป็น subcomponents กำกับอยู่ ซึ่งในที่นี้เนื่องมีแค่ DomainComponent ที่เราต้องการให้เป็น SubComponent ของ AppComponent ดังนั้นเราจึงใส่ DomainComponent ให้กับ subcomponents ใน @Module

สุดท้ายเราใช้คำสั่ง @Binds ในบรรทัดที่ 17 เพื่อให้ Parent Component ซึ่งในที่นี้คือ AppComponent รู้จักกับ Builder ของ SubComponent ที่เราจะผูกด้วย จากนั้นเราบอกให้ ใช้ @IntoMap และ @SubComponentKey เพื่อระบุให้ Dagger สร้าง Map ระหว่างชื่อ Builder class และ Object ของ Builder โดยในที่นี่เราจะระบุเป็น DomainComponent.Builder ซึ่งหากเรามีมากกว่าหนึ่ง SubComponent ก็ทำแบบเดียวกัน

หลังจากที่เราสร้าง Binder เสร็จแล้ว ขั้นตอนต่อไปคือการกำหนด Binder ให้กับ AppComponent โดยจะได้หน้าตาประมาณนี้

จากโค้ดด้านบน เราจะเพิ่ม ApplicationBinderModule ใน modules ของ @Component ที่บรรทัดที่ 2 จากนั้นเราจะสร้างฟังก์ชันที่คืนค่าเป็น Map ที่ภายใน Map จะเก็บ Object ของทุก Builder ของ Subcomponent ที่ประกาศใน Binder ไว้ และจุดสำคัญที่อยากให้สังเกตคือ เราไม่ต้องประกาศ Subcomponent ไว้ภายใน Interface ของ Parent Component อีกแล้ว

ขั้นตอนสุดท้ายคือการเรียกใช้ Subcomponent เพื่อ Inject Dependencies ก็จะได้หน้าตาประมาณนี้

ในที่นี้เราทำ DomainComponent ให้ดูเป็นตัวอย่าง โดย ViewComponent ก็จะทำแบบเดียวกัน แค่สร้าง Binder และระบุให้กับ DomainComponent ที่เป็น Parent Component ของ ViewComponent

Conclusion

บทความนี้ เราได้เรียนรู้การทำ Dependencies ระหว่าง Component ด้วยการใช้ Subcomponent รวมถึงเรียนรู้การใช้ SubComponent Builder เพื่อช่วยในการสร้าง Subcomponent และทำให้ Component และ Subcomponent ไม่ผูกมัดกันมากเกินไป

ส่วนสำคัญที่อยากจะเน้นย้ำของการใช้ subcomponent คือ

  • เราไม่จำเป็นต้องประกาศ Object dependencies ที่จะถูกใช้ใน Subcomponent ใน Parent Component เหมือนกับ Component Dependencies แต่ Parent component ต้องประกาศว่ามี Subcomponent อะไรบ้าง
  • Subcomponent สามารถเข้าถึงได้ทุก Object dependencies ใน Parent Component ซึ่งรวมถึง Parent ของ Parent Component ด้วย
  • Subcomponent มี Parent Component ได้แค่ตัวเดียว

แต่อย่างไรก็ตาม สำหรับ Application ใหญ่ๆ ขอแนะนำให้ใช้ Component with Dependencies มากกว่าการใช้ SubComponent เพราะเบื้องหลังแล้ว Dagger อาจสร้างโค้ดที่ไม่จำเป็นต่อเรา ดังจะเห็นได้จากตัวอย่างที่ Subcomponent สามารถเข้าถึงได้ทุก Object dependencies ใน Parent Component ซึ่งรวมถึง Parent ของ Parent Component ด้วย ซึ่ง Object dependencies บางตัวเราไม่ได้ต้องการใช้ใน SubComponent การใช้ SubComponent จึงเป็นการหว่านแห และอาจทำให้เกิด Build time ที่เพิ่มขึ้นโดยไม่จำเป็น

ในบทความหน้าเราจะมาพูดถึงวิธีการพิเศษที่ Dagger สร้างขึ้นมา เพื่อรองรับการใช้งาน Dagger ในการพัฒนา Application บน Android โดยเฉพาะ เราจะมาดูกันว่ามันมันจะช่วยให้นักพัฒนา พัฒนา Application ได้ง่ายขึ้นมากน้อยเพียงใด คอยติดตามกันนะครับ

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

--

--