พื้นฐาน Dagger2 - 102 - Component & Component Builder & @BindInstance

Nutron
5 min readSep 21, 2020

ในบนความที่แล้ว เราได้เรียกรู้หลักการของ Dependency Injection รวมถึงได้เข้าไปสัมผัสโลกของ Dagger2 (ต่อไปนี้จะเรียกว่า Dagger) บางส่วนในเรื่องของ Scope กันมาบ้างแล้ว โดยเฉพาะเรื่องของ Global Singelton และ Local Singleton ซึ่งหากใครยังไม่เข้าใจให้กลับไปอ่านก่อนนะจ๊ะ หากเห็นว่ามีประโยชน์ ฝากกด ❤️ กด Share ให้ด้วยนะจ๊ะ จะได้มีกำลังใจในการเขียนบทความต่อๆไป 😄

ในบทความนี้ เราจะมารู้จักการใช้งาน Dagger ซึ่งเราจะพูดถึงการสร้าง Component การใช้ Component Builder รวมถึง annotation พิเศษอย่าง @BindInstanceโดยบทความนี้เป็นส่วนหนึ่งของชุดบทความที่มีเนื้อหาแบ่งออกเป็นหัวข้อดังนี้

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

Component & Module คืออะไร?

ก่อนจะไปเริ่มใช้งาน Dagger เรามารู้จักองค์ประกอบที่สำคัญของ Dagger กันก่อน ในการใช้งาน Dagger จะมีสององค์ประกอบที่สำคัญคือ Component และ Module โดย Component คือตัวกลางหรือสะพานที่เชื่อมระหว่าง Module ที่เป็นตัวที่ Provide Dependencies กับ object หรือ Class ที่เราจะ Inject Dependencies เข้าไป โดยหนึ่ง component สามารถมีได้หลายๆ Module ซึ่งเราอาจจะมองว่า component เปรียบเสมือนกุ๊กที่คอยปรุงอาหารพร้อมเสิร์ฟ โดยมี Mudule ทำหน้าที่เป็นตำราอาหาร

หลักการของ Component คือการ encapsulated ขั้นตอนในการสร้าง dependencies โดยที่ Caller หรือ Class สามารถ reference ถึง Object Dependencies เหล่านั้นได้ผ่านทาง Component โดยที่ไม่จำเป็นต้องรู้ว่า Object Dependencies เหล่านั้นถูกสร้างขึ้นมาอย่างไร โดย Component จะสร้าง Dependencies Graph ขึ้นมา เพื่อที่มันจะได้รู้ว่ามันต้องใช้ Dependencies ตัวไหนบ้างเพื่อสร้าง Object Dependencies ที่ Caller ต้องการ

จากรูปด้านบนเป็นรูปที่เราเคยเห็นมาแล้วในบทความที่แล้ว แต่ตอนนี้เราจะเห็นว่าแต่ละ layer ถูกแยกออกด้วย component โดยแต่ละ Component ก็จะรับผิดชอบ Dependecies ที่แตกต่างกันออกไป

Create a Component

ในการใช้งาน Dagger จะมีอยู่ 3 รูปแบบหลักๆ คือ Dependency Component, SubComponent และ Android Injection Component แต่ก่อนที่เราจะไปถึงรูปแบบเหล่านั้น เราจะมาเรียนรู้วิธีสร้าง Component แบบง่ายๆ กันก่อน

แน่นอนว่าอย่างแรกที่เราต้องทำคือการ Set up project ของเราให้สามารถใช้งาน Dagger2 ได้เสียก่อน โดยให้เราใส่ code ด้านล่างลงให้ build.gradle ไฟล์ของเรา

...
apply plugin: 'kotlin-kapt'
...
dependencies {
// Dagger
implementation 'com.google.dagger:dagger:2.x'
kapt 'com.google.dagger:dagger-compiler:2.x'
// Using classes in dagger.android you'll also want to include:
implementation 'com.google.dagger:dagger-android:2.x'
// support libraries
implementation 'com.google.dagger:dagger-android-support:2.x'
kapt 'com.google.dagger:dagger-android-processor:2.x'
}

ทีนี้เรามาลองสร้าง Compoment แบบง่ายกันดู โดยเราจะสร้าง AppCompoment เป็น Component ที่เราต้องการให้อยู่ในระดับ Application Scope ซึ่ง Component นี้จะประกอบไปด้วย Application context และ SharePreference โดยในขั้นแรกให้เราสร้างไฟล์ Module ขึ้นมาก่อนตามนี้

จาก Module ข้างต้นจะเห็นว่าเราใช้ @Module (บรรทัดที่ 1) เป็นตัวกำหนดว่า class นี้จะทำหน้าที่เป็น Module โดยเจ้า AppModule นี้จะรับ Application เข้ามาผ่านทาง Constructor จากนั้นสร้างฟังก์ชั่นที่ชื่อว่าprovideAppContext() ซึ่งคืนค่ากลับไป เป็น Context และใช้ @Provides เพื่อบอกให้ Dagger รู้ว่าหาก dependencies ตัวใดต้องการ Context ให้ provide ด้วยวิธีที่ระบุในฟังก์ชั่นนี้ นั่นก็คือ application.applicationContext ทั้งนี้ชื่อฟังก์ชันเราจะตั้งเป็นชื่ออะไรก็ได้เพราะส่วนสำคัญอยู่ตรงที่ Retrun Type

ถัดมาจะเห็นว่ามีฟังก์ชั่นที่ Provide SharePreference โดยฟังก์ชั่นมีการรับ context เข้ามาเพื่อใช้สร้าง SharePreference คำถามถัดมาคือ แล้ว Dagger จะรู้ได้อย่างไรว่าจะเอา Context มาจากไหน? อย่างที่บอกไปตอนต้น เมื่อดูจาก Dependencies Graph ก็จะพบว่า มันสามารถหาContext ได้จากฟังก์ชั่น provideAppContext() ดังนั้น Context จะถูก Provide ให้กับฟังก์ชั่น provideSharePreference(context: Context) เพื่อนำไปสร้าง SharePreference ต่อไป

ถึงจุดนี้ ส่วนที่สำคัญสุดคือ Return Type เพราะ Dagger จะสร้าง Dependencies Graph โดยดูจากเจ้า Return Type นี่แหละดังนั้นมันจะออกแนวบังคับนิดหน่อยว่าต้องเป็น explicitly return type จะมาใช้พวก Generic type หรือ widecast type อย่างเช่น List<R> หรือ List<*> ไม่ได้

ขั้นตอนต่อไปเราจะสร้าง AppComponent ขึ้นมา ซึ่งจะได้หน้าตาประมาณนี้

จากโค้ดด้านบนจะเห็นว่า เราใช้ @Component คู่กับ Interface เพื่อระบุให้ Dagger สร้าง Classes ที่เกี่ยวข้องของ Component นี้ขึ้นมาภายหลัง (auto generate code) ถัดมาเราจะเห็นว่า มีการกำหนด Module ให้กับ Component ผ่านทาง parameter ของ @Component และสุดท้ายเราจะเห็นฟังก์ชั่น inject(activity: MainActivity) เป็นฟังก์ชั่นที่เราไว้ใช้เรียกเมื่อต้องการ inject dependencies ลงใน MainActivity ส่วน Class อื่นๆที่ไม่ได้ระบุลงใน AppComponent เช่นเดียวกับ MainActivity ก็จะไม่สามารถ Inject dependencies ผ่านทาง AppComponent ได้

Note: ชื่อฟังก์ชั่นไม่จำเป็นต้องเป็น inject() จะเป็นชื่ออะไรก็ได้ แต่ส่วนสำคัญคือ Parameter ที่รับ ซึ่งจะบอกว่าเราสามารถใช้ Component นั้นเพื่อ injectใน class ไหนได้บ้าง

ถัดมาคือการสร้าง instance ของ AppComponent โดยในที่นี้เราจะสร้างไว้ใน Class Application เนื่องจากเราต้องการให้ instance นี้อยู่ไปตลอดจนกว่า Application ของเราจะตาย โดยโค้ดของการสร้าง AppComponent จะเป็นดังนี้

ให้เราสังเกตบรรทัดที่ 11 เราเรียก Class ที่ชื่อว่า DaggerAppComponent ซึ่งเป็น Class ที่ Dagger สร้างขึ้นหลังจากเรา build project โดยจะเติม Dagger นำหน้าชื่อ Component ของเรา ถัดมาจะเห็นฟังก์ชั่น builder() เป็นฟังก์ชั่นที่ถูกสร้างขึ้นมาเพื่อใช้ในการสร้าง instance ของ Component จากนั้นเราจะเห็นฟังก์ชั่น appModule() โดยฟังก์ชั่นนี้จะอิงจาก Module ที่เรากำหนดไว้ใน @Component(modules = arrayOf(...)) ซึ่งในที่นี่เรามีแค่ AppModule โดยเราจะต้องสร้าง Instance ของ AppModule ให้กับฟังก์ชั่น appModule() เนื่องจาก AppModule มีการรับ Application ผ่านทาง Constructor และสุดท้ายคือการเรียกฟังก์ชั่น build() ในบรรทัดที่ 13 เพื่อสร้าง instance ของ AppComponent

เสริมความรู้: หาก Moduleใดก็ตามที่เรากำหนดให้กับ Component ไม่ได้รับ Argumentsใดๆเป็นพิเศษผ่านทาง Constructor เราไม่จำเป็นต้องเรียกฟังก์ชั่นของ module นั้นๆตอนสร้าง Component ตัวอย่างเช่น หาก AppMudule ไม่ได้รับ Application เราสามารถเขียนให้สั้นลงแบบนี้ได้ DaggerAppComponent.builder().build()

หลังจากที่เราได้ Instance ของ AppComponent แล้วขั้นตอนต่อไปคือการ inject depencencies ให้กับ MainActivity ผ่านทาง AppComponent ก็จะได้หน้าตาประมาณนี้

จากโค้ดด้านบน ในบรรทัดที่ 3 เราใช้ @Inject เพื่อระบุว่าเราต้องการให้ Inject SharePreference ลงใน class นี้ และในบรรทัดที่ 9 คือการสั่งให้ Dagger inject เพียงเท่านี้เราก็สามารถใช้งาน SharePreference ใน MainActivty ได้แล้ว

Component & Scope

ในบทความแรกเราได้พูดถึง Scope กันไปแล้ว โดย Scope คือวิธีการที่ช่วยให้เราสามารถทำ Global Singleton หรือ Local Singleton ซึ่งจะเก็บรักษา instance ของ Dependencies ไว้จนกว่า Scope ของ Component นั้นจะตายไป หากเราไม่ระบุ Scope จะทำให้ Dependencies ถูกสร้างใหม่ทุกครั้งที่มีการเรียกใช้

ให้เราลองนึกถึงการสร้าง object dependencies บางตัวที่กินทรัพยากรและเวลามาก (expensive initiated instance) อย่างเช่นพวก Network instance แบบ Retrofit หรือ object ที่ช่วยในการเข้าถึง Database อย่าง Repository เราคงไม่อยากสร้างมันทุกครั้งที่มีการเรียกใช้งาน ดังนั้นการทำ scope จึงมาช่วยแก้ปัญหานี้

อย่างที่บอกไว้ตอนต้นว่า เราต้องการให้ AppComponent คงอยู่ตลอดจนกว่า Application ของเราจะตายไป และเมื่อสมมุติว่า SharePreference คือ expensive instance เราก็คงไม่อยากจะสร้างมันบ่อยๆ ดังนั้นเราจะกำหนด Scope ให้มันดังนี้

ขั้นแรกให้ใส่ Scope ให้กับ SharePreference ใน AppModule ก่อน โดยเราจะใช้ Scope ที่ Dagger มีให้อย่าง @Singleton

เมื่อเราระบุ Scope ให้กับ Dependencies แล้ว เราต้องระบุ Scope ให้กับ Component ด้วย ไม่เช่นนั้น Dagger จะฟ้อง error ว่า “Component (unscoped) may not reference scoped bindings”

ถึงจุดนี้เราได้ AppComponent ที่สามารถ Inject Context และ SharePreference โดยจะคืนค่า object เหล่านี้เป็นตัวเดิมเสมอ ไม่มีการสร้างใหม่ และจะยังคงอยู่ตลอดจนกว่า AppComponent จะหายไป และเนื่องจาก AppComponent ถูกสร้างขึ้นมาในระดับ Application ดังนั้น มันจะยังคงอยู่จนกว่า Application จะตาย หรือที่เรียกว่า Application Scope (Global Singleton) นั่นเอง

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

@Component.Builder

Component Builder คือรูปแบบหนึ่งของการสร้าง Component ที่ช่วยให้เราสามารถ Customise บางอย่างได้ก่อนที่ Component จะถูกสร้างขึ้น โดยขณะที่สร้าง Compoment ขึ้นจาก @Component นั้น Dagger จะมองหา @Component.Builder ก่อน และสร้าง Component ขึ้นมาตามที่ระบุไว้ใน @Component.Builder

ทีนี่เรามาลองเปลี่ยน AppComponent ของเรา ให้มาใช้ @Component.Builder กันดูบ้าง แทนที่จะให้ Dagger generate code ขึ้นมาเอง จะได้หน้าตาประมาณนี้

จากโค้ดด้านบน เราประกาศ inner interface ที่ชื่อว่า Builder ในบรรทัดที่ 7 โดยกำกับด้วย @Component.Builder ในบรรทัดที่ 6 ภายใน Interface เราจะระบุฟังก์ชัน ที่รับ Module ทั้งหมด โดยแยกเป็นหนึ่งฟังก์ชั่นต่อหนึ่งโมดูล ซึ่งในที่นี่ Component นี้มีแค่ Module เดียวคือ AppModule และสุดท้าย ต้องมีหนึ่งฟังก์ชั่นที่คืนค่ากลับไปเป็น Component นั้นเสมอ ซึ่งในที่นี่คือ build(): AppComponent

DaggerAppComponent.builder()
.appModule(AppModule(application))
.build()

ถึงจุดนี้การเรียกใช้ AppComponent ก็ยังคงเหมือนเดิม ไม่มีอะไรเปลี่ยนไป ตามโค้ดด้านบน แล้วอย่างนี้ การใช้ @Component.Builder จะมีประโยชน์ อย่างไรล่ะ?

Customise AppComponent

อย่างที่บอก @Component.Builder มาเพื่อช่วยให้เราสามารถ Customise อะไรบางอย่างได้ก่อนที่ Component จะถูกสร้าง และยังสามารถให้เราใช้ annotation หรือท่าพิเศษอะไรบางอย่างได้ด้วย annotation หนึ่งที่นิยมใช้คู่กับ @Component.Builder นั้นคือ @BindsInstance

@BindsInstance ช่วยให้เราสามารถที่จะผูก instance ใดๆเข้ากับ Component รวมถึง provide dependencies นั้นให้กับ Component นั้นด้วย ฟังแล้วอาจจะดู งงๆ ลองไปดูตัวอย่างกันก่อน โดยเราจะแก้ไข้ AppComponent ดังนี้

จากนั้นให้แก้ AppModule เป็นดังนี้

จากโค้ดด้านบนจะเห็นว่าเราใช้ @BindsInstance ผูก Application instance เข้ากับ AppComponent ส่งผลให้ ณ ตอนนี้ AppComponent รู้จัก Application Type แล้ว ดังนั้นหากมีการอ้างถึง Application มันจะรู้ได้ทันทีว่าจะหาได้จากที่ไหน ซึ่งส่งผลให้ AppModule ไม่จำเป็นต้องรับ Application ผ่านทาง Constructer อีกแล้ว โดยมันสามารถอ้างถึง Application ได้เลย ดังจะสังเกตได้จากบรรทัดที่ 6 ใน AppModule ที่ฟังก์ชั่น provideContext() รับ paremeter เป็น Application เพื่อใช้ในการ provide Context และเมื่อ AppModule ไม่ต้องรับ Arguments ใดๆผ่านทาง Constructor ทำให้เราไม่จำเป็นต้องประกาศฟังก์ชั่น appModule() ใน Component Builder ไปด้วย

โดยทั่วไปเรามักใช้ @BindsInstance กับ Module ที่รับ argument ผ่านทาง Constructor หรือต้องการผูกค่าอะไรบางอย่างที่สำคัญและไม่อาจสร้างเองได้ใน Module เข้ากับ Component ตัวอย่างเช่น หากเราต้องการสร้าง Object ของ Retrofit เราสามารถใช้ @BindsInstance เพื่อ provide Base Url ได้ เนื่องจากขั้นตอนในการสร้าง Retrofit ต้องการ Base URL

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

@Inject constructor

ทิ้งท้ายกันนิดนึง จากที่เราทราบกันไปแล้วว่าหากเราต้องการ provide Dependencies อะไร ให้เอาไปใส่ไว้ใน Module แต่จริงๆแล้วยังมีอีกหนึ่งวิธิที่สามารถ provide Dependencies ได้โดยไม่ต้องประกาศใน Module นั้นคือ การใช้ @Inject ใน contructor ของ class นั้นๆ ตัวอย่างเช่น

@Singleton
class
GetNumberInteractor @Inject constructor() {
...
}

วิธีนี้จะทำให้ Dagger รู้จัก Type ของ Class นี้และสามารถ provide dependencies ให้กับใครก็ตามที่อ้างถึง instance ของ class นี้ได้ โดยที่เราไม่จำเป็นต้อง provide ใน Module แต่เราไม่แนะนำให้ใช้วิธีนี้!! เนื่องจากเมื่อโปรเจคของเราใหญ่ขึ้น เราจะ track back เพื่อหาว่า Dependencies object นี้ถูกสร้างขึ้นมาเมื่อไรยากมาก ดังนั้นถ้าไม่อยากปวดหัวกับการหาที่มาที่ไปของ Dependencies แนะนำว่าอย่าใช้

Conclusion

ถึงจุดนี้เราได้เรียนรู้ว่า Component และ Module คืออะไร มันสำคัญไฉน รวมถึงเราได้เรียนรู้วิธีการสร้าง Component แบบง่ายๆ และเข้าใจถึงการทำงานของ Component ร่วมกับ Scope รวมถึงได้เรียนรู้การใช้งาน @Component.Builder และ @BindInstance ด้วย

แต่นั้นเป็นเพียงแค่ Component เดียวซึ่งจริงๆแล้วใน Application หนึ่งคงไม่ได้มีแค่ Component เดียวเป็นแน่ ในบทความหน้า เราจะดูกันว่า หากเรามีหลายๆ component มันจะทำงานร่วมกันได้อย่างไร และเราจะสามารถอ้างถึง Dependencies ของอีก Component นึงได้อย่างไร รอติดตามกันนะจ๊ะ

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

--

--