Computer >> Hướng Dẫn Máy Tính >  >> Hệ Thống >> Android

Thành thạo Dependency Injection trong Android với Dagger 2 – Hướng dẫn từng bước

Series ứng dụng Kriptofolio - Phần 4

Dependency Injection (DI) cải thiện code đáng kể. Code trở nên modular, linh hoạt và dễ test hơn. Thực ra tên gọi phức tạp hơn ý tưởng đằng sau nó.

Trong phần này, chúng ta học về DI và triển khai trong app "Kriptofolio" (trước đây "My Crypto Coins"). Sử dụng Dagger 2 — framework DI open-source phổ biến nhất cho Android. Kỹ năng quý giá cho app hiện đại, dù độ dốc học tập khá cao.

Nội dung Series

  • Giới thiệu: Lộ trình xây dựng app Android hiện đại 2018–2019
  • Phần 1: Giới thiệu nguyên lý SOLID
  • Phần 2: Bắt đầu xây dựng app Android: Mockup, UI, XML layouts
  • Phần 3: Kiến trúc app: khám phá các pattern và cách áp dụng
  • Phần 4: Triển khai Dependency Injection với Dagger 2 (bạn đang ở đây)
  • Phần 5: Xử lý RESTful Web Services với Retrofit, OkHttp, Gson, Glide và Coroutines

Dependency Injection là gì?

Để hiểu DI, trước tiên hiểu dependency trong lập trình. Dependency là khi một object phụ thuộc vào implementation cụ thể của object khác. Nhận ra dependency khi bạn khởi tạo object bên trong object khác. Ví dụ thực tế:

class MyAppClass() { private val library: MyLibrary = MyLibrary(true) ... } class MyLibrary(private val useSpecialFeature: Boolean) { ... }

Như ví dụ, class MyAppClass phụ thuộc trực tiếp vào cấu hình và implementation cụ thể của MyLibrary. Muốn dùng thư viện thứ ba? Muốn dùng cùng cấu hình library ở class khác? Phải tìm khắp code và sửa. Chỉ là vài ví dụ.

Ý tưởng: sự ràng buộc chặt (tight coupling) giữa các component làm phát triển khó khăn khi dự án lớn. Dùng DI để lỏng kết nối (loose coupling).

class MyAppClass(private val library: MyLibrary) { ... } class MyLibrary(private val useSpecialFeature: Boolean) { ... }

Đó là nó, ví dụ DI nguyên thủy. Thay vì tạo và cấu hình MyLibrary bên trong MyAppClass, bạn truyền (inject) nó qua constructor. MyAppClass hoàn toàn không chịu trách nhiệm về MyLibrary.

Dagger 2 là gì?

Dagger là framework DI hoàn toàn tĩnh, compile-time, open-source cho Java và Android. Bài này nói về phiên bản 2 do Google maintain. Square tạo phiên bản trước.

Dagger 2 được coi là framework DI hiệu quả nhất tính đến nay. So sánh Dagger 1, Dagger 2, Dagger 2.10 — mỗi implementation khác biệt, phải học lại do thay đổi lớn. Bài viết dùng Dagger 2.16, tập trung chỉ vào phiên bản này.

Như bạn hiểu về DI, class không nên tự tạo dependency mà nhận từ bên ngoài. Dagger 2 cung cấp tất cả dependency cần thiết bằng cách sinh ra nhiều boilerplate code. Code sinh ra có thể trace được và mô phỏng code viết tay. Dagger 2 viết bằng Java, code sinh ra bởi annotation processor cũng là Java.

Tuy nhiên hoạt động tốt với Kotlin không cần sửa đổi. Kotlin tương thích hoàn toàn với Java. So với framework tương tự, Dagger 2 ít dynamic hơn. Hoạt động tại compile-time thay vì runtime với reflection. Không dùng reflection. Nghĩa là khó setup và học hơn, nhưng tăng hiệu năng với an toàn compile-time.

Manual Dependency Injection không dùng công cụ

Bạn có thể nhận thấy trong source code My Crypto Coins phần trước có code inject object không dùng công cụ DI. Nó hoạt động tốt, giải pháp đủ cho app nhỏ như này. Xem 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 này làm tất cả việc. Tạo ViewModel factory cho activity/fragment cần.

/** * 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 <T : ViewModel?> create(modelClass: Class<T>): T { return MainViewModel(application, repository) as T } }

Sau đó dùng InjectorUtils ở nơi cần ViewModel factory:

/** * A placeholder fragment containing a simple view. */ class MainListFragment : Fragment() { ... private lateinit var viewModel: MainViewModel ... override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setupList() ... } ... private fun subscribeUi(activity: FragmentActivity) { // Cách cũ inject code trước khi dùng Dagger. val factory = InjectorUtils.provideMainViewModelFactory(activity.application) // Obtain ViewModel from ViewModelProviders, using parent activity as LifecycleOwner. viewModel = ViewModelProviders.of(activity, factory).get(MainViewModel::class.java) ... } }

Như bạn thấy MainListFragment không biết về CryptocurrencyRepository hay AppDatabase. Nó nhận factory đã xây dựng từ InjectorUtils. Đây là cách đơn giản. Chúng ta sẽ xóa nó và học setup Dagger 2 cho DI nâng cao. Nếu app mở rộng chức năng và code, chắc chắn sẽ thấy lợi ích framework DI chuyên nghiệp so với giải pháp thủ công.

Vậy xóa class InjectorUtils ngay và học setup Dagger 2 trong My Crypto Coins app.

Dependency Injection cho MVVM với Kotlin

Cách setup Dagger 2 với ViewModels, Activities và Fragments

Bây giờ đi qua setup Dagger 2 từng bước trên dự án My Crypto Coins app.

Đầu tiên, bật Kotlin Annotation Processing Tool (kapt). Sau đó thêm dependencies Dagger 2 đặc biệt.

Thêm các dòng sau vào file gradle:

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

Plugin Kapt cho phép compiler sinh stub class cần thiết cho tương thích Java-Kotlin. Để tiện lợi, định nghĩa phiên bản Dagger 2 cụ thể trong file gradle riêng, như làm với mọi dependency.

def versions = [:] versions.dagger = "2.16" ext.versions = versions

Kiểm tra phiên bản mới nhất tại repository chính thức Dagger 2 trên Github.

Tiếp theo, tạo class App application.

Bỏ qua nếu đã có class này. Sau khi xong, để nguyên một lúc, sẽ quay lại sau.

class App : Application() { override fun onCreate() { super.onCreate() } }

Với My Crypto Coins app, đã tạo application class trước đó.

Cập nhật manifest để bật class App.

Bỏ qua nếu đã làm trước đó.

<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" ...

My Crypto Coins app cũng đã set App class trong manifest trước đó.

Tạo package mới tên dependencyinjection.

Chứa tất cả file liên quan Dagger.

Tạo class module AppModule cung cấp dependency toàn ứng dụng.

/** * AppModule cung cấp dependency app-wide cho một phần ứng dụng. * Khởi tạo object dùng xuyên suốt app: Room database, Retrofit, Shared Preference, etc. */ @Module(includes = [ViewModelsModule::class]) class AppModule() { @Singleton // Chỉ tạo một instance trong toàn bộ vòng đời ứng dụng. @Provides // Phương thức này là constructor cho kiểu trả về Context. fun provideContext(app: App): Context = app // Prefix provide là quy ước chung nhưng không bắt buộc. @Singleton @Provides fun provideCryptocurrencyRepository(context: Context): CryptocurrencyRepository { return CryptocurrencyRepository.getInstance(AppDatabase.getInstance(context).cryptocurrencyDao()) } }

Để tạo Dagger module, annotate với @Module. Dự án thường có nhiều module. Thường có một module cung cấp dependency app-wide. AppModule khởi tạo object dùng xuyên suốt app: Room database, Retrofit, Shared Preference, etc.

Ví dụ kịch bản phổ biến: AppModule cung cấp Context object khi cần truy cập ở đâu đó trong app. Phân tích code:

Cần annotation @Provides của Dagger. Nó báo cho Dagger biết phương thức cung cấp dependency kiểu cụ thể, ở đây là Context. Khi app request inject Context, Dagger tìm ở AppModule. Tên phương thức không quan trọng, Dagger chỉ quan tâm kiểu trả về. Prefix provide là quy ước.

Annotation @Singleton không phải annotation của Dagger. Nằm trong package javax. Nó báo Dagger chỉ nên có một instance duy nhất của dependency đó. Không cần viết boilerplate kiểm tra instance đã tồn tại. Dagger xử lý logic đó nhờ annotation này. Lưu ý AppModule include ViewModelsModule. Tạo nó ngay.

Tạo class module ViewModelsModule. Module này cung cấp ViewModels toàn ứng dụng.

/** * Chịu trách nhiệm cung cấp ViewModels. */ @Module abstract class ViewModelsModule { // Lấy implementation ViewModel class này và làm cho nó có sẵn trong map injectable với MainViewModel::class làm key. @Binds @IntoMap @ViewModelKey(MainViewModel::class) // Dùng restriction trên multibound map định nghĩa bởi @ViewModelKey, nếu không cần thì dùng @ClassKey do Dagger cung cấp. 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 này dùng tính năng map multi bindings của Dagger 2. Sử dụng nó, chúng ta đóng góp object vào map injectable khắp app. Kết hợp @Binds, @IntoMap và custom annotation @ViewModelKey (sẽ tạo), tạo entry trong map với key MainViewModel::class và value instance MainViewModel. Bind factory cụ thể nhờ class ViewModelFactory chung. Cần tạo class này.

Tạo custom annotation class ViewModelKey.

/** * Annotation class báo cho Dagger biết nó dùng để xác định key trong multibound maps. */ @MustBeDocumented @Target( AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER ) @Retention(AnnotationRetention.RUNTIME) @MapKey annotation class ViewModelKey(val value: KClass<out ViewModel>) // Chỉ dùng class kế thừa từ ViewModel.

Class này dùng để bind ViewModels trong ViewModelsModule. Annotation @ViewModelKey đại diện key của map. Key chỉ có thể là class kế thừa ViewModel.

Tạo class ViewModelFactory.

/** * Factory tự động generate Class to Provider Map. * Dùng Provider<T> để tạo object injectable tại thời điểm sau. */ @Suppress("UNCHECKED_CAST") @Singleton class ViewModelFactory @Inject constructor(private val viewModelsMap: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>) : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { var creator: Provider<out ViewModel>? = 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 là utility class giúp tạo ViewModel động. Cung cấp map đã sinh ra làm argument. Phương thức create() chọn instance đúng từ map.

Tạo class module ActivityBuildersModule.

/** * Tất cả activity dự định dùng Dagger @Inject phải liệt kê ở đây. */ @Module abstract class ActivityBuildersModule { @ContributesAndroidInjector(modules = [MainListFragmetBuildersModule::class]) // Nơi áp dụng injection. abstract fun contributeMainActivity(): MainActivity @ContributesAndroidInjector abstract fun contributeAddSearchActivity(): AddSearchActivity }

Module này xây dựng tất cả activity. Sinh AndroidInjector cho tất cả Activity định nghĩa. Object inject vào activity dùng AndroidInjection.inject(this) trong onCreate của activity lifecycle. Module này cũng dùng module riêng cho fragment. Tạo module đó tiếp.

Tạo class module MainListFragmetBuildersModule.

/** * Tất cả fragment liên quan MainActivity dùng Dagger @Inject liệt kê ở đây. */ @Module abstract class MainListFragmetBuildersModule { @ContributesAndroidInjector() // Gắn fragment vào Dagger graph. abstract fun contributeMainListFragment(): MainListFragment }

Module xây dựng fragment liên quan MainActivity. Sinh AndroidInjector cho Fragment định nghĩa. Inject vào Fragment dùng AndroidSupportInjection.inject(this) trong onAttach của fragment lifecycle.

Tạo class component AppComponent.

/** * Singleton component interface cho app. Gắn tất cả module lại. * Component kết nối object với dependency của chúng. * Dagger tự sinh DaggerAppComponent dùng để khởi tạo tại Application. */ @Singleton @Component( modules = [ // AndroidSupportInjectionModule là class của Dagger, không cần tạo. // Muốn inject fragment thì dùng AndroidSupportInjectionModule.class, ngược lại dùng AndroidInjectionModule. AndroidSupportInjectionModule::class, AppModule::class, ActivityBuildersModule::class ] ) interface AppComponent { @Component.Builder // Dùng để instantiate component. interface Builder { @BindsInstance // Bind application instance vào Dagger graph. fun application(application: App): Builder fun build(): AppComponent } // Application được phép request dependency khai báo bởi modules (bằng @Inject) khai báo ở đây với từng inject() method. fun inject(app: App) }

Component rất quan trọng. Nó cho phép mọi thứ trên hoạt động cùng nhau. Kết nối object với dependency. Dagger dùng interface này sinh code cần thiết để thực hiện DI.

Tạo component class cần annotation @Component. Nhận list module làm input. Annotation @Component.Builder cho phép bind instance vào component.

Sinh graph object.

Lúc này đã có tất cả module và component setup. Sinh graph object bằng Build → Make Module trong Android Studio. Cần generation này cho các bước sau.

Tạo interface Injectable.

/** * Interface marker rỗng, báo tự động inject activity/fragment nếu implement nó. */ interface Injectable

Cần cho các bước sau. Injectable implement bởi activity/fragment muốn inject tự động.

Tạo helper class AppInjector.

/** * Helper class đơn giản tránh gọi inject method trên mỗi activity/fragment. */ object AppInjector { fun init(app: App) { // Khởi tạo Dagger. DaggerAppComponent tự sinh từ AppComponent. 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) { // Gọi inject() khiến Dagger tìm singleton trong dependency graph khớp kiểu trả về. // Nếu tìm thấy, gán reference vào field tương ứng. 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 class đơn giản tránh gọi inject trên mỗi activity/fragment.

Setup class App đã tạo trước.

class App : Application(), HasActivityInjector { @Inject // Triển khai cơ chế Dagger tìm injector factory phù hợp cho kiểu. lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Activity> override fun onCreate() { super.onCreate() // Khởi tạo để tự động inject activity/fragment nếu implement Injectable. AppInjector.init(this) ... } // Bắt buộc bởi HasActivityInjector để setup Dagger cho Activity. override fun activityInjector(): AndroidInjector<Activity> = dispatchingAndroidInjector }

Vì application có activity, implement interface HasActivityInjector. Nếu Android Studio báo lỗi DaggerAppComponent, vì chưa sinh file mới như bước trước.

Setup MainActivity inject main ViewModel factory và hỗ trợ fragment injection.

// Để hỗ trợ inject fragment thuộc activity này, implement HasSupportFragmentInjector. // Không cần nếu activity không chứa fragment hoặc fragment không cần inject. class MainActivity : AppCompatActivity(), HasSupportFragmentInjector { @Inject lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Fragment> @Inject lateinit var viewModelFactory: ViewModelProvider.Factory private lateinit var mainViewModel: MainViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Obtain ViewModel từ ViewModelProviders, dùng activity này làm LifecycleOwner. mainViewModel = ViewModelProviders.of(this, viewModelFactory).get(MainViewModel::class.java) ... } ... override fun supportFragmentInjector(): AndroidInjector<Fragment> = dispatchingAndroidInjector ... }

Vì activity có fragment con, implement HasSupportFragmentInjector. Cần thiết vì plan inject vào fragment. Activity không nên biết cách nó được inject. Dùng AndroidInjection.inject(this) trong onCreate() override.

Gọi inject() khiến Dagger 2 tìm singleton trong dependency graph khớp kiểu trả về. Tuy nhiên không cần viết code ở đây vì đã làm bởi AppInjector helper khởi tạo trong application class.

Setup MainListFragment inject main ViewModel factory.

/** * A placeholder fragment containing a simple view. */ 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) { // Obtain ViewModel từ ViewModelProviders, dùng parent activity làm LifecycleOwner. viewModel = ViewModelProviders.of(activity, viewModelFactory).get(MainViewModel::class.java) ... } }

Tương tự activity, nếu muốn fragment injectable, trong onAttach viết AndroidSupportInjection.inject(this). Nhưng việc này do AppInjector helper lo, nên bỏ qua. Chú ý cần thêm interface Injectable tạo trước để helper hoạt động.

Chúc mừng, đã triển khai Dagger 2 trong My Crypto Coins app. Bài viết là quick guide deploy Dagger 2 ngay, không đi sâu. Khuyến nghị tiếp tục nghiên cứu nếu cảm thấy bỡ ngỡ về cơ bản.

Repository

Xem source code app "Kriptofolio" (trước "My Crypto Coins") cập nhật trên GitHub.

View Source On GitHub

Ačiū! Cảm ơn đã đọc! Bài gốc đăng trên blog cá nhân www.baruckis.com ngày 7/10/2018.