หลังจากที่เกริ่นเรื่อง Dagger ไปคร่าวๆแล้วในบทความก่อนหน้านี้ คราวนี้ก็ไม่ขอพูดพร่ำทำเพลง มาเริ่มเขียนโค้ดสำหรับ Dagger กันต่อเลยดีกว่า

สำหรับบทความตอนที่ 2 นี้ก็จะขอเริ่มเตรียมโค้ดต่างๆให้พร้อมสำหรับใช้งาน Dagger ในโปรเจคแอนดรอยด์กันเลยนะ

บทความในซีรีย์เดียวกัน

เพิ่ม Dagger ใน build.gradle

อย่างที่บอกกันไปแล้วว่า Dagger เวอร์ชันใหม่ๆเพิ่มความสามารถให้ใช้งานบนแอนดรอยด์ได้ง่ายขึ้นแล้ว ดังนั้นนอกจากจะเพิ่ม Dagger เข้าไปในโปรเจคแล้ว ก็อย่าลืมเพิ่ม Dagger สำหรับแอนดรอยด์ด้วยล่ะ

// build.gradle (Module: app)
apply plugin: 'kotlin-kapt'
/* ... */

dependencies {
    /* ... */
    // Dagger 2
    implementation 'com.google.dagger:dagger:2.27'
    kapt 'com.google.dagger:dagger-compiler:2.27'

    // For Android
    implementation 'com.google.dagger:dagger-android:2.27'
    implementation 'com.google.dagger:dagger-android-support:2.27'
    kapt 'com.google.dagger:dagger-android-processor:2.27'
}

Dependency 2 ตัวแรกนั้นเป็นของ Dagger แต่ถ้าอยากจะให้ใช้งานบนแอนดรอยด์ง่ายๆก็ให้เพิ่ม 3 ตัวหลังเข้าไปด้วยซะ และเนื่องจากเจ้าของบล็อกใช้เป็น Kotlin ดังนั้นจะต้องใช้ kapt แทน apt เพื่อให้ทำงานกับ Kotlin ได้ ดังนั้นใน build.gradle จึงต้องเพิ่ม Plugin ที่ชื่อว่า kotlin-kapt เข้าไปด้วย เพื่อให้สามารถใช้งาน kapt ได้

และถ้าในโปรเจคนั้นใช้ AndroidX ในตอนนี้ Dagger ยังใช้เป็น Android Support อยู่ เพื่อแก้ปัญหานี้ก็ให้ Exclude เฉพาะ Android Support ออกไปซะ

implementation 'com.google.dagger:dagger:2.27'
kapt 'com.google.dagger:dagger-compiler:2.27'

implementation('com.google.dagger:dagger-android:2.27') {
    exclude group: "com.android.support"
}
implementation('com.google.dagger:dagger-android-support:2.27') {
    exclude group: "com.android.support"
}
kapt('com.google.dagger:dagger-android-processor:2.27') {
    exclude group: "com.android.support"
}

แต่ก็คงอีกไม่น่าทีมงานก็น่าจะอัพเดท Dagger ตัวใหม่ที่ใช้ AndroidX แทนแล้วล่ะ

Dagger ประกอบไปด้วยอะไรบ้าง?

ในการใช้งาน Dagger นั้นจะประกอบไปด้วย 3 ส่วนหลักๆ คือ Component, Module และ Provide โดยที่

  • Component เป็นตัวกลางที่ควบคุมการทำ Dependency Injectio
  • Module เป็นตัวจัดกลุ่ม Provide
  • Provide เป็นตัวบอก Dagger ว่าทำคลาสใดๆให้เป็น Dependency

ดังนั้นในโปรเจคจะมี Component แค่ตัวเดียวเพื่อทำ DI และคลาสใดๆก็ตามที่อยากจะทำเป็น Dependency ก็กำหนดไว้ใน Provide (1 คลาสต่อ 1 Provide) นั่นหมายความว่าในโปรเจคสามารถมี Provide ได้หลายตัว และทุกตัวสามารถจัดกลุ่มให้อยู่ตาม Module ต่างๆได้ตามใจชอบ ถึงแม้ว่าจะทำเป็น Module ตัวเดียวก็ได้ แต่การทำ Module หลายๆตัว เพื่อแยกประเภทของ Provide แต่ละตัวให้เป็นระเบียบ เพราะว่า Provide จะถูกสร้างขึ้นมาเยอะมากตอนที่ใช้ Dagger

ถ้าไม่แบ่งกลุ่ม Module ให้ดีๆตั้งแต่แรก เวลามานั่งไล่โค้ดทีหลังก็จะลำบากนิดๆ

สมมติว่าเจ้าของบล็อกมี HomeActivity อยู่ และในนั้นต้องการใช้ Retrofit, Checker และ UserDao เจ้า Dagger ก็จะทำการสร้างคลาสดังกล่าวแล้วส่งเข้ามาให้ทันที และเนื่องจาก Retrofit นั้นจำเป็นต้องสร้างโดยใช้ OkHttp ด้วย เจ้า Dagger ก็จะไปสร้าง OkHttp ให้กับ Retrofit แล้วส่ง Retrofit มาให้ HomeActivity โดยอัตโนมัติ นั่นล่ะ เวทมนต์ของ Dagger

โดยมีเงื่อนไขว่าคลาสไหนก็ตามที่อยากจะให้ Dagger จัดการให้ ก็เตรียม Provide ไว้ให้เรียบร้อยซะ และถ้าคลาสนั้นๆจำเป็นต้องใช้คลาสอื่นด้วย ก็ให้ทำ Provide สำหรับคลาสย่อยด้วย (ตามหลักการของ Dependency Injection นั่นเอง)

จริงๆแล้วยังมีอย่างอื่นนอกจาก Component, Module และ Provide อีกนะ แต่เดี๋ยวจะพูดถึงในทีหลัง

เตรียม Dagger ในแบบฉบับแอนดรอยด์

ในการใช้งาน Dagger บนโปรเจคแอนดรอยด์จะต้องเตรียมคลาสต่างๆดังนี้

เตรียม Application

ในการใช้งาน Dagger จำเป็นต้องสร้างคลาส Application ขึ้นมาด้วย เพื่อกำหนดให้ Dagger ทำงาน

// AwesomeApplication.kt
class AwesomeApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        // เดี๋ยวจะใส่คำสั่งของ Dagger 2 ไว้ที่นี่
    }
}

แล้วก็อย่าลืมกำหนดใน Android Manifest เพื่อใช้คลาส Appliction ตัวนี้ด้วยล่ะ

<!-- AndroidManifest.xml -->
<manifest ...>
    <application ...
        android:name=".AwesomeApplication">
        /* ... */
    </application>

</manifest>

ในตอนนี้จะเตรียมไว้แค่นี้ก่อน เพราะว่าเดี๋ยวจะสร้างคลาสในส่วนอื่นๆต่อแล้วค่อยมากำหนดในนี้ทีหลัง

เตรียม Component

เพื่อให้ Component พร้อมสำหรับการ Inject เข้าไปใน Applicaiton ดังนั้นจึงต้องสร้างคลาส Component ขึ้นมาแบบนี้

// AppComponent.kt
@Singleton
@Component(
        modules = [
            AndroidSupportInjectionModule::class
        ])
interface AppComponent {
    @Component.Builder
    interface Builder {
        @BindsInstance
        fun application(application: Application): Builder

        fun build(): AppComponent
    }

    fun inject(awesomeApplication: AwesomeApplication)
}

จะเห็นว่ามีการกำหนด Module ที่ชื่อว่า AndroidSupportInjectionModule เข้าไปด้วย ซึ่งเป็น Module ใน Dagger ที่เตรียมมาให้แล้วสำหรับใช้งานกับแอนดรอยด์ เมื่อสร้าง Module ขึ้นมาใหม่ ก็จะต้องเพิ่มเข้าไปในนี้ด้วยนะ

โดยใน Component จะกำหนดให้ Inject คลาส Application ที่เจ้าของบล็อกสร้างขึ้นมา และใน Builder จะมี BindsInstance ด้วย ซึ่งเป็นการโยน Instance จากคลาส Application ของเจ้าของบล็อกนั่นเอง ซึ่งตรงนี้เป็นหัวใจสำคัญของการสร้าง Component สำหรับแอนดรอยด์เลยล่ะ

Build Gradle ซะ

หลังจากเตรียม Component เสร็จแล้ว จะต้องสั่งให้ Gradle ทำการ Build Project ใหม่อีกรอบก่อน เพื่อให้ Dagger สร้างไฟล์ต่างๆของ Dagger ขึ้นมา

เจ้า Component ที่เจ้าของบล็อกเคยสร้างไว้โดยกำหนดชื่อไฟล์ว่า AppComponent ก็จะถูก Dagger เอาไปสร้างเป็นไฟล์ที่ชื่อว่า DaggerAppComponent (ชื่อไฟล์อะไรก็ได้ จะถูกเพิ่ม Prefix ด้วยคำว่า Dagger เข้าไป)

เพื่อทดสอบว่า Component นั้นสร้างขึ้นมาอย่างถูกต้องและ Dagger ได้สร้างไฟล์ให้เรียบร้อยแล้ว ให้ลองค้นหาชื่อไฟล์นั้นดู ถ้ามีก็แปลว่ามาถูกทางแล้ว แต่ถ้าไม่มีก็ต้องลองเช็คดูใหม่ตั้งแต่ต้นแล้วล่ะว่าพลาดขั้นตอนไหนไป

เตรียม Injector

เริ่มจากสร้าง Interface ที่ชื่อว่า Injectable ขึ้นมา เอาไว้ระบุว่า Fragment ตัวไหนที่สามารถ Inject ได้ ซึ่ง Interface ตัวนี้จะเอาไปกำหนดไว้ใน Fragment ทุกๆตัวนั่นเอง

//Injectable.kt
interface Injectable

มาถึงขั้นตอนที่ต้องทำให้ Component สามารถใช้งานใน Application ได้แล้วล่ะ โดยให้สร้าง AppInjector ขึ้นมา

// AppInjector.kt
object AppInjector {
    fun init(awesomeApp: AwesomeApplication) {
        DaggerAppComponent.builder()
                .application(awesomeApp)
                .build().inject(awesomeApp)
        awesomeApp.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
            override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
                handleActivity(activity)
            }

            override fun onActivityStarted(activity: Activity) {

            }

            override fun onActivityResumed(activity: Activity) {

            }

            override fun onActivityPaused(activity: Activity) {

            }

            override fun onActivityStopped(activity: Activity) {

            }

            override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle?) {

            }

            override fun onActivityDestroyed(activity: Activity) {

            }
        })
    }

    private fun handleActivity(activity: Activity) {
        if (activity is HasSupportFragmentInjector) {
            AndroidInjection.inject(activity)
        }
        if (activity is FragmentActivity) {
            activity.supportFragmentManager
                    .registerFragmentLifecycleCallbacks(
                            object : FragmentManager.FragmentLifecycleCallbacks() {
                                override fun onFragmentCreated(
                                        fm: FragmentManager,
                                        f: Fragment,
                                        savedInstanceState: Bundle?
                                ) {
                                    if (f is Injectable) {
                                        AndroidSupportInjection.inject(f)
                                    }
                                }
                            }, true
                    )
        }
    }
}

ถ้าลองอ่านโค้ดดูก็จะพบว่าคำสั่งดังกล่างคือการทำให้ Activity และ Fragment นั้นรองรับ Dagger โดยดักตอนที่ Activity ถูกสร้างขึ้นมาก็จะเช็คว่า Activity มี Interface ที่ชื่อว่า HasSupportFragmentInject หรือป่าว ถ้ามีก็ทำการ Inject Activity ตัวนั้นๆเข้าไปใน AndroidInjection

และจะเช็คต่อว่าใน Activity นั้นๆมี Fragment หรือไม่ ถ้ามีก็จะทำการเช็คเมื่อ Fragment ถูกสร้างขึ้น โดยเช็คว่า Fragment ตัวนั้นๆมี Interface ที่ชื่อว่า Injectable หรือไม่ ถ้ามีก็จะทำการ Inject Fragment ตัวนั้นๆเข้าไปใน AndroidSupportInjection

นั่นหมายความว่าตอนที่สร้าง Activity ขึ้นมาจะต้องใส่ HasSupportFragmentInject ด้วย และถ้าสร้าง Fragment ก็ต้องใส่ Injectable เข้าไปเช่นกัน โดยที่ HasSupportFragmentInjector มีให้อยู่แล้วใน Dagger แต่ว่า Injectable จะต้องสร้างขึ้นมาเอง (ไม่เข้าใจเหมือนกันว่าทำไม)

หัวใจสำคัญของคลาสนี้ก็คือ DaggerAppComponent ที่เจ้าของบล็อกได้เตรียมไว้แล้วให้ Gradle สร้างขึ้นมาให้ก่อนหน้านี้

// AppInjector.kt
object AppInjector {
    fun init(awesomeApp: AwesomeApplication) {
        DaggerAppComponent.builder()
                .application(awesomeApp)
                .build().inject(awesomeApp)
        /* ... */
    }
    /* ... */
}

จึงเป็นที่มาว่าทำไมต้องไปสร้าง Application และ Component เตรียมไว้ก่อน

เมื่อเตรียม AppInjector พร้อมแล้ว ก็เอาไปใส่ไว้ใน Application ได้เลย

// AwesomeApplication.kt
class AwesomeApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        AppInjector.init(this)
    }
}

จริงๆแล้วผู้ที่หลงเข้ามาอ่านสามารถพิมพ์คำสั่งเข้าไปใน Application โดยตรงได้เลยน่ะแหละ แต่เนื่องจากโค้ดมันยาวมาก จึงควรสร้างคลาสขึ้นมาเพื่อแยกคำสั่งเหล่านี้ออกมาจากคำสั่งต่างๆในคลาส Application แทน

เพียงเท่านี้โปรเจคแอนดรอยด์ของเจ้าของบล็อกก็พร้อมสำหรับใช้งาน Dagger แล้ว

เตรียม Module และ Provide

สมมติว่าเจ้าของบล็อกมีคลาสตัวหนึ่งที่คอยจัดการเก็บข้อมูลของผู้ใช้ลงใน SharedPreference ที่มีหน้าตาแบบนี้

// UserPreference.kt
class UserPreference(var context: Context) {
    companion object {
        private const val PREFERENCE_NAME = "user_preference"
        private const val KEY_ID = "key_id"
        private const val KEY_NAME = "key_name"
    }

    private val sharedPreferences: SharedPreferences =
            Contextor.get().getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)

    fun saveUserId(id: Long) {
        sharedPreferences.edit().putLong(KEY_ID, id).apply()
    }

    fun saveUserName(name: String) {
        sharedPreferences.edit().putString(KEY_NAME, name).apply()
    }

    fun getUserId(): String? = sharedPreferences.getString(KEY_ID, null)

    fun getUserName(): String? = sharedPreferences.getString(KEY_NAME, null)
}

จะเห็นว่าคลาสดังกล่าวต้องโยน Context เข้ามาด้วย เพื่อให้สามารถใช้งาน SharedPreference ได้ และเจ้าของบล็อกก็อยากให้คลาสตัวนี้เป็น Dependency ซะ เพื่อให้ Dagger สามารถ Inject คลาสนี้ได้

ดังนั้นเจ้าของบล็อกจึงสร้าง Module และ Provide สำหรับ UserPreference ขึ้นมาแบบนี้

// PreferenceModule.kt
@Module
class PreferenceModule {
    @Singleton
    @Provides
    fun provideUserPreference(application: AwesomeApplication) =
            UserPreference(application.applicationContext)
}

คลาสที่เจ้าของบล็อกสร้างขึ้นมาจะเป็น Module ส่วนฟังก์ชันที่อยู่ข้างในนี้จะเป็น Provide โดยเจ้าของบล็อกจะต้องสร้าง Provide ขึ้นมาสำหรับ UserPreference จึงสร้างฟังก์ชันที่ชื่อว่า provideUserPreference ขึ้นมา (จริงๆจะกำหนดชื่อว่าอะไรก็ได้) แล้ว Return ค่าเป็นคลาส UserPreference ซะ

จะเห็นว่ามีการใส่ Singleton ไว้ด้วย ซึ่งเป็นการบอกให้ Dagger รู้ว่าสร้าง Instance ตัวนี้ขึ้นมาแค่ตัวเดียวนะ ไม่ต้องสร้างขึ้นมาทุกครั้งที่เรียกใช้งาน ถ้าเคยสร้างแล้วก็ให้โยนตัวนั้นมาให้เลย หรือก็คือ Dagger จะทำเหมือนว่าคลาสตัวนี้เป็น Singleton ให้อัตโนมัติโดยที่เจ้าของบล็อกไม่ต้องเขียนโค้ด Singleton เองเลย

เมื่อสังเกตดีๆจะเห็นว่าคำสั่ง provideUserPreference มีการโยน AwesomeApplication เข้ามาด้วย ซึ่งมาจากตอนที่เจ้าของบล็อกเขียนไว้ใน Injector และ Component นั่นเอง จึงทำให้ Dagger สามารถ Inject ได้แม้กระทั่งคลาส Application เลย และนั่นก็ทำให้เจ้าของบล็อกสามารถดึง Application Context เพื่อเอาไปกำหนดใน UserPreference ได้นั่นเอง

// AppInjector
object AppInjector {
    fun init(awesomeApp: AwesomeApplication) {
        DaggerAppComponent.builder()
                .application(awesomeApp)
        ...
    }
    ...
}

// AppComponent.kt
...
interface AppComponent {
    @Component.Builder
    interface Builder {
        @BindsInstance
        fun application(application: Application): Builder
        ...
    }
    ...
}

ตรงจุดนี้ผู้ที่หลงเข้ามาอ่านสามารถเขียนให้ Inject แค่ Context เข้ามาแทน Application ก็ได้เหมือนกันนะ แต่อย่าลืมนะว่า Context ที่ Inject เข้ามานั้นเป็นได้แค่ Base Context หรือ Application Context เท่านั้น

และสมมติว่าเจ้าของบล็อกมีอีกคลาสหนึ่งที่อยากจะทำเป็น Dependency เหมือนกัน และคลาสนั้นก็ต้องใช้ UserPreference ด้วย เจ้าของบล็อกสามารถทำเป็น Provide แล้วโยน UserPreference เข้ามาในฟังก์ชันนั้นได้เลย เดี๋ยว Dagger มันจัดการให้ เพราะว่าเคยสร้าง Provide สำหรับ UserPreference ไว้แล้วนั่นเอง

// PreferenceModule.kt
@Module
class PreferenceModule {
    @Singleton
    @Provides
    fun provideUserPreference(application: AwesomeApplication) =
            UserPreference(application.applicationContext)

    @Singleton
    @Provides
    fun provideAwesomeManager(userPreference: UserPreference) =
            AwesomeManager(userPreference)
}

เมื่อสร้าง Module และ Provide เรียบร้อยแล้ว ก็เอา Module ไปกำหนดไว้ใน Component ด้วยล่ะ เดี๋ยว Dagger มองไม่เห็น

// AppComponent.kt
@Singleton
@Component(
        modules = [
            AndroidSupportInjectionModule::class,
            PreferenceModule::class
        ])
interface AppComponent {
    /* ... */
}

เสร็จเรียบร้อยแล้ว ตอนนี้โปรเจคของเจ้าของบล็อกก็สามารถใช้งาน Dagger ได้แล้ว โดยมีคลาส UserPreference เป็นคลาสที่สามารถเอาไป Inject ที่ไหนก็ได้ (ต้องเป็นที่ที่ทำเป็น Dependency ไว้แล้วเหมือนกัน)

สรุป

ในการใช้งาน Dagger นั้นจะขาด Component, Module และ Provide ไปไม่ได้เลย เพราะทั้ง 3 คือองค์ประกอบสำคัญในการทำงานของ Dagger ซึ่งจำเป็นต้องทำทุกครั้งเพื่อให้ใช้งาน Dagger ได้ และเมื่อมีคลาสใดๆก็ตามที่อยากจะทำเป็น Dependency เพื่อให้ Dagger สามารถ Inject ได้ ก็จะต้องสร้าง Provide สำหรับคลาสนั้นๆขึ้นมาเสมอ และเมื่อ Provide มีจำนวนเยอะมาก ก็ควรแบ่งออกเป็นหลายๆ Module เพื่อจัดกลุ่มให้ง่ายต่อการอ่านโค้ดด้วยนะ

สำหรับในบทความนี้ก็จะเป็นการเตรียมคลาสพื้นฐานของ Dagger และคลาสที่ต้องการทำเป็น Dependency เท่านั้น ที่ยังขาดไปก็จะเป็นการสร้าง Activity และ Fragment เพื่อ Inject คลาสเหล่านี้เข้าไปเพื่อเรียกใช้งาน โปรดรอบทความหน้านะ