ซีรีส์ Kriptofolio App - ภาค 4
Dependency Injection ปรับปรุงโค้ดให้ดีขึ้นอย่างมาก ทำให้ Modular, Flexible และ Testable มากขึ้น ชื่อดูซับซ้อนแต่มันง่ายกว่าที่คิด
ในภาคนี้เราจะเรียนรู้ Dependency Injection แล้วนำไปใช้ในแอป "Kriptofolio" (เดิมชื่อ "My Crypto Coins") ใช้ Dagger 2 Framework DI ยอดนิยม Open-source สำหรับ Android ทักษะมีค่าสำหรับสร้าง Modern Apps แม้ Learning Curve จะสูง
เนื้อหาซีรีส์
- Introduction: Roadmap สร้าง Modern Android App ปี 2018–2019
- Part 1: แนะนำ SOLID Principles
- Part 2: เริ่มสร้าง Android App: Mockups, UI, XML Layouts
- Part 3: Architecture Patterns และการนำไปใช้
- Part 4: Dependency Injection ด้วย Dagger 2 (คุณอยู่ที่นี่)
- Part 5: RESTful Web Services ด้วย Retrofit, OkHttp, Gson, Glide, Coroutines
Dependency Injection คืออะไร?
ต้องเข้าใจ Dependency ใน Programming ก่อน Dependency คือ Object หนึ่งขึ้นอยู่กับ Concrete Implementation ของ Object อีกตัว พบได้เมื่อสร้าง Object ภายใน Object อีกตัว ดูตัวอย่าง:
class MyAppClass() {
private val library: MyLibrary = MyLibrary(true)
// ...
}
class MyLibrary(private val useSpecialFeature: Boolean) {
// ...
}
MyAppClass ผูกติดกับ Concrete Implementation ของ MyLibrary ตรงๆ ถ้าอยากเปลี่ยน Library หรือใช้ Config เดียวกันใน Class อื่น ต้องหาโค้ดแก้ทุกที่ Coupling แน่นทำให้พัฒนายากขึ้นเมื่อโปรเจกต์โต
แก้ด้วย Dependency Injection แบบ Loose Coupling:
class MyAppClass(private val library: MyLibrary) {
// ...
}
class MyLibrary(private val useSpecialFeature: Boolean) {
// ...
}
แทนที่จะสร้าง MyLibrary เองใน MyAppClass ให้ "Inject" ผ่าน Constructor MyAppClass ไม่ต้องรับผิดชอบ MyLibrary แล้ว
Dagger 2 คืออะไร?
Dagger เป็น Framework DI แบบ Fully Static, Compile-time, Open-source สำหรับ Java และ Android บทความนี้พูดถึง Version 2 ที่ Google Maintain (Square สร้าง Version 1)
Dagger 2 ถือว่าเป็น DI Framework ที่มีประสิทธิภาพที่สุด Dagger 1, 2, 2.10 ต่างกันมาก ต้องเรียนใหม่ทุก Version บทความนี้ใช้ Dagger 2.16
Classes ไม่ควรสร้างหรือมี Dependencies เอง ควรได้รับจากภายนอก Dagger 2 จะ Generate Boilerplate Code ให้ Code ที่ Generate จะ Traceable ได้ และเลียนมือนโค้ดที่เขียนด้วยมือ Dagger 2 เขียนด้วย Java Code ที่ Generate ก็เป็น Java ทำงานกับ Kotlin ได้ปกติ (Interoperable)
ต่างจาก Framework อื่นๆ Dagger 2 ไม่ Dynamic ทำงาน Compile-time ไม่ใช้ Reflection ตั้งต้น Setup และเรียนรู้ยากกว่า แต่ให้ Performance Boost และ Compile-time Safety
Manual Dependency Injection โดยไม่ใช้ Tools
ใน Source Code My Crypto Coins ภาคก่อนมีโค้ด Inject Objects โดยไม่ใช้ DI Tools ทำงานได้ดี เหมาะกับแอปเล็กๆ ดูที่ Package Utilities:
/**
* Static methods used to inject classes needed for various Activities and Fragments.
*/
object InjectorUtils {
private fun getCryptocurrencyRepository(context: Context): CryptocurrencyRepository {
return CryptocurrencyRepository.getInstance(
AppDatabase.getInstance(context).cryptocurrencyDao())
}
fun provideMainViewModelFactory(application: Application): MainViewModelFactory {
val repository = getCryptocurrencyRepository(application)
return MainViewModelFactory(application, repository)
}
fun provideAddSearchViewModelFactory(context: Context): AddSearchViewModelFactory {
val repository = getCryptocurrencyRepository(context)
return AddSearchViewModelFactory(repository)
}
}
Class นี้ทำงานทั้งหมด สร้าง ViewModel Factories ให้ Activities/Fragments ที่ต้องการ
/**
* Factory for creating a [MainViewModel] with a constructor that takes a
* [CryptocurrencyRepository].
*/
class MainViewModelFactory(
private val application: Application,
private val repository: CryptocurrencyRepository
) : ViewModelProvider.NewInstanceFactory() {
@Suppress("UNCHECKED_CAST")
override fun create(modelClass: Class): T {
return MainViewModel(application, repository) as T
}
}
ใช้ InjectorUtils ที่ต้องการ ViewModel Factory:
class MainListFragment : Fragment() {
// ...
private lateinit var viewModel: MainViewModel
// ...
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupList()
// ...
}
// ...
private fun subscribeUi(activity: FragmentActivity) {
// วิธีเก่า Inject ก่อนใช้ Dagger
val factory = InjectorUtils.provideMainViewModelFactory(activity.application)
viewModel = ViewModelProviders.of(activity, factory).get(MainViewModel::class.java)
// ...
}
}
MainListFragment ไม่รู้จัก CryptocurrencyRepository หรือ AppDatabase ได้ Factory ที่สร้างเสร็จจาก InjectorUtils วิธีนี้ Simple หนึ่งทาง แต่ถ้าแอปขยายใหญ่ จะเห็นประโยชน์ของ DI Framework มืออาชีพเร็วมาก
ลบ InjectorUtils ทิ้ง และเรียน Setup Dagger 2 ใน My Crypto Coins
Dependency Injection สำหรับ MVVM ด้วย Kotlin
Setup Dagger 2 กับ ViewModels, Activities, Fragments
ผ่านขั้นตอน Setup Dagger 2 ทีละขั้นใน My Crypto Coins App
1. เปิดใช้ Kotlin KAPT และเพิ่ม Dagger 2 Dependencies
เพิ่มใน Gradle (Module: app):
apply plugin: 'kotlin-kapt' // For annotation processing
...
implementation "com.google.dagger:dagger:$versions.dagger"
implementation "com.google.dagger:dagger-android:$versions.dagger"
implementation "com.google.dagger:dagger-android-support:$versions.dagger"
kapt "com.google.dagger:dagger-compiler:$versions.dagger"
kapt "com.google.dagger:dagger-android-processor:$versions.dagger"
KAPT Plugin เปิดให้ Compiler Generate Stub Classes สำหรับ Interoperability Java-Kotlin กำหนด Dagger Version ในไฟล์ Gradle แยก:
def versions = [:]
versions.dagger = "2.16"
ext.versions = versions
เช็ค Version ล่าสุดที่ Dagger 2 GitHub Releases
2. สร้าง Application Class App
ข้ามถ้ามีอยู่แล้ว
class App : Application() {
override fun onCreate() {
super.onCreate()
}
}
3. Update Manifest ให้ใช้ App Class
ข้ามถ้าทำแล้ว
<manifest xmlns:android="https://schemas.android.com/apk/res/android"
package="com.baruckis.mycryptocoins">
<application
android:name=".App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
...>
4. สร้าง Package dependencyinjection
เก็บไฟล์ Dagger ทั้งหมดไว้ที่นี่
5. สร้าง AppModule Class Module - Provide Dependencies ทั่วแอป
/**
* AppModule จะ Provide App-wide Dependencies สำหรับส่วนของแอปพลิเคชัน
* Initialize Objects ที่ใช้ทั่วแอป เช่น Room Database, Retrofit, Shared Preference ฯลฯ
*/
@Module(includes = [ViewModelsModule::class])
class AppModule() {
@Singleton
@Provides
fun provideContext(app: App): Context = app
@Singleton
@Provides
fun provideCryptocurrencyRepository(context: Context): CryptocurrencyRepository {
return CryptocurrencyRepository.getInstance(AppDatabase.getInstance(context).cryptocurrencyDao())
}
}
สร้าง Dagger Module ต้อง Annotate @Module โปรเจกต์มักมีหลาย Modules ปกติหนึ่งในนั้น Provide App-wide Dependencies AppModule Initialize Objects ทั่วแอป
ตัวอย่าง: Provide Context Object ใช้ @Provides บอก Dagger ว่า Method นี้ Provide Dependency ประเภท Context ไม่สำคัญชื่อ Method Dagger ดู Return Type แต่อนุสาวนิยมใช้ Prefix "provide"
@Singleton (จาก javax package) บอก Dagger ให้สร้าง Instance เดียวตลอด Lifecycle Dagger จะ Handle Boilerplate ให้
สังเกต AppModule Include ViewModelsModule มาสร้างต่อ
6. สร้าง ViewModelsModule - Provide ViewModels ทั่วแอป
/**
* จะรับผิดชอบ Provide ViewModels
*/
@Module
abstract class ViewModelsModule {
// นำ MainViewModel Implementation ใส่ Injectable Map
// Key คือ MainViewModel::class
@Binds
@IntoMap
@ViewModelKey(MainViewModel::class)
abstract fun bindMainViewModel(mainViewModel: MainViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(AddSearchViewModel::class)
abstract fun bindAddSearchViewModel(addSearchViewModel: AddSearchViewModel): ViewModel
@Binds
abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
}
Module นี้ใช้ Dagger 2 Map Multi-bindings Contribute Objects เข้า Map ที่ Inject ได้ ทุกที่ ใช้ @Binds, @IntoMap, @ViewModelKey (Custom Annotation จะสร้าง) สร้าง Entry ใน Map Key = MainViewModel::class Value = MainViewModel Instance Bind Factory ผ่าน ViewModelFactory Class (จะสร้างต่อไป)
7. สร้าง Custom Annotation ViewModelKey
/**
* Annotation Class บอก Dagger ว่าใช้เป็น Key ใน Multi-bound Maps ได้
*/
@MustBeDocumented
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass)
// Key ต้องเป็น Class ที่繼承 ViewModel เท่านั้น
ใช้ Binding ViewModels ใน ViewModelsModule Key ของ Map คือ @ViewModelKey Key ต้องเป็น Class ที่ Extends ViewModel
8. สร้าง ViewModelFactory Class
/**
* Factory สร้าง Class to Provider Map อัตโนมัติ
* ใช้ Provider<T> สร้าง Injectable Object ในภายหลัง
*/
@Suppress("UNCHECKED_CAST")
@Singleton
class ViewModelFactory @Inject constructor(
private val viewModelsMap: Map, @JvmSuppressWildcards Provider>
) : ViewModelProvider.Factory {
override fun create(modelClass: Class): T {
var creator: Provider? = viewModelsMap[modelClass]
if (creator == null) {
for (entry in viewModelsMap.entries) {
if (modelClass.isAssignableFrom(entry.key)) {
creator = entry.value
break
}
}
}
if (creator == null) {
throw IllegalArgumentException("Unknown model class $modelClass")
}
try {
return creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
ViewModelFactory Utility Class ช่วยสร้าง ViewModels แบบ Dynamic ใส่ Generated Map เป็น Argument เมธอด create() จะ Pick Instance ที่ถูกต้องจาก Map
9. สร้าง ActivityBuildersModule Class Module
/**
* Activities ที่ต้องการใช้ Dagger @Inject ต้องรายชื่อที่นี่
*/
@Module
abstract class ActivityBuildersModule {
@ContributesAndroidInjector(modules = [MainListFragmentBuildersModule::class])
abstract fun contributeMainActivity(): MainActivity
@ContributesAndroidInjector
abstract fun contributeAddSearchActivity(): AddSearchActivity
}
Module นี้ Construct Activities ทั้งหมด Generate AndroidInjector สำหรับ Activities ที่กำหนด Inject Objects เข้า Activities ด้วย AndroidInjection.inject(this) ใน onCreate ใช้ Module อีกตัวสำหรับ Fragments (จะสร้างต่อ)
10. สร้าง MainListFragmentBuildersModule Class Module
/**
* Fragments ที่เกี่ยวกับ MainActivity และต้องการ Dagger @Inject รายชื่อที่นี่
*/
@Module
abstract class MainListFragmentBuildersModule {
@ContributesAndroidInjector()
abstract fun contributeMainListFragment(): MainListFragment
}
Build Fragments ของ MainActivity Generate AndroidInjector สำหรับ Fragments Inject เข้า Fragments ด้วย AndroidSupportInjection.inject(this) ใน onAttach
11. สร้าง AppComponent Class Component
/**
* Singleton Component Interface สำหรับ App รวม Modules ทั้งหมด
* Component ใช้เชื่อม Objects กับ Dependencies
* Dagger จะ Auto-generate DaggerAppComponent ใช้ Initialize ที่ Application
*/
@Singleton
@Component(
modules = [
AndroidSupportInjectionModule::class,
AppModule::class,
ActivityBuildersModule::class
]
)
interface AppComponent {
@Component.Builder
interface Builder {
@BindsInstance
fun application(application: App): Builder
fun build(): AppComponent
}
fun inject(app: App)
}
Component สำคัญมาก ทำให้ทุกอย่างทำงานร่วมกัน เชื่อม Objects กับ Dependencies Dagger ใช้ Interface นี้ Generate Code สำหรับ DI @Component รับ List of Modules @Component.Builder ให้ Bind Instance เข้า Component
12. Generate Graph Object
ตอนนี้มี Modules และ Component ครบแล้ว Generate Graph Object: Build → Make Module ใน Android Studio จำเป็นสำหรับขั้นตอนต่อไป
13. สร้าง Injectable Interface
/**
* Marker Interface บอกว่า Auto-inject Activities/Fragments ที่ Implement มัน
*/
interface Injectable
ใช้ในขั้นตอนต่อไป Activities/Fragments ที่ต้องการ Auto-inject ต้อง Implement Interface นี้
14. สร้าง Helper Class AppInjector
/**
* Helper Class หลีกเลี่ยงการเรียก Inject Method ทุก Activity/Fragment
*/
object AppInjector {
fun init(app: App) {
// Initialize Dagger
DaggerAppComponent.builder().application(app).build().inject(app)
app.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
override fun onActivityPaused(activity: Activity) { }
override fun onActivityResumed(activity: Activity) { }
override fun onActivityStarted(activity: Activity) { }
override fun onActivityDestroyed(activity: Activity) { }
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle?) { }
override fun onActivityStopped(activity: Activity) { }
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
handleActivity(activity)
}
})
}
private fun handleActivity(activity: Activity) {
if (activity is HasSupportFragmentInjector || activity is Injectable) {
AndroidInjection.inject(activity)
}
if (activity is FragmentActivity) {
activity.supportFragmentManager.registerFragmentLifecycleCallbacks(
object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentCreated(fragmentManager: FragmentManager, fragment: Fragment, savedInstanceState: Bundle?) {
if (fragment is Injectable) {
AndroidSupportInjection.inject(fragment)
}
}
}, true)
}
}
}
Helper เรียบง่าย หลีกเลี่ยงเรียก Inject ทุก Activity/Fragment
15. Setup App Class ที่สร้างไว้ก่อนหน้า
class App : Application(), HasActivityInjector {
@Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector
override fun onCreate() {
super.onCreate()
AppInjector.init(this)
// ...
}
override fun activityInjector(): AndroidInjector = dispatchingAndroidInjector
}
Application มี Activities ต้อง Implement HasActivityInjector ถ้า Android Studio Error บน DaggerAppComponent เพราะยังไม่ Generate File (ขั้นตอน 12)
16. Setup MainActivity Inject ViewModel Factory และรองรับ Fragment Injection
// รองรับ Inject Fragments ต้อง Implement HasSupportFragmentInjector
// ไม่ต้อง Implement ถ้า Activity ไม่มี Fragments หรือ Fragments ไม่ต้อง Inject
class MainActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var mainViewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainViewModel = ViewModelProviders.of(this, viewModelFactory).get(MainViewModel::class.java)
// ...
}
override fun supportFragmentInjector(): AndroidInjector = dispatchingAndroidInjector
// ...
}
Activities มี Child Fragments ต้อง Implement HasSupportFragmentInjector ต้องการ Inject เข้า Fragments ใช้ AndroidInjection.inject(this) ใน onCreate() แต่ทำโดย AppInjector แล้ว ข้ามได้
17. Setup MainListFragment Inject ViewModel Factory
class MainListFragment : Fragment(), Injectable {
// ...
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var viewModel: MainViewModel
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// ...
subscribeUi(activity!!)
}
private fun subscribeUi(activity: FragmentActivity) {
viewModel = ViewModelProviders.of(activity, viewModelFactory).get(MainViewModel::class.java)
// ...
}
}
เหมือน Activities Fragment ต้องการ Inject ให้เขียน AndroidSupportInjection.inject(this) ใน onAttach แต่ AppInjector ทำแล้ว เพียง Implement Injectable Interface ที่สร้างไว้
ยินดีด้วย! Implement Dagger 2 ใน My Crypto Coins เรียบร้อย บทความนี้ Quick Guide Deploy Dagger 2 ทันที ไม่ใช่ Deep Coverage แนะนำศึกษาต่อหากสับสนพื้นฐาน
Repository
ดู Source Code แอป "Kriptofolio" (เดิม "My Crypto Coins") ที่อัปเดตแล้วบน GitHub
View Source On GitHub
Ačiū! ขอบคุณที่อ่าน! ประกาศบน Blog ส่วนตัว www.baruckis.com วันที่ 7 ตุลาคม 2018