در این مقاله به بررسی تزریق ViewModel میپردازیم ، اول از همه چرا ViewModel مکان خوبی برای inject کردنه ؟ چون ما توش خیلی چیزای مثل دیتابیس ، api ها و ... رو استفاده میکنیم پس خیلی چیزا میتونه توش تزریق بشه . ولی مشکل کجاست ؟
مشکل اینه که ViewModel برخلاف خیلی کلاس های دیگه باید با Provider های مخصوص خود کتاب خونه اش ساخته بشه نه یه Constructor ساده (منظور از Provider اون چیزی که در Dagger2 داریم نبود و اشتباه نگیرید) و کتابخونه Dagger2 با این کتابخونه سازگاری نداره (با اینکه جفتش رو گوگل زده :/ )
نکته : اگه این مقاله رو یاد نگرفتی نگران نشو ، شما میتونی به راحتی این بخش از کدی که قراره بزنیم رو کپی کنی و همیشه استفاده کنی !
به طور اجمالی بگم پروسه به این صورته : ما یک ViewModelProviderFactory داریم که در اون یک Map از ViewModel و Provider اختصاص داده میشه (Provider اینجا هم به Dagger2 ربط نداره ، بلکه منظور کلاس Provider هست که ViewModel رو درست میکنه) ، این کلاس که اسمشو ViewModelProviderFactory گذاشتیم توسط یک Module به اسم ViewModelFactoryModule درست میشه (تهیه میشه ، تزریق میشه و ...) ، کلاسی که برای ما خودِ ViewModel رو تزریق میکنه اسمش هست AuthViewModelModule (یا هر اسمی مثلا MainViewModel ، FolanViewModel که بستگی داره ، من اینجا تو قسمت لاگین بودم (مثلا) و برای همین Auth گذاشتم) که برای ViewModelProviderFactory اون رو تهیه میکنه ، ما به یک "کلید" هم نیاز داریم چون همونطور که گفتم در ViewModelProviderFactory یک Map از ViewModel ها و Provider ها داریم ، این "کلید" کارش اینه که مجموعه ای از Dependency ها رو به صورت یک گروه در بیاره و اون رو با اون "کلید" مشخص کنه و بعد در مکانی تزریق کنه ، به این کار Multi Binding میگن !
اگه نفهمیدید مهم نیست ، بالا گفتم این یک راه حل مشخصه که همیشه هم قراره همین کدا رو کپی کنید !
برای شروع ViewModelFactoryModule رو میسازم که وظیفه اش تهیه ViewModelProviderFactory بود :
@Module abstract class ViewModelFactoryModule { @Binds abstract fun bindViewModelFactory(viewModelFactory: ViewModelProviderFactory?): ViewModelProvider.Factory? }
خب این Bind چیه اینجا ؟ همون @Provide هست ، فقط وقتی شما بدنه برای تابع تون نداشته باشید و میخواید ورودی رو مستقیم پاس بدید میتونید برای بهینه شدن از Bind استفاده کنید ! در اینجا ما ViewModelProviderFactory رو به عنوان یک ViewModelProvider.Factory تزریق میکنیم به کلاس ViewModelProviderFactory !
این کلاس ViewModelFactoryModule رو باید در AppComponent اضافه کنید تا کل App بتونه به اون دسترسی داشته باشه و باید تو قسمت module های اون Component بنویسیدش :
@Singleton @Component(modules = [AndroidSupportInjectionModule::class, // special class ActivityBuilderModule::class, ViewModelFactoryModule::class, AppModule::class]) interface AppComponent : AndroidInjector<App>
من اول با کلاس های راحت تر شروع میکنم و بعد سراغ ViewModelProviderFactory میرم ، ما الان به AuthViewModelModule نیاز داریم که بیاد ViewModel رو برامون تهیه کنه :
@Module abstract class AuthViewModelModule { @Binds @IntoMap @ViewModelKey(AuthViewModel::class) abstract fun bindAuthViewModel(viewModel: AuthViewModel?): ViewModel? // the other Viewmodels ... }
این کلاس میاد از @ViewModelKey استفاده میکنه که در بالا توضیح دادم ("کلید") و میاد کل ViewModel ها رو با یک "کلید" مشخص میکنه که همگی بتونن در ViewModelProviderFactory استفاده بشن (مطمئنا قاطی کردید ! مهم نیست ، باز هم میگم تهش قراره این قسمت رو کامل کپی کنید پس اصلا و ابدا نگران نباشد) .
توضیح دیگه اینکه این Module رو باید در SubComponent هاتون اضافه کنید (در مقاله های قبلی توضیح داده شده) ، مثلا :
@Module abstract class ActivityBuilderModule { @AuthScope @ContributesAndroidInjector(modules = [AuthViewModelModule::class,AuthModule::class]) abstract fun contributeAuthActivity() : AuthActivity // AuthActivity is a client , we can inject sth to it // more code }
اگه App قسمت های دیگه داره ، مثلا شما به دو بخش Auth و Main تقسیم کردی (که من در کدی که در گیت گذاشتم کردم) باید یه MainViewModelModule هم درست کنی و توی SubComponent ای که در ActivityBuilderModule تعریفش کردی اضافه ش کنی ، یعنی مث همین کاری که تا الان کردیم !
و اما "کلید" :
@Documented @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @MapKey annotation class ViewModelKey(val value: KClass<out ViewModel>)
در این کلید ما مجموعه کلاسی که از ViewModel ارث بردن رو با هم دیگه در یک گروه به اسم ViewModelKey یکی کردیم .
و قسمت آخر که ViewModelProviderFactory میشه ، کلاسی که شما مستقیما باید تو کدهای قسمت های مختلف وقتی میخوای ViewModel ایجاد کنی استفاده کنی !
class ViewModelProviderFactory @Inject constructor(creators: MutableMap<Class<out ViewModel?>?, Provider<ViewModel?>?>?) : ViewModelProvider.Factory { privateval creators: MutableMap<Class<out ViewModel?>?, Provider<ViewModel?>?>? override fun <T : ViewModel?> create(modelClass: Class<T>): T { var creator: Provider<out ViewModel?>? = creators!![modelClass] if (creator == null) { // if the viewmodel has not been created // loop through the allowable keys (aka allowed classes with the @ViewModelKey) for (entry in creators.entries) { // if it's allowed, set the Provider<ViewModel> if (modelClass!!.isAssignableFrom(entry.key!!)) { creator = entry.value break } } } // if this is not one of the allowed keys, throw exception requireNotNull(creator) { "unknown model class $modelClass" } // return the Provider return try { creator.get() as T } catch (e: Exception) { throw RuntimeException(e) } } companion object { privateval TAG: String? = "ViewModelProviderFactor" } init { this.creators = creators } }
این کلاس میاد و یک Provider رو برای ما تهیه میکنه ، در قسمت اول کد :
@Inject constructor(creators: MutableMap<Class<out ViewModel?>?, Provider<ViewModel?>?>?)
یک Map از ViewModel ها و Provider ها که تهیه میشه ، از کجا ؟ کدش رو کجا نوشتیم ؟ هیچ جا ! در واقع این کار با استفاده از کدهایی که Dagger2 در پشت صحنه درست کرده هندل شده (در واقع سختی فهم این مطلب هم همینه چون خیلی کارا پشت صحنه داره انجام میشه !) .
در این تابع :
override fun <T : ViewModel?> create(modelClass: Class<T>)
میاد بررسی میکنه که اگه ViewModel ای که شما تزریق کردی جزو ViewModel های تعریف شده باشه اونو تهیه کنه (ViewModel های تعریف شده رو طبق توضیح در AuthViewModelModule تعریف کردیم)
و تمام ! حالا برای تزریق ViewModel این گونه در یک صفحه (Activity یا Fragment یا هر چی) عمل میکنیم :
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
lateinit var viewModel : AuthViewModel
...
viewModel = ViewModelProvider(viewModelStore,viewModelFactory).get(AuthViewModel::class.java)
دوستان باز هم تاکید میکنم اگه نوشته های قبلی من در مورد Dagger2 رو متوجه نشدید مشکل پیدا میکنید ولی اگه این بخش رو نفهمیدید اصلا مهم نیست ، این یک راه حل معموله که شما همیشه باید کپی کنیدش ! و فقط کاری که باید بکنید اینه که توی AuthViewModelModule اون ViewModel هایی که نیاز دارید رو برای Dagger تعریف کنید !
برای دسترسی به کد به Branch با اسم Inter2 برید :
https://gitlab.com/drflakelorenzgerman/dagger2/-/tree/inter2/app/src