พื้นฐาน Dagger2 - 102 - Component & Component Builder & @BindInstance
ในบนความที่แล้ว เราได้เรียกรู้หลักการของ Dependency Injection รวมถึงได้เข้าไปสัมผัสโลกของ Dagger2 (ต่อไปนี้จะเรียกว่า Dagger) บางส่วนในเรื่องของ Scope กันมาบ้างแล้ว โดยเฉพาะเรื่องของ Global Singelton และ Local Singleton ซึ่งหากใครยังไม่เข้าใจให้กลับไปอ่านก่อนนะจ๊ะ หากเห็นว่ามีประโยชน์ ฝากกด ❤️ กด Share ให้ด้วยนะจ๊ะ จะได้มีกำลังใจในการเขียนบทความต่อๆไป 😄
ในบทความนี้ เราจะมารู้จักการใช้งาน Dagger ซึ่งเราจะพูดถึงการสร้าง Component การใช้ Component Builder รวมถึง annotation พิเศษอย่าง @BindInstance
โดยบทความนี้เป็นส่วนหนึ่งของชุดบทความที่มีเนื้อหาแบ่งออกเป็นหัวข้อดังนี้
- Dependency Injection Concept & Scope
- Component & Component Builder & @BindInstance
- Component dependencies & Qualified types
- Subcomponent & SubComponent Builder
- 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 ให้กันด้วยนะจ๊ะ จะได้มีกำลังใจในการเขียนบทความต่อๆไป 😄