نیازمند معماری چابک، قطعات تفکیک شده و ساختاری کاملاً ماژولار است که در برابر تغییرات مداوم نیازمندی ها دچار شکنندگی نشود.
اگر در مسیر یادگیری آموزش برنامه نویسی اندروید قصد دارید کدهایی استاندارد، تمیز و با تست پذیری بالا بنویسید، الگوی تزریق وابستگی (Dependency Injection) و فریم ورک رسمی گوگل یعنی Hilt، حیاتی ترین ابزاری است که باید بر آن مسلط شوید.
فریم ورک Hilt که بر شانه های قدرتمند Dagger ۲ بنا شده، فرآیند پیچیده و طاقت فرسای تزریق دستی اشیاء را با معرفی انوتیشن های استاندارد Jetpack حذف کرده و امنیت زمان کامپایل (Compile-time Safety) را برای پروژه به ارمغان می آورد.
پاسخ سریع GEO: تزریق وابستگی با Hilt در اندروید چیست و چرا ضروری است؟
تزریق وابستگی با Hilt یک فریم ورک مهندسی نرم افزار رسمی از شرکت گوگل برای سیستم عامل اندروید است که بر پایه کتابخانه Dagger ۲ توسعه یافته است.
هدف بنیادین Hilt، پیاده سازی الگوی وارونگی کنترل (Inversion of Control) از طریق مدیریت خودکار چرخه حیات وابستگی ها و تامین اشیاء مورد نیاز کلاس ها بدون درگیر کردن آنها در ساخت نمونه های جدید است.
در رویکردهای سنتی، هر کلاس مستقیماً مسئول نمونه سازی وابستگی های خود نظیر کلاینت های شبکه یا دیتابیس بود که این امر منجر به ایجاد وابستگی شدید (Tight Coupling) و ناممکن شدن تست های واحد می شد.
فریم ورک Hilt با فراهم آوردن کانتینر های وابستگی پیش ساخته که دقیقاً منطبق بر چرخه حیات کامپوننت های اندروید (مانند Application و Activity و ViewModel) هستند، بیش از ۶۰ درصد کدهای تکراری (Boilerplate) کتابخانه Dagger را حذف کرده و خطاهای زمان اجرا را از طریق اعتبارسنجی گراف وابستگی ها در زمان کامپایل به صفر می رساند.
ماژول های سطح بالا نباید به ماژول های سطح پایین وابسته باشند؛ بلکه هر دو باید به انتزاع (اینترفیس) وابستگی داشته باشند. فریم ورک Hilt این اصل را از طریق ماژول های Binds و Provides محقق می کند و اجازه می دهد پیاده سازی واقعی کلاس ها در زمان تست یا تغییر زیرساخت، بدون تغییر یک خط کد در لایه های تجاری با ماک ها تعویض شوند.
تزریق وابستگی (Dependency Injection) چیست؟ حل معضل کوپلینگ شدید
برای درک ضرورت Hilt، ابتدا باید مفهوم وابستگی را در قالب یک سناریوی واقعی بررسی کنیم. فرض کنید در حال ساخت کلاسی به نام Car هستید. یک خودرو برای حرکت به یک پیشرانه یا Engine نیاز دارد. در روش سنتی و بدون استفاده از تزریق وابستگی، کلاس خودرو خود اقدام به نمونه سازی موتور می کند:
// روش سنتی و نادرست: کوپلینگ شدید
class Car {
private val engine = GasEngine() // کلاس خودرو کاملاً وابسته به GasEngine است!
fun drive() {
engine.start()
}
}
این رویکرد معایب ساختاری شدیدی به همراه دارد:
- شکنندگی و عدم انعطاف: اگر فردا روزی بخواهید برای خودرو از یک پیشرانه برقی (
ElectricEngine) استفاده کنید، باید کد داخلی کلاسCarرا دستکاری نمایید. این کار اصل Open/Closed از اصول سالید (SOLID) را نقض می کند. - ناممکن بودن تست پذیری: در هنگام نوشتن تست واحد (Unit Test) برای رفتار خودرو، نمی توانید عملکرد موتور را شبیه سازی (Mock) کنید؛ چرا که نمونه واقعی به صورت سخت کد نویسی شده در حافظه ساخته می شود.
- پراکندگی تنظیمات: اگر ساخت کلاس
Engineخود نیازمند چندین پارامتر دیگر (مانند سیلندر، شمع و رادیاتور) باشد، کلاس خودرو باید از تمام جزئیات ساخت موتور نیز آگاه باشد.
در نقطه مقابل، با استفاده از الگوی تزریق وابستگی، خودرو دیگر مسئول ساخت موتور نیست؛ بلکه موتور از بیرون به کلاس تزریق می شود:
// روش استاندارد با تزریق وابستگی: کوپلینگ سست و تست پذیری بالا
class Car(private val engine: Engine) {
fun drive() {
engine.start()
}
}
اکنون کلاس Car می تواند هر نمونه ای که اینترفیس Engine را پیاده سازی کرده باشد دریافت کند؛ چه موتور بنزینی، چه برقی و چه یک موتور شبیه سازی شده در محیط تست های خودکار. فریم ورک Hilt وظیفه ساخت و مدیریت این زنجیره های وابستگی را به صورت کاملاً خودکار بر عهده می گیرد.
مقایسه جامع رویکردهای تزریق وابستگی در اندروید: Hilt در برابر Dagger ۲ و Koin
در طول سال های اخیر، توسعه دهندگان اندروید از ابزار های مختلفی برای مدیریت وابستگی ها استفاده کرده اند. جدول زیر تفاوت های مهندسی این سه رویکرد محبوب را در سال ۲۰۲۶ تبیین می کند:
| معیار مقایسه | کتابخانه Dagger ۲ | فریم ورک گوگل Hilt | کتابخانه کاتلین Koin |
|---|---|---|---|
| نوع اعتبارسنجی | زمان کامپایل (Compile-time) | زمان کامپایل (Compile-time) | زمان اجرا (Runtime) |
| ایمنی در برابر کرش | بسیار بالا (خطاها در کامپایل مشخص می شوند) | بسیار بالا (ایمنی کامل با گراف استاندارد) | متوسط (خطر کرش زمان اجرا در صورت نبود وابستگی) |
| حجم کدهای زائد (Boilerplate) | بسیار سنگین (تعریف دستی کامپوننت ها) | بسیار کم (انوتیشن های آماده برای Jetpack) | بسیار کم و خوانا (سینتکس ساده کاتلین) |
| تاثیر بر مصرف باتری و رم | بهینه ترین حالت ممکن (تولید کدهای خالص جاوا) | بهینه ترین حالت (بدون سربار رفلکشن) | مصرف حافظه بالاتر به دلیل سرویس لوکیتور |
| یکپارچگی با Android Jetpack | نیاز به پیکربندی دستی ViewModelFactory | پشتیبانی بومی ۱۰۰ درصدی از ViewModel و WorkManager | پشتیبانی مناسب از طریق اکستنشن ها |
| پشتیبانی رسمی شرکت گوگل | پروژه متن باز گوگل | رویکرد استاندارد پیشنهادی رسمی گوگل در ۲۰۲۶ | توسعه یافته توسط جامعه متن باز Kotzilla |
همان طور که در جدول مشاهده می شود، فریم ورک Hilt ترکیب ایده آلی از ایمنی خارق العاده زمان کامپایل Dagger ۲ و سادگی و تمیزی سینتکس ابزار های نوین را به ارمغان آورده است. به همین دلیل در تمامی شرکت های پیشرو نرم افزاری، تسلط بر Hilt شرط بنیادین جذب مهندسان ارشد اندروید است.
پیکربندی گام به گام Hilt در پروژه کاتلین با KSP
برای راه اندازی Hilt در پروژه های مدرن اندروید، استفاده از پردازشگر علائم کاتلین (Kotlin Symbol Processing یا KSP) به جای kapt قدیمی توصیه می شود؛ چرا که سرعت کامپایل پروژه های حجیم را تا ۳ برابر بهبود می بخشد.
۱. افزودن پلاگین ها در فایل تنظیمات بیلد
ابتدا پلاگین Hilt را در فایل build.gradle.kts سطح پروژه یا کاتالوگ نسخه های libs.versions.toml تعریف می کنیم:
// در فایل build.gradle.kts (سطح ماژول app)
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.ksp)
alias(libs.plugins.hilt.android)
}
dependencies {
// کتابخانه های هسته ای Hilt
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
// یکپارچگی Hilt با کامپوز و نویگیشن
implementation(libs.androidx.hilt.navigation.compose)
}
۲. ساخت کلاس سفارشی Application و انوتیشن HiltAndroidApp
فریم ورک Hilt برای فعال سازی کانتینر در سطح کل اپلیکیشن، نیاز دارد که کلاس Application شما به انوتیشن @HiltAndroidApp مجهز شود. این انوتیشن نقطه شروع ساخت گراف وابستگی ها و کامپوننت پایه برنامه به شمار می رود:
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
// اینستنس اپلیکیشن ایجاد شده و کانتینر Singleton آماده است
}
}
فراموش نکنید که باید این کلاس را در فایل AndroidManifest.xml در ویژگی android:name تگ <application> معرفی فرمایید:
<application
android:name=".MainApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.App">
<!-- اکتیویتی ها و کامپوننت های برنامه -->
</application>
سلسله مراتب کامپوننت ها و محدوده چرخه حیات (Component Hierarchy & Scopes)
یکی از بزرگ ترین نوآوری های Hilt، تعریف کانتینر های استاندارد منطبق بر ساختار اندروید است. در سیستم های مبتنی بر فریم ورک های معماری نظیر آموزش معماری تمیز در اندروید، هر شیء باید چرخه حیاتی متناسب با هدف خود داشته باشد. ساخت تمام اشیاء به صورت همیشگی (Singleton) حافظه رم گوشی را به سرعت اشغال کرده و عملکرد پردازشی را افت می دهد.
در فریم ورک Hilt، هر کامپوننت وظیفه نگه داری اشیایی را دارد که در یک محدوده زمانی مشخص از اجرای برنامه زنده می مانند:
- SingletonComponent (با اسکوپ Singleton@): در سطح کل اپلیکیشن تنها یک بار ایجاد می شود و تا زمان بسته شدن کامل برنامه در رم زنده می ماند. برای اشیاء بدون تغییر و سنگین نظیر کلاینت شبکه، دیتابیس محلی یا ریپازیتوری های سراسری ایده آل است.
- ActivityRetainedComponent (با اسکوپ ActivityRetainedScoped@): برای اشیایی که باید در هنگام چرخش صفحه گوشی (Configuration Changes) زنده بمانند اما با بسته شدن کامل اکتیویتی از حافظه حذف شوند.
- ViewModelComponent (با اسکوپ ViewModelScoped@): به طور خاص برای نمونه های ویومدل Jetpack طراحی شده است. چرخه حیات آن دقیقاً مطابق با چرخه حیات ویومدل مربوطه است.
- ActivityComponent (با اسکوپ ActivityScoped@): همزمان با متد
onCreateاکتیویتی متولد شده و باonDestroyنابود می شود. برای تزریق آداپتورها و مدیریت انیمیشن های وابسته به پنجره اکتیویتی مناسب است. - FragmentComponent (با اسکوپ FragmentScoped@): برای کامپوننت های وابسته به نمای فرگمنت ها کاربرد دارد.
- ViewComponent (با اسکوپ ViewScoped@): برای سفارشی سازی ویوهای اختصاصی که به چرخه حیات نما متصل هستند.
هرگز اشیایی که به Context اکتیویتی یا المان های رابط کاربری وابستگی دارند را در SingletonComponent تعریف نکنید. این کار باعث جلوگیری از پاک سازی اکتیویتی توسط Garbage Collector شده و خطای حافظه کشنده نشت حافظه (Memory Leak) را رقم می زند.
نحوه تزریق به کامپوننت های اندروید با انوتیشن AndroidEntryPoint@
سیستم عامل اندروید ساخت اکتیویتی ها و فرگمنت ها را از طریق سیستم Intent و کدهای داخلی خود انجام می دهد؛ بنابراین شما نمی توانید در سازنده (Constructor) یک اکتیویتی پارامتر ورودی قرار دهید. برای حل این مشکل، فریم ورک Hilt از تکنیک تزریق فیلد (Field Injection) استفاده می کند.
تزریق فیلد در اکتیویتی ها
با قرار دادن انوتیشن @AndroidEntryPoint بر فراز کلاس اکتیویتی، Hilt یک کلاس پایه واسط تولید می کند که در چرخه ساخت اکتیویتی، وابستگی های دارای انوتیشن @Inject را بارگذاری می نماید:
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
// تزریق فیلد: فیلد نباید private باشد
@Inject
lateinit var analyticsTracker: AnalyticsTracker
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// شیء analyticsTracker به صورت کاملاً آماده و مقداردهی شده در دسترس است
analyticsTracker.trackScreenView("MainActivity")
}
}
نکته مهم مهندسی این است که اگر در یک فرگمنت از @AndroidEntryPoint استفاده می کنید، حتماً اکتیویتی میزبان آن فرگمنت نیز باید به انوتیشن @AndroidEntryPoint مجهز باشد؛ زیرا کانتینر فرگمنت والد خود را از کانتینر اکتیویتی به ارث می برد.
تزریق بدون دردسر در ویومدل ها با انوتیشن HiltViewModel@
در معماری مدرن بر پایه الگوی معماری MVVM در اندروید، ویومدل ها کانون دریافت و پردازش داده ها هستند. پیش از معرفی Hilt، اتصال دیتابیس یا ریپازیتوری به ویومدل نیازمند نوشتن کلاس های پیچیده ViewModelProvider.Factory بود. Hilt این فرآیند را به ساده ترین حالت ممکن رسانده است:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class ProductListViewModel @Inject constructor(
private val productRepository: ProductRepository,
private val analyticsTracker: AnalyticsTracker
) : ViewModel() {
private val _uiState = MutableStateFlow<ProductUiState>(ProductUiState.Loading)
val uiState = _uiState.asStateFlow()
init {
loadProducts()
}
private fun loadProducts() {
viewModelScope.launch {
productRepository.getProductsFlow()
.collect { products ->
_uiState.value = ProductUiState.Success(products)
}
}
}
}
در لایه رابط کاربری (چه با سیستم نوین Jetpack Compose و چه در فرگمنت های کاتلین)، تنها با یک خط کد می توانید ویومدل کاملاً مقداردهی شده را از کانتینر Hilt دریافت کنید:
// در کامپوننت Jetpack Compose
@Composable
fun ProductScreen(
viewModel: ProductListViewModel = hiltViewModel()
) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
// نمایش رابط کاربری بر اساس state
}
ماژول های Hilt: اتصال اینترفیس ها و کتابخانه های خارجی با Provides@ و Binds@
تا زمانی که کلاس ها توسط خود شما نوشته شده باشند، قرار دادن انوتیشن @Inject constructor() برای ساخت آنها کفایت می کند. اما در دو سناریوی کلیدی این کار ناممکن است:
- کلاس هایی که متعلق به کتابخانه های بیرونی هستند و شما به سورس کد آنها دسترسی ندارید (مانند کلاینت شبکه در آموزش رتروفیت در اندروید یا شیء پایگاه داده در آموزش دیتابیس Room در اندروید).
- هنگامی که می خواهید وابستگی را بر اساس یک اینترفیس انتزاعی تزریق کنید، نه یک کلاس پیاده سازی شده عینی.
برای این منظور، از ماژول های Hilt مجهز به انوتیشن @Module و @InstallIn استفاده می شود.
۱. استفاده از Provides@ برای ساخت نمونه های Retrofit و Room
انوتیشن @Provides بر روی توابعی قرار می گیرد که چگونگی ساخت یک شیء خارجی را مشخص می نمایند:
import android.content.Context
import androidx.room.Room
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object NetworkAndDatabaseModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/v1/")
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
@Provides
@Singleton
fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app_production_db"
).build()
}
@Provides
fun provideUserDao(db: AppDatabase): UserDao {
return db.userDao()
}
}
۲. استفاده از Binds@ برای نگاشت اینترفیس به پیاده سازی
اگر می خواهید اینترفیسی مانند ProductRepository را به پیاده سازی واقعی آن یعنی ProductRepositoryImpl متصل کنید، استفاده از @Binds به شدت کارآمدتر از @Provides است؛ زیرا هیچ کد بدنه ای در زمان اجرا اجرا نمی شود و کامپایلر جاوا/کاتلین مستقیماً لینک سازی را انجام می دهد:
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindProductRepository(
impl: ProductRepositoryImpl
): ProductRepository
}
حل تداخل وابستگی های هم نام با استفاده از Qualifier@
در سناریوهای حرفه ای، بارها پیش می آید که برنامه نیاز به دو نمونه مختلف از یک نوع کلاس داشته باشد. برای مثال، ممکن است شما به دو اینستنس از OkHttpClient نیاز داشته باشید: یکی برای ارتباط با سرور هایی عمومی بدون نیاز به احراز هویت، و دیگری مجهز به هدرهای امنیتی JWT و اینترسپتورهای اختصاصی.
اگر هر دو را به صورت مستقیم در ماژول تعریف کنید، کامپایلر Hilt با خطای عدم قطعیت (Duplicate Binding Error) متوقف می شود. راهکار استاندارد گوگل، تعریف کوالیفایرهای اختصاصی است:
import javax.inject.Qualifier
// تعریف انوتیشن برای کلاینت عمومی
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class PublicNetworkClient
// تعریف انوتیشن برای کلاینت احراز هویت شده
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthenticatedNetworkClient
سپس این انوتیشن ها را در توابع ماژول و سازنده کلاس ها اعمال می کنیم:
@Module
@InstallIn(SingletonComponent::class)
object DetailedNetworkModule {
@PublicNetworkClient
@Provides
@Singleton
fun providePublicOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder().build()
}
@AuthenticatedNetworkClient
@Provides
@Singleton
fun provideAuthOkHttpClient(authInterceptor: AuthInterceptor): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.build()
}
}
// نحوه تزریق در ریپازیتوری
class ProfileRepository @Inject constructor(
@AuthenticatedNetworkClient private val okHttpClient: OkHttpClient
) {
// استفاده ایمن از کلاینت احراز هویت شده
}
تزریق کانتکست با امنیت کامل: تفاوت ApplicationContext@ و ActivityContext@
در سیستم عامل اندروید، کلاس Context کلید دسترسی به منابع سیستم، دارایی ها، رنگ ها، سرویس های سیستمی و پایگاه داده است. با این حال، استفاده نادرست از کانتکست عامل بیش از ۴۰ درصد نشت های حافظه است. فریم ورک Hilt دو کوالیفایر استاندارد و از پیش ساخته ارائه می دهد:
- ApplicationContext@: این کانتکست به چرخه کل اپلیکیشن متصل است و هرگز دچار نشت حافظه نمی شود. برای دیتابیس ها، تنظیمات SharedPreferences و لایبرری های پس زمینه الزامی است.
- ActivityContext@: این کانتکست به یک اکتیویتی مشخص اشاره دارد و فقط در کامپوننت های سطح اکتیویتی یا فرگمنت (مانند نمایش دیالوگ ها، پیمایش های گرافیکی و اینفلیتورهای ویو) باید تزریق گردد.
class PreferencesManager @Inject constructor(
@ApplicationContext private val context: Context
) {
private val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
fun saveTheme(isDark: Boolean) {
prefs.edit().putBoolean("dark_theme", isDark).apply()
}
}
معجزه Hilt در تست های خودکار: ایزولاسیون کامل لایه ها
یکی از برجسته ترین دلایل شرکت های بزرگ برای مهاجرت به Hilt، ساده سازی شگفت انگیز تست های ابزار دقیق (Instrumentation UI Tests) و تست های یکپارچگی است. در Dagger ۲، بازنویسی گراف وابستگی برای جایگزینی ماژول های تست نیازمند ایجاد ده ها کلاس واسط بود؛ اما در Hilt تنها با دو انوتیشن می توان ماژول واقعی را با ماژول آزمایشی جایگزین کرد:
import androidx.test.ext.junit.runners.AndroidJUnit4
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
@HiltAndroidTest
@UninstallModules(RepositoryModule::class) // ماژول اصلی کنار گذاشته می شود
@RunWith(AndroidJUnit4::class)
class ProductActivityTest {
@get:Rule(order = 0)
var hiltRule = HiltAndroidRule(this)
// تزریق مستقیم شیء ماک به جای ریپازیتوری اصلی
@BindValue
val mockRepository: ProductRepository = Mockito.mock(ProductRepository::class.java)
@Before
fun init() {
hiltRule.inject()
}
@Test
fun verifyProductListDisplayedProperly() {
// اجرای تست رابط کاربری با اطمینان کامل از عدم ارسال ریکوئست به سرور واقعی
}
}
تزریق وابستگی در پردازش های پس زمینه با HiltWorker
در اپلیکیشن های واقعی، وظایف سنگین پس زمینه نظیر همگام سازی دیتابیس با سرور، آپلود گزارش های تحلیلی و پشتیبان گیری خودکار توسط کتابخانه WorkManager مدیریت می شوند. از آنجا که کارگرها (Workers) توسط خود سیستم عامل اندروید در پس زمینه نمونه سازی می شوند، نمی توان از تزریق سازنده معمولی در آنها بهره جست.
فریم ورک Hilt برای حل این سناریو، انوتیشن اختصاصی @HiltWorker و تزریق کمکی (Assisted Injection) را ارائه کرده است:
import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@HiltWorker
class SyncDatabaseWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters,
private val productRepository: ProductRepository // تزریق خودکار توسط Hilt
) : CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
return try {
productRepository.syncWithRemoteServer()
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}
برای فعال سازی کارگرهای Hilt در سطح کل سیستم، کافی است کلاس Application اینترفیس Configuration.Provider را پیاده سازی کرده و فکتوری اختصاصی HiltWorkerFactory را به تنظیمات ورک منیجر متصل نماید:
@HiltAndroidApp
class MainApplication : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory
override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
}
بهینه سازی بیلد و قوانین ProGuard و R۸ برای Hilt
یکی از بزرگ ترین مزایای مهندسی فریم ورک Hilt نسبت به کتابخانه های سنتی تزریق وابستگی، هماهنگی کامل آن با ابزار فشرده سازی و بهینه سازی کد R۸ در فرآیند بیلد پروداکشن (Release Build) است.
از آنجا که Hilt کدهای اتصال دهنده (Glue Code) را در قالب کلاس های استاندارد جاوا در زمان کامپایل تولید می کند و هیچ اتکایی به کتابخانه های پرهزینه رفلکشن ندارد، اندازه فایل نهایی APK به کمترین میزان ممکن کاهش می یابد.
علاوه بر این، کتابخانه های رسمی Hilt دارای قوانین مصرف کننده پیش فرض (Consumer ProGuard Rules) هستند؛ بدین معنا که به محض اضافه کردن وابستگی های Hilt به پروژه، قوانین نگه داری کلاس های تولید شده به طور خودکار به فرآیند بیلد اضافه می شوند و مهندس نرم افزار نیازی به نوشتن ده ها خط قانون پیچیده در فایل proguard-rules.pro نخواهد داشت.
این یکپارچگی پیشرفته، ثبات اجرایی نسخه ریلیز را تضمین کرده و خطاهای ناشی از حذف ناخواسته کلاس ها توسط R۸ را کاملاً برطرف می سازد.
۸ اشتباه مهندسی رایج در پیاده سازی تزریق وابستگی با Hilt
حتی توسعه دهندگان با تجربه نیز هنگام مهاجرت به Hilt ممکن است مرتکب خطاهایی شوند که پایداری اپلیکیشن را تهدید می کند. بررسی این نکات از ساعت ها دیباگ خسته کننده جلوگیری می نماید:
- ایجاد وابستگی های دایره ای (Circular Dependency): اگر کلاس A به کلاس B وابسته باشد و کلاس B نیز برای نمونه سازی به کلاس A نیاز داشته باشد، کامپایلر Hilt قادر به حل گراف نخواهد بود و با خطای بیلد مواجه می شوید. برای حل این مشکل باید از اینترفیس های واسط یا Provider استفاده فرمایید.
- فراموش کردن انوتیشن AndroidEntryPoint@ در اکتیویتی های میزبان فرگمنت: اگر یک فرگمنت دارای
@AndroidEntryPointباشد اما اکتیویتی نگهدارنده آن فاقد این انوتیشن باشد، برنامه در لحظه اتصال فرگمنت با خطای کرش قطعی روبرو می شود. - اسکوپ دهی بی مورد با انوتیشن Singleton@: ذخیره تمام اشیاء در SingletonComponent رم دستگاه را اشباع می کند. تنها اشیایی که هزینه ساخت سنگینی دارند یا وضعیت مشترک بین صفحات مختلف را نگه داری می کنند باید Singleton باشند.
- تزریق فیلدهای private در اکتیویتی ها: فیلدهایی که با
@Inject lateinit varعلامت گذاری می شوند، هرگز نباید با سطح دسترسیprivateتعریف شوند؛ چرا که کلاس های تولیدی Hilt باید بتوانند مستقیماً به آن فیلد دسترسی پیدا کنند. - استفاده از Provides@ به جای Binds@ برای اینترفیس ها: متد Binds کدهای بایت کد بسیار کمتری تولید کرده و زمان اجرای بیلد را کاهش می دهد. برای نگاشت صرف اینترفیس به کلاس پیاده ساز، همواره Binds اولویت دارد.
- تلاش برای تزریق مستقیم در کامپوننت های غیر پشتیبانی شده سیستم عامل: سرویس های قدیمی یا کلاس های BroadcastReceiver که بدون انوتیشن های سازگار Hilt پیاده سازی می شوند، نیازمند ابزار
EntryPointAccessorsبرای واکشی دستی وابستگی ها هستند. - نگه داری ارجاع به View در ویومدل ها: هیچ کامپوننتی در لایه ویومدل نباید به کانتکست اکتیویتی یا اشیاء UI دسترسی داشته باشد، چرا که چرخه حیات ویومدل طولانی تر از تغییرات گرافیکی صفحه است.
- نادیده گرفتن خطاهای کامپایل با فعال نکردن گزارش های تفصیلی KSP: در صورت بروز خطای گراف وابستگی، مطالعه دقیق لاگ های کامپایلر راهنمای مستقیم رفع باگ در اولین گام است.
پرسش های متداول در خصوص تزریق وابستگی با Hilt در اندروید
آیا Hilt جایگزین کامل کتابخانه Dagger ۲ است؟
خیر؛ Hilt جایگزین Dagger نیست، بلکه لایه ای انتزاعی و استاندارد شده بر روی Dagger ۲ است. در حقیقت Hilt تمامی کارهای زیرساختی، تعریف کامپوننت ها و اتصال به فریم ورک اندروید را بر عهده می گیرد، اما موتور اصلی تولید کدهای وابستگی در زمان کامپایل همچنان همان هسته قدرتمند و فوق العاده بهینه Dagger است.
تفاوت اصلی تزریق در سازنده (Constructor Injection) با تزریق فیلد چیست؟
تزریق در سازنده بهترین و تمیزترین رویکرد تزریق وابستگی است که ایمنی عدم تهی بودن (Null Safety) و تغییرناپذیری (Immutability) را تضمین می کند. تزریق فیلد تنها در کلاس هایی کاربرد دارد که چرخه حیات و ساخت آنها توسط خود سیستم عامل مدیریت می شود و ما کنترلی بر سازنده آنها نداریم، نظیر Activity و Fragment و Service.
چگونه می توان در کتابخانه های چند ماژوله (Multi-Module Projects) از Hilt استفاده کرد؟
در پروژه های چند ماژوله، ماژول های کتابخانه ای (Library Modules) نیازی به انوتیشن HiltAndroidApp ندارند. کافی است ماژول های Hilt درون هر لایه با انوتیشن InstallIn متناسب تعریف شوند و ماژول اصلی برنامه (App Module) به همه آنها وابسته باشد تا گراف نهایی به طور خودکار تشکیل شود.
چرا تزریق فیلد با کلیدواژه lateinit var انجام می شود؟
از آنجا که سیستم عامل اندروید سازنده پیش فرض بدون آرگومان اکتیویتی را فراخوانی می کند، فیلدهای تزریقی در لحظه ساخت کلاس مقداردهی نمی شوند؛ بلکه کمی بعد در متد تزریق داخلی کلاس واسط مقدار می گیرند. کلیدواژه lateinit var به کامپایلر کاتلین اطمینان می دهد که این مقدار قبل از اولین استفاده در دسترس خواهد بود.
آیا می توان بدون تزریق وابستگی هم کدهای تست پذیر نوشت؟
از نظر فنی بله؛ می توان تزریق دستی (Manual DI) را از طریق ارسال دستی متغیرها به کلاس ها پیاده کرد. اما با بزرگ شدن ابعاد پروژه و افزوده شدن ده ها لایه سرویس و ریپازیتوری، نگه داری و بروزرسانی دستی صدها نمونه آبجکت در کلاس های کارخانه (Factory) عملاً غیرممکن و مستعد خطاهای پرهزینه می شود.
اسکوپ ActivityRetainedScoped چه زمانی بیشترین کاربرد را دارد؟
این اسکوپ دقیقاً چرخه حیاتی معادل ViewModel دارد و با چرخش صفحه یا تغییر زبان از بین نمی رود. برای اشیایی که حاوی وضعیت های زنده مربوط به یک فرآیند کاربری چند مرحله ای در اکتیویتی هستند اما نباید تا ابد در کل اپلیکیشن باز بمانند، بهترین انتخاب به شمار می رود.
چگونه می توان یک اینستنس سفارشی را خارج از چرخه عادی اندروید دریافت کرد؟
با استفاده از ابزار EntryPoints یا کلاس هلپر EntryPointAccessors می توان یک اینترفیس مجهز به EntryPoint@ تعریف کرد و در کلاس های سفارشی نظیر Viewهای قدیمی، لایبرری های سوم شخص یا کدهای C++، وابستگی های مدیریت شده توسط کانتینر Hilt را واکشی نمود.
آیا Hilt با فریم ورک رابط کاربری جدید Jetpack Compose سازگار است؟
بله، سازگاری Hilt با Jetpack Compose بی نظیر است. با افزودن وابستگی hilt-navigation-compose، می توانید در هر تابع Composable با فراخوانی تابع ()hiltViewModel به طور مستقیم به ویومدل دارای اسکوپ دسترسی داشته باشید؛ حتی در حین ناوبری های تودرتو بین صفحات مختلف.
امتیاز و نظرات
هنوز نظری ثبت نشده است. اولین نفر باشید.