آموزش Dagger Hilt – راهنمای گام به گام
هنگام کار بر روی یک پروژه اندروید، ما نیاز به ترکیب و ادغام بسیاری از وابستگیهای مختلف داریم . برای مدیریت این وابستگیها از یک فریم ورک تزریق وابستگی مانند Dagger استفاده میکنیم.
اما برای راه اندازی و کار با Dagger نیاز به مقدار زیادی کد boilerpalte (نوشتن کدهایی ک باعث میشه حجم کد بره بالا) و منحنی یادگیری بسیار شیب دار داریم . سپس Dagger-Android آمد که کد boilerpalte را کاهش دهد، اما موفق نشد.
در حال حاضر، اکنون راه پیشنهادی گوگل ، انتشار Dagger-Hilt به عنوان بخشی از کتابخانه های Jetpack برای استفاده از آن است. در Dagger-Hilt :
- کد dagger برای توسعه دهندگان آسان و ساده شود.
- ارائه مجموعه ای متفاوت از اتصالات) binding (برای انواع build type ها.
- مراقبت از محل تزریق وابستگیها و باقی ماندن همه code generation توسط خود dagger با استفاده از annotation ها و در نتیجه حذف همه کدهای boilerpalte اتفاق میافتد.
در این آموزش می آموزیم:
- درک dagger
- راه اندازی یک پروژه جدید
- ساختار پروژه
- ادغام Dagger-Hilt
- WorkManager با Dagger-Hilt
- Qualifier ها
شناخت Dagger
قبل از شروع با Dagger-Hilt باید اصول اولیه Dagger را بدانیم. در این مقاله به شما کمک می کنیم تا Dagger و اصطلاحات آن را بشناسید.
اساساً برای شناخت Dagger باید چهار annotation اصلی را بشناسیم.
- Module
- Component
- Provides
- Inject
برای درک بهتر آن ، Module را به عنوان یک فراهم کننده(provider) وابستگی در نظر بگیرید و یک activity یا هر class دیگری را به عنوان مصرف کننده(consumer) در نظر بگیرید. اکنون برای ایجاد وابستگی از provider به consumer ، پلی بین آنها داریم، در Dagger، Component به عنوان آن پل خاص کار میکند.
در چنین حالتی، یک Module یک کلاس است و ما آن را با @Module برای Dagger حاشیه نویسی می کنیم تا آن را به عنوان Module (واحد مستقل) درک کنیم.
یک component یک interface است که با @Component حاشیه نویسی می شود و Module ها را در آن جا می گیرد. (اما اکنون، این annotation در Dagger-Hilt مورد نیاز نیست)
Provide ها یک annotation در کلاس Module برای فراهم کردن وابستگی هستند و
Inject یک annotation برای تعریف وابستگی در داخل مصرف کننده(consumer) استفاده می شود.
به شدت توصیه می شود قبل از مهاجرت به سمت Dagger-Hilt در مورد Dagger خام(raw)، بدانید.
راه اندازی یک پروژه جدید
در اینجا، ما قصد داریم پروژه اندرویدی را راه اندازی کنیم.
یک پروژه ایجاد کنید
- یک پروژه Android Studio جدید را شروع کنید
- Empty Activityو سپس Next را انتخاب کنید
- نام: Dagger-Hilt-Tutorial
- نام بسته: mindorks.framework.mvvm
- زبان: کاتلین
- پایان
پروژه شروع شما اکنون آماده است
وابستگی های زیر را در فایل build.gradle برنامه اضافه کنید،
implementation 'android.arch.lifecycle:extensions:{latest-version}'
implementation 'com.github.bumptech.glide:glide:{latest-version}'
implementation 'androidx.activity:activity-ktx:{latest-version}'
اکنون پروژه ما با وابستگی ها آماده است.
ساختار پروژه
برای پروژه، ما میخواهیم یک نسخه اولیه از MVVM را دنبال کنیم. بسته ما در پروژه به شکل زیر خواهد بود:
برای نشان دادن وضعیت UI به enum نیاز داریم که ما آن را در پکیج utils ایجاد خواهیم کرد.
package com.mindorks.framework.mvvm.utils
enum class Status {
SUCCESS,
ERROR,
LOADING
}
ما به یک کلاس کاربردی نیاز داریم که مسئول برقراری ارتباط وضعیت فعلی تماس شبکه با لایه UI باشد. ما آن را به عنوان Resource نام گذاری می کنیم. بنابراین، یک data class Resource کاتلین را در داخل همان پکیج utils ایجاد کنید و کد زیر را اضافه کنید.
package com.mindorks.framework.mvvm.utils
data class Resource<out T>(val status: Status, val data: T?, val message: String?) {
companion object {
fun <T> success(data: T?): Resource<T> {
return Resource(Status.SUCCESS, data, null)
}
fun <T> error(msg: String, data: T?): Resource<T> {
return Resource(Status.ERROR, data, msg)
}
fun <T> loading(data: T?): Resource<T> {
return Resource(Status.LOADING, data, null)
}
}
}
پکیج Utils ما اکنون آماده است.
ادغام و ترکیب Dagger-Hilt
برای راه اندازی Dagger در پروژه، موارد زیر را در فایل build.gradle برنامه اضافه می کنیم.
implementation 'com.google.dagger:hilt-android:{latest-version}'
kapt 'com.google.dagger:hilt-android-compiler:{latest-version}'
سپس در گام بعدی، پلاگین dagger.hilt را در بالای build.gradle برنامه به صورت زیر اعمال می کنیم.
plugins {
……..
id 'dagger.hilt.android.plugin'
}
و در نهایت موارد زیر را درclasspath فایلbuild.gradle پروژه اضافه می کنیم مانند
classpath “com.google.dagger:hilt-android-gradle-plugin:{latest-version}”
این موارد ، تنظیمات لازم برای شروع استفاده از Dagger-Hilt در پروژه است.
ایجاد و راه اندازی پروژه dagger-hilt را به صورت مرحله به مرحله شرح می دهیم.
1- ما ابتدا کلاس Application برنامه خود را به صورت زیر آپدیت می کنیم:
class App : Application()
و در فایل Manifest ، قسمت Application را به صورت زیر به روز رسانی میکنیم:
android:name=".App"
اکنون، برای شروع کار با Dagger، باید کلاس Application را با @HiltAndroidApp حاشیه نویسی کنیم. کد به روز شده به صورت زیر است:
@HiltAndroidApp
class App : Application()
اگر قصد دارید از Dagger-Hilt در برنامه خود استفاده کنید، مرحله ذکر شده در بالا یک مرحله الزامی است. این انوتیشن، تمام کلاس های component را تولید (generate) می کند که باید به صورت manual در حین استفاده از Dagger انجام دهیم.
2- اکنون، وابستگیهای Retrofit و Kotlin-Coroutines را در build.gradle برنامه اضافه میکنیم، مانند:
// Networking
implementation "com.squareup.retrofit2:retrofit:{latest-version}"
implementation "com.squareup.retrofit2:converter-moshi:{latest-version}"
implementation "com.squareup.okhttp3:okhttp:{latest-version}"
implementation "com.squareup.okhttp3:logging-interceptor:{latest-version}"
// Coroutine
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:{latest-version}"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:{latest-version}"
اکنون، در پروژه کاری که انجام خواهیم داد این است که یک فراخوانی API انجام می دهیم و لیستی از کاربران را نشان می دهیم. ما همچنین از Kotlin-Coroutine برای multithreading استفاده خواهیم کرد.
اکنون، پکیج های api، model، repository را در پکیج data ایجاد میکنیم. دارای فایل هایی مانند
ApiService به صورت زیر است.
interface ApiService {
@GET("users")
suspend fun getUsers(): Response<List<User>>
}
همچنین ApiHelper به صورت زیر است
interface ApiHelper {
suspend fun getUsers(): Response<List<User>>
}
و در نهایت، در ApiHelperImpl، ApiService را در سازنده با استفاده از @Inject تزریق کرده و ApiHelper را پیاده سازی می کنیم.
در اینجا، @Inject به انتقال وابستگی مورد نیاز توسط ApiHelperImpl در خود سازنده کمک می کند.
data class User به صورت زیر است.
data class User(
@Json(name = "id")
val id: Int = 0,
@Json(name = "name")
val name: String = "",
@Json(name = "email")
val email: String = "",
@Json(name = "avatar")
val avatar: String = ""
)
و در نهایت در MainRepository ، ApiHelper را در سازنده repository پاس می دهیم. MainRepository به صورت زیر است.
class MainRepository @Inject constructor(private val apiHelper: ApiHelper) {
suspend fun getUsers() = apiHelper.getUsers()
}
اکنون، می بینید که ApiHelper و ApiService را به ترتیب در MainRepository و ApiHelperImpl پاس داده ایم. بنابراین، برای تزریق (inject) همه چیز در سازنده، باید آن را با استفاده از انوتیشن @Provide در Dagger فراهم کنیم.
3- اکنون یک پکیج di -> module ایجاد می کنیم و در داخل آن، ApplicationModule را ایجاد می کنیم. همانطور که می بینید، ما ApplicationComponent را ایجاد نمی کنیم، زیرا از کامپوننت ارائه شده توسط خود Dagger-Hilt استفاده می کنیم . ما یک کلاس ApplicationModule ایجاد می کنیم و آن را با @Module حاشیه نویسی می کنیم. استفاده از این انوتیشن باعث می شود که Dagger بفهمد که این کلاس، یک ماژول(module) است.
@Module
class ApplicationModule { }
اکنون باید این کلاس ماژول را در یک کامپوننت مشخص وصل کنیم. بنابراین آن را در ApplicationComponent ، راه اندازی خواهیم کرد، مانند:
@Module
@InstallIn(ApplicationComponent::class)
class ApplicationModule {}
در اینجا، می بینید که ما از انوتیشن @InstallIn برای راه اندازی آن در ApplicationComponent استفاده کرده ایم ApplicationComponent . توسط Dagger-Hilt ارائه شده است.
این بدان معنی است که وابستگی های ارائه شده در اینجا در سراسر برنامه استفاده می شود. بیایید در نظر بگیریم که می خواهیم در سطح آن activity که ماژول را در آن نصب می کنیم استفاده کنیم،
@InstallIn(ActivityComponent::class)
به طور مشابه مانند ApplicationComponent/ActivityComponent، ما انواع مختلفی از کامپوننت ها را داریم مانند:
FragmentComponent برای Fragmentها، ServiceComponent برای Service و غیره.
4- اکنون، در داخل ApplicationModule، تمام وابستگی ها را یکی یکی ارائه می کنیم و کد به روز شده کلاس ApplicationModule به صورت زیر است:
@Module
@InstallIn(ApplicationComponent::class)
class ApplicationModule {
@Provides
fun provideBaseUrl() = BuildConfig.BASE_URL
@Provides
@Singleton
fun provideOkHttpClient() = if (BuildConfig.DEBUG) {
val loggingInterceptor = HttpLoggingInterceptor()
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()
} else OkHttpClient
.Builder()
.build()
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient, BASE_URL: String): Retrofit =
Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create())
.baseUrl(BASE_URL)
.client(okHttpClient)
.build()
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit) = retrofit.create(ApiService::class.java)
@Provides
@Singleton
fun provideApiHelper(apiHelper: ApiHelperImpl): ApiHelper = apiHelper
}
در اینجا، ما وابستگی هایی را با استفاده از انوتیشن @Provide ارائه کرده ایم که در سراسر برنامه قابل دسترسی است.
انوتیشن @Singleton کمک می کند تا instance (نمونه) ، یک بار در برنامه ایجاد و استفاده شود.
به طور مشابه، مانند Singleton که تا lifecycle برنامه باقی میماند، ما @ActivityScoped، @FragmentScoped و غیره را نیز داریم که در آنها وابستگیها تا چرخه حیات Activity و Fragment گسترده میشوند.
حالا اگر یادتان باشد در مرحله آخر، ApiHelper و ApiService را به ترتیب در MainRepository و ApiHelperImpl پاس دادیم و برای inject موفقیت آمیز آن ، باید این دو dependency را فراهم کنیم.
در ApplicationModule ، دو تابع آخر یعنی provideApiService و provideApiHelper نمونه ApiService و ApiHelper را فراهم (provide) می کنند.
همچنین، برای BASE_URL، موارد زیر را در بلوک defaultConfig در فایل build.gradle برنامه اضافه می کنیم.
buildConfigField 'String', 'BASE_URL', "\"https://5e510330f2c0d300147c034c.mockapi.io/\""
5- اکنون، از آنجایی که همه چیز راه اندازی شده است، اکنون باید از آنها در کلاس های اندروید استفاده کنیم/ تزریق کنیم. در ادامه ، ما به activity خود برای شروع استفاده از آنها نیاز داریم.
بنابراین برای ساختن هر کلاس اندرویدی که توسط Dagger-Hilt پشتیبانی میشود، از انوتیشن زیر استفاده می کنیم.
@AndroidEntryPoint
بنابراین، در کد خود، یک پکیج ui دیگر ایجاد می کنیم و در داخل آن یک پکیج فرعی دیگر به نام main ایجاد می کنیم که دارای MainActivity ، MainViewModel و MainAdapter برای نمایش لیست کاربران است.
اکنون، انوتیشن AndroidEntryPoint را در MainActivity اضافه می کنیم، مانند:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {}
در اینجا، @AndroidEntryPoint به این معنی است که Dagger-Hilt اکنون میتواند وابستگیها را در این کلاس تزریق کند.
از انوتیشن @AndroidEntryPoint می توان در موارد زیر استفاده کرد
- Activity
- Fragment
- View
- Service
- BroadcastReceiver
Hilt در حال حاضر فقط از Activityهایی پشتیبانی می کند که ComponentActivity و fragment هایی که از androidx library Fragment، extend می کنند، پشتیبانی می کند.
6- MainActivity به این صورت خواهد بود:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val mainViewModel : MainViewModel by viewModels()
private lateinit var adapter: MainAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupUI()
setupObserver()
}
private fun setupUI() {
recyclerView.layoutManager = LinearLayoutManager(this)
adapter = MainAdapter(arrayListOf())
recyclerView.addItemDecoration(
DividerItemDecoration(
recyclerView.context,
(recyclerView.layoutManager as LinearLayoutManager).orientation
)
)
recyclerView.adapter = adapter
}
private fun setupObserver() {
mainViewModel.users.observe(this, Observer {
when (it.status) {
Status.SUCCESS -> {
progressBar.visibility = View.GONE
it.data?.let { users -> renderList(users) }
recyclerView.visibility = View.VISIBLE
}
Status.LOADING -> {
progressBar.visibility = View.VISIBLE
recyclerView.visibility = View.GONE
}
Status.ERROR -> {
progressBar.visibility = View.GONE
Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
}
}
})
}
private fun renderList(users: List<User>) {
adapter.addData(users)
adapter.notifyDataSetChanged()
}
}
و کلاس MainAdapter به صورت زیر است:
class MainAdapter(
private val users: ArrayList<User>
) : RecyclerView.Adapter<MainAdapter.DataViewHolder>() {
class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(user: User) {
itemView.textViewUserName.text = user.name
itemView.textViewUserEmail.text = user.email
Glide.with(itemView.imageViewAvatar.context)
.load(user.avatar)
.into(itemView.imageViewAvatar)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
DataViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_layout, parent,
false
)
)
override fun getItemCount(): Int = users.size
override fun onBindViewHolder(holder: DataViewHolder, position: Int) =
holder.bind(users[position])
fun addData(list: List<User>) {
users.addAll(list)
}
}
در اینجا، می توانید مشاهده کنید که MainViewModel برای مدیریت تغییرات داده ها استفاده می شود.
7- در اینجا، ما می خواهیم موارد زیر را در سازنده ViewModel پاس بدهیم.
private val mainRepository: MainRepositoryprivate
val networkHelper: NetworkHelper
برای pass دادن در اینجا, ما باید ابتدا یک NetworkHelper شبیه به کد زیر بسازیم.
@Singleton
class NetworkHelper @Inject constructor(@ApplicationContext private val context: Context) {
fun isNetworkConnected(): Boolean {
var result = false
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val networkCapabilities = connectivityManager.activeNetwork ?: return false
val activeNetwork =
connectivityManager.getNetworkCapabilities(networkCapabilities) ?: return false
result = when {
activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
else -> false
}
} else {
connectivityManager.run {
connectivityManager.activeNetworkInfo?.run {
result = when (type) {
ConnectivityManager.TYPE_WIFI -> true
ConnectivityManager.TYPE_MOBILE -> true
ConnectivityManager.TYPE_ETHERNET -> true
else -> false
}
}
}
}
return result
}
}
در اینجا، می بینید که ما در حال پاس دادن context در سازنده NetworkHelper هستیم. ما همچنین در اینجا context را با @ApplicationContext حاشیه نویسی می کنیم، به این معنی که context ای که می خواهیم استفاده کنیم، context application خواهد بود.
توجه: اگر بخواهیم context اکتیویتی را اعمال کنیم، میتوانیم از @ActivityContext استفاده کنیم که باید در ماژول فراهم (provide) شود.
8- اکنون، از آنجایی که باید NetworkHelper و MainRepository را در MainViewModel پاس بدهیم ، ViewModelها مستقیماً توسط Dagger-Hilt پشتیبانی نمی شوند و برای کار با Dagger-Hilt در ViewModel از Jetpack Extension استفاده می کنیم.
ابتدا باید dependency ها را در gradle برای Jetpack Extension تنظیم کنیم.
بیایید موارد زیر را در build.gradle برنامه اضافه کنیم،
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:{latest-version}'
kapt 'androidx.hilt:hilt-compiler:{latest-version}'
و برای پشتیبانی از kapt، افزونه پشتیبانی برای kapt را مانند شکل زیر در build.gradle برنامه اضافه می کنیم.
plugins {
……..
id ' kotlin-kapt'
}
اکنون، برای pass دادن NetworkHelper و MainRepository، از ViewModelFactory در اینجا استفاده نمیکنیم، بلکه مستقیماً هر دو را پاس میکنیم و از انوتیشن @ViewModelInject مانند زیر استفاده می کنیم:
class MainViewModel @ViewModelInject constructor(
private val mainRepository: MainRepository,
private val networkHelper: NetworkHelper
) : ViewModel() {
}
در اینجا، انوتیشن ViewModelInject وابستگی را با استفاده از سازنده تزریق (inject) می کند و اکنون عملیات را در MainViewModel انجام خواهیم داد مانند:
در اینجا، ما users را در بلوک init و در داخل viewModelScope واکشی می کنیم، اتصال اینترنت را بررسی می کنیم و اگر اتصال خوب است، از طریق فراخوانی API اقدام می کنیم یا در غیر این صورت با خطا مقدار را روی LiveData تنظیم می کنیم.
سپس user LiveData در MainActivity برای نمایش item ها در recyclerView مشاهده می شود.
اگر مراحل بالا را مشاهده کردید، نمونه ViewModel را با استفاده از viewModels() دریافت می کنیم.
ViewModel که توسط @ViewModelInject حاشیه نویسی شده است فقط می تواند توسط View هایی که توسط @AndroidEntryPoint حاشیه نویسی شده اند reference داده شود.
به عنوان آخرین مرحله، permission زیر را در فایل Manifest خود اضافه کنید.
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
اکنون، ما راه اندازی پروژه را تمام کرده ایم و اگر پروژه را اجرا کنید، لیست کاربرانی که در recyclerView پر شده اند را مشاهده خواهید کرد.
به این ترتیب میتوانیم dagger را در پروژه اندروید خود پیادهسازی کنیم.
اکنون، بیایید در مورد امکانات بیشتری که در طول توسعه برنامه Android ما به وجود می آید، بیاموزیم.
WorkManger با Dagger-Hilt
چگونه می توانیم با Dagger-Hilt و WorkManager کار کنیم؟
اگر از WorkManger استفاده می کنیم، از @WorkerInject برای تزریق وابستگی در سازنده با استفاده از برنامه های افزودنی Jetpack (extention)استفاده می کنیم.
همچنین باید وابستگی زیر را برای WorkManager اضافه کنیم،
implementation 'androidx.hilt:hilt-work:{latest-version}'
Qualifier
مثالی را در نظر بگیرید که در آن دو تابع داریم که مقادیر strings را برمی گرداند. اما در حین فراهم کردن (provide) آن از طریق Dagger، چگونه dagger میداند که کدام کلاس به چه مقدار strings نیاز دارد زیرا هر دو از یک نوع هستند.
برای حل این مشکل از Qualifiers در Dagger استفاده می کنیم.
مثالی را در نظر بگیرید که در آن باید دو رشته مختلف یکی برای یک کلید API و دیگری برای مقداردهی اولیه کتابخانه مانند زیر داریم.
@Provides
@Singleton
fun provideApiKey() = "My ApiKey"
@Provides
@Singleton
fun provideLibraryKey() = "My Library Key"
در اینجا، Dagger-Hilt هرگز با موفقیت ساخته نمیشود، زیرا dagger هر دو را یکسان در نظر میگیرد، زیرا هر دو دارای یک string و یک نوع هستند و خطای زیر اتفاق خواهد افتاد
error: [Dagger/DuplicateBindings] java.lang.String is bound multiple times:
در اینجا، ما دو انوتیشن مختلف ApiKey و LibraryKey ایجاد کردیم و هر دو به عنوان @qualifier علامت گذاری شده اند.
این انوتیشن ها به ما کمک میکنند تا اجرای ApiKey و LibraryKey را متمایز کنیم.
اکنون، در ApplicationModule، هر دو ارائهدهنده(provider) کلید را با attach کردن انوتیشنی که ایجاد کردهایم، بهروزرسانی میکنیم:
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ApiKey
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class LibraryKey
اکنون، در اینجا میتوانید ببینید که ما qualifier منحصر به فرد را به هر provider رشته (string)متصل کردهایم و اکنون Dagger-Hilt کد را به صورت داخلی (internal) برای فراهم کردن (provide) مقادیر این رشتهها ایجاد میکند.
حالا برای تزریق تک تک آنها به MainActivity می رویم:
@ApiKey
@Inject
lateinit var apiKey:String
@LibraryKey
@Inject
lateinit var libraryKey:String
و اکنون، اگر آنها را به صورت منحصر به فرد ثبت کنیم،
/MainActivity: My ApiKey
/MainActivity: My Library Key
به این ترتیب میتوانید وابستگیهای چندگانه از یک نوع را با استفاده از qualifier فراهم کنید.
اگر به خاطر داشته باشید در NetworkHelper از @ApplicationContext استفاده کردیم که آن هم نوعی Qualifier است اما توسط خود Dagger-Hilt ارائه شده است.
به این ترتیب می توانید با Dagger-Hilt، کتابخانه جدید تزریق وابستگی که در بالای Dagger در پروژه شما ساخته شده است، کار کنید.
دیدگاهتان را بنویسید