Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> ระบบ >> Android

เชี่ยวชาญ Dependency Injection ใน Android ด้วย Dagger 2 - คู่มือฉบับสมบูรณ์

ซีรีส์ 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"
Module: app

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