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

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

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

เริ่มต้นที่ Activity กันก่อนเลย

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

// ActivityModule.kt
@Module
abstract class ActivityModule {
    // เดี๋ยวจะใส่ Activity ทุกๆตัวไว้ในนี้เพื่อทำเป็น Dependency
}

ซึ่งในนี้ก็จะใส่ Activity ทุกๆตัวเพื่อทำเป็น Dependency นั่นเอง

ถ้าโปรเจคของผู้ที่หลงเข้ามาอ่านมีขนาดใหญ่มาก และเต็มไปด้วย Activity ต่างๆมากมาย ก็สามารถสร้าง Module เป็นหลายๆตัวเพื่อแยก Activity เป็นหลายๆกลุ่มได้เช่นกันนะ

เมื่อสร้าง Module ใดๆก็ตามให้กับ Dagger 2 ก็อย่าลืมเพิ่มเข้าไปใน AppComponent ด้วยนะ

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

เมื่อ Module สำหรับ Activity นั้นพร้อมใช้งานแล้ว ต่อไปก็มาดูกันที่คลาส Activity กันต่อ

// MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        ...
    }
}

ในตอนนี้ MainActivity ยังไม่รองรับ Dependency Injection เลยซักนิด ดังนั้นถ้าเจ้าของบล็อกไปเรียกใช้งานคลาส AwesomeManager ผ่าน Dagger 2 แบบนี้

// MainActivity.kt
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var awesomeManager : AwesomeManager
    ...
}

ก็จะเจอ Runtime Exception แบบนี้

FATAL EXCEPTION: main
Process: com.akexorcist.awesomeapp, PID: 16869
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.akexorcist.awesomeapp/com.akexorcist.awesomeapp.MainActivity}: kotlin.UninitializedPropertyAccessException: lateinit property awesomeManager has not been initialized

เนื่องจาก MainActivity ยังไม่ได้ทำเป็น Dependency ดังนั้น Dagger 2 ก็จะไม่ Inject คลาส AwesomeManager เข้ามาให้นั่นเอง

สำหรับการทำ Activity ให้กลายเป็น Dependency ก็จะยุ่งยากเล็กน้อย (แค่ครั้งแรกแหละ) เพราะว่า Activity นั้นไม่ใช่ Component ทั่วๆไปเหมือนอย่าง AwesomeManager หรือ UserPreference จึงเป็นที่มาว่าทำไมต้องมี Dagger 2 สำหรับ Android เพิ่มเข้ามาด้วยนั่นเอง

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

// AwesomeApplication.kt
class AwesomeApplication : Application(), HasActivityInjector {
    @Inject
    lateinit var activityDispatchingAndroidInjector: DispatchingAndroidInjector<Activity>

    override fun activityInjector(): AndroidInjector<Activity> = activityDispatchingAndroidInjector
    ...
}

คลาส Application จะต้องประกาศ​ HasActivityInjector ไว้ด้วยเพื่อให้ Dagger 2 รู้ว่ามี Activity ที่จะต้องทำ Dependency Injection อยู่นะ โดยที่ DispatchingAndroidInjector เป็นคลาสที่จะช่วยให้ Dagger 2 สามารถทำ Dependency Injection ให้กับ Android Framework Component อย่าง Activity หรือ Fragment ได้ เพราะตัว Dagger 2 ไม่สามารถสร้าง Component เหล่านี้ขึ้นมาได้ด้วยตัวมันเอง จำเป็นต้องให้ Android Framework สร้างขึ้นมาให้ผ่านคลาส Application นี่แหละ

และอย่าลืมเพิ่ม Activity ที่ต้องการเข้าไปใน ActivityModule ด้วยล่ะ

// ActivityModule.kt
@Module
abstract class ActivityModule {
    @ContributesAndroidInjector()
    abstract fun contributeMainActivity(): MainActivity
}

สำหรับ Activity นั้นจะไม่ได้ใช้ @Inject เหมือนคลาสทั่วๆไป แต่จะใช้เป็น @ContributesAndroidInjector แทน ซึ่งเป็น Annotation ที่ใช้กับ Android Framework Component เท่านั้น เพราะมันจะทำงานร่วมกับคลาส AndroidInjector ที่ประกาศไว้ในคลาส Application ก่อนหน้านี้นั่นเอง ส่วนการตั้งชื่อ Method ว่า contributeMainActivity() จริงๆแล้วสามารถตั้งได้ตามใจชอบ ไม่จำเป็นต้องเป็นชื่อนี้ แต่การตั้งชื่อในรูปแบบนี้จะช่วยให้เข้าใจได้ง่ายกว่าการตั้งชื่อแบบอื่นๆ

และใน Activity ตัวนั้นๆจะต้องประกาศ HasSupportFragmentInjector และเตรียม DispatchingAndroidInjector สำหรับ Fragment เพื่อให้ Dagger 2 รองรับกับ Fragment ด้วย เพราะว่า Fragment จะถูกสร้างขึ้นตอนที่ Activity ทำงานนั่นเอง ก็เลยต้องกำหนดไว้ในนี้เพื่อให้ Dagger 2 สามารถทำ Dependency Injection กับ Fragment ได้

// MainActivity.kt
class MainActivity : AppCompatActivity(), HasSupportFragmentInjector {
    @Inject
    lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>

    override fun supportFragmentInjector() = fragmentDispatchingAndroidInjector
    ...
}

คล้ายๆกับ Activity เลยเนอะ แต่ของ Fragment จะต้องมาประกาศไว้ใน Activity ทุกๆตัวแทนนั่นเอง

เอ…​เดี๋ยวนะ ต้องประกาศไว้ใน Activity ทุกตัวเลยหรอ?

ใช่ครับ แต่นักพัฒนาที่ดีก็ต้องมีความขี้เกียจเป็นพื้นฐานเนอะ เจ้าของบล็อกก็เลยรวบคำสั่งเหล่านี้ไปใส่ไว้ใน Abstract Class ให้กับ Activity แทนดีกว่า จะได้ไม่ต้องมานั่งประกาศทุกครั้งให้เสียเวลา

// BaseActivity.kt
abstract class BaseActivity : AppCompatActivity(), HasSupportFragmentInjector {
    @Inject
    lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>

    override fun supportFragmentInjector() = fragmentDispatchingAndroidInjector
}

// MainActivity.kt
class MainActivity : BaseActivity() {
    ...
}

เวลาสร้าง Activity ขึ้นมาใหม่ก็แค่ Extend จาก BaseActivity ก็พอ

เพียงเท่านี้คลาส Activity ของเจ้าของบล็อกก็สามารถ Inject คลาสอื่นๆอย่าง AwesomeManager หรือ UserPreference เข้ามาใช้งานได้แล้ว

และเมื่อใดก็ตามที่มีการเพิ่ม Activity เข้ามาใหม่ สิ่งที่เจ้าของบล็อกต้องทำทุกครั้งก็คือให้คลาสนั้น Extend จาก BaseActivity และเพิ่ม Activity นั้นๆเข้าไปใน ActivityModule เท่านั้นเอง ไม่ต้องทำอะไรเพิ่มแล้ว

// ProfileActivity.kt
class ProfileActivity : AppCompatActivity() {
    ...
}

// ActivityModule.kt
@Module
abstract class ActivityModule {
    ...
    @ContributesAndroidInjector()
    abstract fun contributeProfileActivity(): ProfileActivity
}

เห็นมั้ยล่ะ ลำบากแค่ตอนแรกๆเท่านั้นแหละ แต่หลังจากนั้นชีวิตก็สะดวกสบายแล้ว

แล้วถ้าเป็น Fragment ล่ะ?

แน่นอนว่านักพัฒนาส่วนใหญ่นั้นใช้ Fragment กันเป็นชีวิตจิตใจกันอยู่แล้ว และโค้ดส่วนใหญ่ก็มักจะอยู่ใน Fragment เช่นกัน ดังนั้นเพื่อให้ชีวิตสะดวกสบายมากขึ้นกว่าเดิมก็มาทำให้ Fragment ของผู้ที่หลงเข้ามาอ่านนั้นรองรับกับ Dagger 2 กันเถอะ

อย่างแรกสุดก็คือการเตรียม Module สำหรับ Fragment เพื่อที่จะได้บอก Dagger 2 ว่ามี Fragment ตัวไหนบ้างที่ต้องทำเป็น Dependency

// FragmentModule.kt
@Module
abstract class FragmentModule {
    // เดี๋ยวจะใส่ Fragment ทุกๆตัวไว้ในนี้เพื่อทำเป็น Dependency
}
ถ้าโปรเจคมีขนาดใหญ่มากและมี Fragment มากมายหลายตัวก็สามารถแยกเป็นหลายๆ Module เพื่อจัดกลุ่มของ Fragment แต่ละตัวได้เหมือนกันนะ

แต่ทว่า Module ของ Fragment เนี่ย ไม่จำเป็นต้องเพิ่มเข้าไปใน AppComponent หรอกนะ แต่จะให้เพิ่มเข้าไปใน Module ของ Activity แบบนี้แทน

// ActivityModule.kt
@Module
abstract class ActivityModule {
    @ContributesAndroidInjector(modules = [FragmentModule::class])
    abstract fun contributeMainActivity(): MainActivity

    @ContributesAndroidInjector(modules = [FragmentModule::class])
    abstract fun contributeProfileActivity(): ProfileActivity
}

Parameter ของ @ContributesAndroidInjector ที่ชื่อว่า modules นั้นเป็นการบอกให้รู้ว่า Activity แต่ละตัวนั้นมี Sub Module ที่ชื่อว่า FragmentModule อยู่นะ ดังนั้นถ้า Dagger 2 ทำการ​ Inject คลาส Activity ตัวไหนเมื่อไรก็อย่าลืม Inject คลาส Fragment ที่อยู่ใน FragmentModule ด้วย

ในโปรเจคที่มีขนาดใหญ่มากและมีการจัดกลุ่ม Activity และ Fragment ออกเป็นหลายๆกลุ่ม ก็สามารถแยก Module ทั้ง Activity และ Fragment ได้ตามใจชอบ แล้วกำหนด Module สำหรับ Fragment แยกตามที่ Activity เหล่านั้นใช้งานได้เช่นกัน

จากนั้นก็ลองสร้าง Fragment ขึ้นมาซักตัวดีกว่า โดยมีหน้าตาที่เรียบง่ายแบบนี้

// BestSellerFragment.kt
class BestSellerFragment : Fragment(), Injectable {
    ...
}

โดยให้ประกาศ Injectable ไว้ด้วยทุกครั้ง

ถ้าขี้เกียจใส่แบบนี้ทุกครั้ง ก็สามารถทำเป็น Abstract Class สำหรับ Fragment ได้เช่นกัน

// BaseFragment.kt
abstract class BaseFragment : Fragment(), Injectable {
    ...
}

// BestSellerFragment.kt
class BestSellerFragment : BaseFragment() {
    ...
}

และอย่าลืมเพิ่ม Fragment เหล่านั้นไว้ใน Module ของ Fragment ด้วยนะ

// FragmentModule.kt
@Module
abstract class FragmentModule {
    @ContributesAndroidInjector
    abstract fun contributeBestSellerFragment(): BestSellerFragment
}

จะเห็นว่าการประกาศ Fragment ไว้ใน Module ของ Fragment จะมีรูปแบบที่คล้ายกับ Activity เป๊ะๆเลย เพราะว่าเป็น Android Framework Component เหมือนกันนั่นเอง

เพียงเท่านี้ Fragment ของเจ้าของบล็อกก็สามารถ Inject คลาสใดๆก็ตามที่เตรียมไว้ใน Dagger 2 มาใช้งานได้แล้ว เย้!

// BestSellerFragment.kt
class BestSellerFragment : BaseFragment() {
    @Inject
    lateinit var awesomeManager: AwesomeManager
    ...
}

สรุป

การทำ Dependency Injection ให้กับ Activity และ Fragment ด้วย Dagger 2 นั้นจะมีความแตกต่างจากคลาสทั่วๆไปพอสมควร เนื่องจากทั้งคู่เป็น Android Framework Component ที่ Dagger 2 ไม่สามารถสร้างขึ้นมาได้ด้วยตัวเอง จึงต้องมีการเพิ่ม Library ของ Dagger 2 เพื่อให้รองรับกับแอนดรอยด์เพิ่มเข้ามา ซึ่งจะมี DispatchingAndroidInjector เพิ่มเข้ามาเพื่อช่วยให้ Dagger 2 สามารถทำ Dependency Injection ให้กับ Component เหล่านี้ได้

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

// AppInjector.kt
object AppInjector {
    ...

    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 ถูกสร้างขึ้นมา ก็จะทำการเช็ค Activity นั้นๆ ว่ามีการประกาศ HasSupportFragmentInjector ไว้หรือไม่ เพื่อทำ Injection ให้กับ Activity ตัวนั้นๆ ถ้า Activity ตัวนั้นๆมีการสร้าง Fragment ขึ้นมาและใน Fragment นั้นๆ มีการประกาศ Injectable เอาไว้ด้วย ก็จะทำการ Injection ให้กับ Fragment ด้วยเช่นกัน

และการกำหนด DispatchingAndroidInjector ให้กับ Activity และ Fragment จะมีรูปแบบง่ายๆดังนี้

  • DispatchingAndroidInject สำหรับ Activity กำหนดไว้ใน Application
  • DispatchingAndroidInject สำหรับ Fragment กำหนดไว้ใน Activity

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

คราวหน้ามาดูกันว่าถ้าอยากจะทำ Dependency Injection ให้กับ Android Framework Component ตัวอื่นๆจะต้องทำยังไงบ้าง