16 October 2018

Dagger 2 in Android [Part 4] - ทำ Dependency Injection ให้กับ Android Framework Component ต่างๆ

Updated on


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

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

บทความที่เกี่ยวข้อง

        • [ตอนที่ 1] - Dependency Injection แบบหล่อๆด้วย Dagger 2
        • [ตอนที่ 2] - มาเตรียมโปรเจคสำหรับ Dagger กัน
        • [ตอนที่ 3] - ทำ Dependency Injection ให้กับ Activity และ Fragment
        • [ตอนที่ 4] - ทำ Dependency Injection ให้กับ Android Framework Component ต่างๆ

        โดยการทำ Dependency Injection ด้วย Dagger 2 ในบทความนี้จะมี Component ของ Android Framework ดังนี้

        • Service
        • Content Provider
        • Broadcast Receiver
        • Work Manager
        • View Model

        อยากจะดูวิธีของ Component ตัวไหนก็เลื่อนลงไปดูได้เลยจ้า

Service

         Service นั้นถือว่าเป็น 1 ใน 4 Component พื้นฐานของแอนดรอยด์ ซึ่งบ่อยครั้งนักพัฒนาก็ต้องใช้งาน Service เพื่อทำงานบางอย่างที่ Activity ไม่สามารถทำได้ และจำเป็นต้องใช้ Dagger 2 เพื่อ Inject บางอย่างเข้ามาใช้งานในนี้นั่นเอง

        เริ่มจากการสร้าง Module สำหรับ Service ขึ้นมาก่อนเลย

// ServiceModule.kt
@Module
abstract class ServiceModule {
    // เดี๋ยวจะมาเพิ่ม Service ไว้ในนี้ทีหลัง
}

        จากนั้นก็เพิ่ม Module ของ Service ไว้ใน AppComponent ซะ

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

         และในคลาส Application จะต้องประกาศ HasServiceInjector เพื่อให้ Dagger 2 รู้ว่าจะต้องทำ Dependency Injection ให้กับ Service ด้วย รวมไปถึงการสร้าง DispatchingAndroidInjector ให้กับ Service

// AwesomeApplication.kt
class AwesomeApplication : Application(), HasServiceInjector {
    @Inject
    lateinit var serviceDispatchingAndroidInjector: DispatchingAndroidInjector<Service>

    override fun serviceInjector(): AndroidInjector<Service> = serviceDispatchingAndroidInjector
    ...
}

        จากนั้นก็สร้าง Service ที่ต้องการขึ้นมา โดยที่ onCreate() ของคลาส Service จะต้องใส่คำสั่งเพื่อให้ Dagger 2 ทำการ Inject คลาส Service ตัวนั้นๆด้วย

// PhotoUploadService.kt
class PhotoUploadService : Service() {
    override fun onCreate() {
        super.onCreate()
        AndroidInjection.inject(this)
    }
    ...
}

        ซึ่งจะต่างจากตอนที่ Inject คลาส Activity หรือ Fragment เพราะว่าทั้งสองคลาสนั้นจะทำในคลาส Application อีกทีหนึ่ง แต่เพราะว่าคลาส Service ไม่ได้มี Event Listener เพื่อบอกให้รู้ว่าคลาส Service ถูกสร้างขึ้นเมื่อใด ดังนั้นผู้ที่หลงเข้ามาอ่านก็จะต้องมาใส่คำสั่งของ AndroidInjection ไว้ในคลาส Service เอง

        และถ้าขี้เกียจประกาศแบบนั้นทุกครั้ง สามารถทำเป็น Abstract Class สำหรับคลาส Service ได้เหมือนกันนะ

// BaseService.kt
abstract class BaseService : Service() {
    override fun onCreate() {
        super.onCreate()
        AndroidInjection.inject(this)
    }
}

// AwesomeService.kt
class AwesomeService : BaseService() {
    ...
}

        เมื่อสร้างคลาส Service ขึ้นมาแล้วก็อย่าลืมไปเพิ่มไว้ใน Module ของ Service ด้วยล่ะ

// ServiceModule.kt
@Module
abstract class ServiceModule {
    @ContributesAndroidInjector()
    abstract fun contributeAwesomeService(): AwesomeService
}

        เพียงเท่านี้ก็สามารถเรียกใช้คลาสต่างๆผ่าน Dependency Injection ของ Dagger 2 ในคลาส Service ได้แล้ว

// AwesomeService.kt
class AwesomeService : BaseService() {
    @Inject
    lateinit var androidUtil: NextzyAndroidUtil
    ...
}

Content Provider

        การสร้าง Content Provider เพื่อให้รองรับ Dependency Injection ใน Dagger 2 นั้นเหมือนกับ Service เป๊ะๆ โดยเริ่มจากการสร้าง Module สำหรับ Content Provider ดังนี้

// ContentProviderModule.kt
@Module
abstract class ContentProviderModule {
    // เดี๋ยวจะประกาศ ContentProvider ไว้ในนี้ทีหลัง
}

        แล้วเพิ่ม Module ของ Content Provider ไว้ใน AppComponent ให้เรียบร้อย

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

        และอย่าลืมประกาศ HasContentProviderInjector และสร้าง DispatchingAndroidInjector สำหรับ Content Provider ในคลาส Application ด้วยล่ะ

// AwesomeApplcation.kt
class AwesomeApplication : Application(), HasContentProviderInjector {
    @Inject
    lateinit var contentProviderDispatchingAndroidInjector: DispatchingAndroidInjector<ContentProvider>

    override fun contentProviderInjector(): AndroidInjector<ContentProvider> = contentProviderDispatchingAndroidInjector
    ...
}

        เมื่อพร้อมแล้วก็ให้สร้าง Content Provider ขึ้นมาได้เลย โดยเรียกคลาส AndroidInjector ใน onCreate() ซะ

// AwesomeContentProvider.kt
class AwesomeContentProvider : ContentProvider() {
    override fun onCreate(): Boolean {
        AndroidInjection.inject(this)
        ...
    }
    ...
}

         จากนั้นก็ให้ประกาศ Content Provider ที่สร้างขึ้นมาไว้ใน Module ของ Content Provider ด้วย

class AwesomeContentProvider : ContentProvider() {
    override fun onCreate(): Boolean {
        AndroidInjection.inject(this)
        ...
    }
    ...
}

           เสร็จแล้ว อยากจะ Inject อะไรเข้ามาก็ทำได้เต็มที่เลย

// AwesomeContentProvider.kt
class AwesomeContentProvider : ContentProvider() {
    @Inject
    lateinit var awesomeManager : AwesomeManager
    ...
}

Broadcast Receiver

        สำหรับ Broadcast Receiver นั้นก็จะมีขั้นตอนไม่ต่างอะไรไปจาก Service และ Content Provider ซักเท่าไร โดยเริ่มจากการสร้าง Module สำหรับ Broadcast Receiver เตรียมไว้ให้เรียบร้อยก่อน

// BroadcastReceiverModule.kt
@Module
abstract class BroadcastReceiverModule {
    // เดี๋ยวจะประกาศ BroadcastReceiver ไว้ในนี้ทีหลัง
}

         เมื่อสร้าง Module สำหรับ Broadcast Receiver เรียบร้อยแล้วก็ให้ประกาศไว้ใน AppComponent ให้เรียบร้อย

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

         และอย่าลืมประกาศ​ HasBroadcastReceiverInjector ไว้ในคลาส Application และสร้าง DispatchingAndroidInjector สำหรับ Broadcast Receiver

// AwesomeApplication.kt
class AwesomeApplication : Application(), HasBroadcastReceiverInjector {
    @Inject
    lateinit var receiverDispatchingAndroidInjector: DispatchingAndroidInjector<BroadcastReceiver>

    override fun broadcastReceiverInjector(): AndroidInjector<BroadcastReceiver> = receiverDispatchingAndroidInjector
    ...
}

         จากนั้นก็ทำการสร้าง Broadcast Receiver ขึ้นมาซะ

// AwesomeBroadcastReceiver.kt
class AwesomeBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        AndroidInjection.inject(this, context)
        ...
    }
}

         จะเห็นว่า Broadcast Receiver เรียกคลาส AndroidInjection ใน onReceive(...) เพราะว่าตัวมันเองนั้นไม่มี onCreate() เหมือนคลาสอื่นๆ

         ประกาศ Broadcast Receiver ที่สร้างขึ้นมาไว้ใน Module ของ Broadcast Receiver ให้เรียบร้อย

// BroadcastReceiverModule.kt
@Module
abstract class BroadcastReceiverModule {
    @ContributesAndroidInjector
    abstract fun contributeAwesomeBroadcastReceiver(): AwesomeBroadcastReceiver
}

        เพียงเท่านี้ Service ก็พร้อมใช้งานแล้ว

// AwesomeBroadcastReceiver.kt
class AwesomeBroadcastReceiver : BroadcastReceiver() {
    @Inject
    lateinit var androidUtil: NextzyAndroidUtil
    ...
}

WorkManager

        WorkManager เป็น Component ตัวใหม่ที่เพิ่มเข้ามาใน Android Architecture Components ที่จะช่วยแก้ปัญหาเรื่อง Background Service ได้อย่างง่ายดาย แต่เพราะว่ามันเป็นของใหม่ ดังนั้นการจะทำให้ WorkManager รองรับกับ Dagger 2 ก็เลยต้องเขียนโค้ดเพิ่มเติมเยอะกว่าชาวบ้านเสียหน่อย ซึ่งเจ้าของบล็อกอ้างอิงจาก [Dagger] integration with workers (For Java People) [Gist GitHub]

        ซึ่งจะต้องสร้างคลาสต่างๆขึ้นมาเองดังนี้

        • AndroidWorkerInjection
        • AndroidWorkerInjectionModule
        • HasWorkerInjector
        • WorkerKey

        ปกติแล้วเจ้าของบล็อกจะต้องสั่ง Inject คลาสใดๆก็ตามด้วย AndroidInjection แต่ทว่าคลาสดังกล่าวรองรับแค่ Component หลักของ Android Framework เท่านั้น ไม่ได้รองรับกับ WorkManager ก็เลยต้องมานั่งสร้างเองแบบนี้

// AndroidWorkerInjection.kt
class AndroidWorkerInjection {
    companion object {
        fun inject(worker: Worker) {
            checkNotNull(worker)
            val application = worker.applicationContext
            if (application !is HasWorkerInjector) {
                throw RuntimeException("${application.javaClass.canonicalName} does not implement ${HasWorkerInjector::class.java.canonicalName}")
            }

            val workerInjector = (application as HasWorkerInjector).workerInjector()
            checkNotNull(workerInjector)
            workerInjector.inject(worker)
        }
    }
}

// AndroidWorkerInjectionModule.kt
@Module
abstract class AndroidWorkerInjectionModule {
    @Multibinds
    internal abstract fun workerInjectorFactories(): Map<Class<out Worker>, AndroidInjector.Factory<out Worker>>
}

        รวมไปถึง HasWorkerInjector เช่นกัน เพราะ Dagger 2 แบบฉบับแอนดรอยด์จะมีให้แค่ Activity, Fragment, Service, Broadcast Receiver และ Content Provider เท่านั้น

// HasWorkerInjector.kt
interface HasWorkerInjector {
    fun workerInjector(): AndroidInjector<Worker>
}

        จบท้ายด้วยการสร้าง WorkerKey

// WorkerKey.kt
@MustBeDocumented
@Target(
        AnnotationTarget.FUNCTION,
        AnnotationTarget.PROPERTY_GETTER,
        AnnotationTarget.PROPERTY_SETTER
)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class WorkerKey(val value: KClass<out Worker>)

        โดย AndroidWorkerInjectionModule จะต้องใส่ไว้ใน AppComponent แบบนี้

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

        และสร้าง Module ของ Worker รอไว้ซะ

// WorkerModule.kt
@Module
abstract class WorkerModule {
    // เดี๋ยวจะประกาศ Worker ต่างๆไว้ในนี้
}

        แล้วประกาศไว้ใน AppComponent ด้วยล่ะ

// AppComponent.kt
@Singleton
@Component(
        modules = [
            ...,
            AndroidWorkerInjectionModule::class,
            WorkerModule::class
        ])
interface AppComponent {
    ...
}

         ในขั้นตอนนี้ให้สร้าง Worker ที่ต้องการขึ้นมาซะ แล้วเรียกคลาส AndroidWorkerInjection ที่สร้างขึ้นมาสำหรับ Worker โดยเฉพาะ โดยให้เรียกใน doWork() เพราะว่า Worker ไม่มี onCreate() ให้ใช้งานเหมือนคลาสอื่นๆ

// PhotoUploadWorker.kt
class PhotoUploadWorker : Worker() {
    override fun doWork(): Result {
        AndroidWorkerInjection.inject(this)
        ...
    }
}

        สำหรับการเพิ่ม Worker เข้าไปใน Module ของ Worker จะซับซ้อนกว่าชาวบ้านอยู่หน่อยนึง เพราะจะต้องสร้างเป็น Sub Component ขึ้นมาก่อน แล้วค่อยเพิ่มเข้าไปใน Module แบบนี้

// WorkerModule.kt
@Module(subcomponents = [PhotoUploadWorkerModule::class])
abstract class WorkerModule {
    @Binds
    @IntoMap
    @WorkerKey(PhotoUploadWorker::class)
    abstract fun bindPhotoUploadWorkerFactory(workerModuleBuilder: PhotoUploadWorkerModule.Builder): AndroidInjector.Factory<out Worker>
}

// PhotoUploadWorkerModule.kt
@Subcomponent
interface PhotoUploadWorkerModule : AndroidInjector<PhotoUploadWorker> {
    @Subcomponent.Builder
    abstract class Builder : AndroidInjector.Builder<PhotoUploadWorker>()
}

        นั่นหมายความว่าถ้ามี Worker ตัวอื่นๆด้วย ก็จะต้องทำเป็น Sub Component ก่อน แล้วค่อยนำมา Binding ใน Module ของ Worker เช่นกัน

         เพียงเท่านี้ก็สามารถใช้งาน WorkManager ร่วมกับ Dagger 2 ได้แล้ว

// PhotoUploadWorker.kt
class PhotoUploadWorker : Worker() {
    @Inject
    lateinit var awesomeManager: AwesomeManager
    ...
}

View Model

        ViewModel ก็เป็นอีกหนึ่ง Component ใหม่ที่เพิ่มเข้ามาใน Android Architecture Components ที่จะช่วยให้นักพัฒนาแอนดรอยด์สามารถจัดการกับโปรเจคด้วย MVVM ได้ง่ายขึ้น แต่ถ้าอยากจะใช้งานร่วมกับ Dagger 2 ก็ต้องเขียนบางส่วนเพิ่มเองเช่นกัน เพราะว่า Dagger 2 ยังไม่ได้รองรับกับ View Model โดยตรง

        โดยจะมีคลาสที่ต้องเตรียมไว้สำหรับ View Model ดังนี้

        • ViewModelFactory
        • ViewModelKey

// ViewModelKey.kt
@MustBeDocumented
@Target(
        AnnotationTarget.FUNCTION,
        AnnotationTarget.PROPERTY_GETTER,
        AnnotationTarget.PROPERTY_SETTER
)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)

// ViewModelFactory.kt
@Singleton
class ViewModelFactory @Inject constructor(
        private val creators: Map<Class<out ViewModel>,
                @JvmSuppressWildcards Provider<ViewModel>>)
    : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        val creator = creators[modelClass] ?: creators.entries.firstOrNull {
            modelClass.isAssignableFrom(it.key)
        }?.value ?: throw IllegalArgumentException("unknown model class $modelClass")
        try {
            @Suppress("UNCHECKED_CAST")
            return creator.get() as T
        } catch (e: Exception) {
            throw RuntimeException(e)
        }

    }
}

        จะเห็นว่าการทำให้ View Model รองรับ Dagger 2 นั้นจะมีการสร้าง Custom Factory ขึ้นมาเอง เพื่อให้ Dagger 2 สามารถจัดการกับ View Model ในตอนที่สร้างขึ้นมาได้

        เมื่อสร้างคลาสทั้ง 2 ตัวขึ้นมาเสร็จแล้ว ก็ให้เตรียม Module สำหรับ View Model ไว้ให้เรียบร้อยซะ

// ViewModelModule.kt
@Module
abstract class ViewModelModule {
    @Binds
    abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
    // เดี๋ยวจะประกาศ ViewModel ไว้ในนี้ทีหลัง
}

        เพื่อให้ Custom Factory จัดการตัวเองโดยไม่ต้องไปเขียนโค้ดอะไรเพิ่ม จึงต้อง Binding ให้ Dagger 2 ซะ โดยประกาศไว้ใน Module ของ View Model นั่นแหละ

        และประกาศ Module ของ View Model ไว้ใน AppComponent ซะ

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

         จากนั้นให้สร้าง View Model ขึ้นมาตามต้องการได้เลย

// ProfileViewModel.kt
class ProfileViewModel : ViewModel() {
    ...
}

        แล้วประกาศไว้ใน Module ของ View Model ให้เรียบร้อยซะ

// ViewModelModule.kt
@Module
abstract class ViewModelModule {
    @Binds
    @IntoMap
    @ViewModelKey(ProfileViewModel::class)
    abstract fun bindProfileViewModel(viewModel: ProfileViewModel): ViewModel
}

        โดยจะเห็นว่าวิธีการเพิ่ม View Model เข้าไปใน Module จะมีลักษณะคล้ายกับ Work Manager อยู่เล็กน้อย (แต่ไม่เวิ่นเว้อเท่า)

        เพียงเท่านี้ก็สามารถใช้ Dependency Injection ใน View Model ได้แล้ว

// ProfileViewModel.kt
class ProfileViewModel @Inject constructor(private var awesomeManager: AwesomeManager) : ViewModel() {
    ...
}

สรุป

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

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