Dagger 2 in Android [Part 4] — ทำ Dependency Injection ให้กับ Android Framework Component ต่างๆ
หลังจากที่ได้อ่านบทความก่อนหน้านี้ไปแล้วก็จะสามารถทำ 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
- ViewModel
อยากจะดูวิธีของ 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 จะต้องประกาศ HasAndroidInjector
เพื่อให้ Dagger 2 รู้ว่าจะต้องทำ Dependency Injection ให้กับ Service ด้วย รวมไปถึงการสร้าง DispatchingAndroidInjector
ให้กับ Service
// AwesomeApplication.kt
class AwesomeApplication : Application(), HasAndroidInjector {
@Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any>
override fun androidInjector(): AndroidInjector<Any> = androidInjector
...
}
จากนั้นก็สร้าง 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 {
...
}
และอย่าลืมประกาศ HasAndroidInjector
และสร้าง DispatchingAndroidInjector
สำหรับ Content Provider ในคลาส Application ด้วยล่ะ
// AwesomeApplcation.kt
class AwesomeApplication : Application(), HasAndroidInjector {
@Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any>
override fun androidInjector(): AndroidInjector<Any> = androidInjector
...
}
เมื่อพร้อมแล้วก็ให้สร้าง Content Provider ขึ้นมาได้เลย โดยเรียกคลาส AndroidInjector
ใน onCreate()
ซะ
// AwesomeContentProvider.kt
class AwesomeContentProvider : ContentProvider() {
override fun onCreate(): Boolean {
AndroidInjection.inject(this)
...
}
...
}
จากนั้นก็ให้ประกาศ Content Provider ที่สร้างขึ้นมาไว้ใน Module ของ Content Provider ด้วย
// ContentProviderModule.kt
@Module
abstract class ContentProviderModule {
@ContributesAndroidInjector()
abstract fun contributeAwesomeContentProvider(): AwesomeContentProvider
}
เสร็จแล้ว อยากจะ 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
...
}
ViewModel
ViewModel ก็เป็นอีกหนึ่ง Component ใหม่ที่เพิ่มเข้ามาใน Android Architecture Components ที่จะช่วยให้นักพัฒนาแอนดรอยด์สามารถจัดการกับโปรเจคด้วย MVVM ได้ง่ายขึ้น แต่ถ้าอยากจะใช้งานร่วมกับ Dagger 2 ก็ต้องเขียนบางส่วนเพิ่มเองเช่นกัน เพราะว่า Dagger 2 ยังไม่ได้รองรับกับ ViewModel โดยตรง
โดยจะมีคลาสที่ต้องเตรียมไว้สำหรับ ViewModel ดังนี้
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)
}
}
}
จะเห็นว่าการทำให้ ViewModel รองรับ Dagger 2 นั้นจะมีการสร้าง Custom Factory ขึ้นมาเอง เพื่อให้ Dagger 2 สามารถจัดการกับ ViewModel ในตอนที่สร้างขึ้นมาได้
เมื่อสร้างคลาสทั้ง 2 ตัวขึ้นมาเสร็จแล้ว ก็ให้เตรียม Module สำหรับ ViewModel ไว้ให้เรียบร้อยซะ
// ViewModelModule.kt
@Module
abstract class ViewModelModule {
@Binds
abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
// เดี๋ยวจะประกาศ ViewModel ไว้ในนี้ทีหลัง
}
เพื่อให้ Custom Factory จัดการตัวเองโดยไม่ต้องไปเขียนโค้ดอะไรเพิ่ม จึงต้อง Binding ให้ Dagger 2 ซะ โดยประกาศไว้ใน Module ของ ViewModel นั่นแหละ
และประกาศ Module ของ ViewModel ไว้ใน AppComponent
ซะ
// AppComponent.kt
@Singleton
@Component(
modules = [
...,
ViewModelModule::class
])
interface AppComponent {
...
}
จากนั้นให้สร้าง ViewModel ขึ้นมาตามต้องการได้เลย
// ProfileViewModel.kt
class ProfileViewModel : ViewModel() {
...
}
แล้วประกาศไว้ใน Module ของ ViewModel ให้เรียบร้อยซะ
// ViewModelModule.kt
@Module
abstract class ViewModelModule {
@Binds
@IntoMap
@ViewModelKey(ProfileViewModel::class)
abstract fun bindProfileViewModel(viewModel: ProfileViewModel): ViewModel
}
โดยจะเห็นว่าวิธีการเพิ่ม ViewModel เข้าไปใน Module จะมีลักษณะคล้ายกับ Work Manager อยู่เล็กน้อย (แต่ไม่เวิ่นเว้อเท่า)
เพียงเท่านี้ก็สามารถใช้ Dependency Injection ใน ViewModel ได้แล้ว
// ProfileViewModel.kt
class ProfileViewModel @Inject constructor(private var awesomeManager: AwesomeManager) : ViewModel() {
...
}
สรุป
จะเห็นว่า Dagger 2 นั้นรองรับ Component พื้นฐานของแอนดรอยด์อยู่แล้ว จึงสามารถทำ Dependency Injection ใน Component เหล่านั้นได้ทันที แต่ก็จะมี Component บางตัวที่เพิ่มเข้ามาใหม่ที่ยังไม่ได้รองรับโดยตรง จึงต้องมีการเขียนโค้ดเพิ่มเข้าไปนิดหน่อยเพื่อให้สามารถใช้งานร่วมกับ Dagger 2 ได้
แต่ถึงจะเขียนเพิ่มเยอะแค่ไหน สุดท้ายตอนที่เรียกใช้งานจริงๆก็เหลือแค่นิดเดียว ลดเวลาเขียนโค้ดซ้ำซากไปได้ตั้งเยอะเลยนะ