davud hosseiny
davud hosseiny
خواندن ۹ دقیقه·۲ سال پیش

مهاجرت از دگر به Hilt به صورت دوره ای(شامل برنامه های Multi-module)

Hilt
Hilt


اگر پروژه ای دارید که توش از دگر استفاده میکنید و نمیدونید مهاجرت به هیلت رو از کجا شروع کنید جای درستی اومدید. مخصوصا اگه پروژتون بزرگه و وقت مایگریت(مهاجرت) کردن همه قسمت ها به هیلت رو ندارید یا می‌خواید فقط قسمت های جدید رو با هیلت انجام بدید.

ما توی این مقاله می‌خوایم راهی برای مهاجرت به هیلت به صورت قسمت قسمت برای پروژه هایی که با Component Dependencies کار میکنن معرفی کنیم.

این مقاله برای کسایی که با دگر و هیلت ی مقداری آشنایی دارن مناسب هست. اگر شما با این کتابخونه‌ها آشنایی کافی ندارید میتونید قبلش سری به اینجا بزنید تا آشنایی بیشتری پیدا کنید.


کمی پیش Google کتابخونه ی جدیدش به اسم hilt رو برای DI معرفی کرد. از ویژگی های این کتابخونه میشه به سادگیش و learning curve پایینش نسبت به دگر اشاره کرد.

هیلت بر پایه ی دگر ساخته شده و در درونش از دگر استفاده میکنه. بخاطر همین این دوتا کتابخونه میتونن در کنار هم در ی پروژه استفاده بشن.

این مقاله کمکتون میکنه پروژه ای که بر پایه ی دگر هست رو به صورت دوره ای به هیلت مایگریت کنید. یعنی مجبور نیستیم همه ی کدهای DI رو به هیلت مایگریت کنید و تیکه تیکه این کارو انجام می‌دیم.

پیش فرض این مقاله اینه که پروژمون بر پایه ی Dagger Componentها ست و نه SubComponentها و ممکنه همش یا قسمت هاییش برای SubComponentها جواب نده.


ما می‌تونیم یک یا چند component رو به هیلت مایگریت کنیم ولی بقیه componentها دگری باشند و کدهای قسمت هیلت و دگر با هم دیگه کار میکنن ولی این نکته رو باید بدونیم که کدهای قسمت دگر میتونن از کدهای هیلت استفاده کنن ولی برعکسش صادق نیست. کدهای هیلت نمیتونن به componentهای دگری وابستگی داشته باشن. بر همین اساس مهاجرت به هیلت رو باید از قسمت های پایه ای کدمون شروع کنیم. در درجه اول باید از کلاس Application شروع کنیم، چون این کلاس پایه ی ساخت گراف دگر هست.


قبل شروع قسمت اصلی میخوام چندتا مفهوم رو که تو مقاله استفاده شدن با هم مرور کنیم:


یک Binding: به طور کلی به هر شی که به گراف دگر اضافه میشه Binding میگیم. این مفهوم رو با انوتیشن Binds@ قاطی نکنید که یکسان نیستن.

دوم EntryPoint: عنصری در هیلت وجود داره به اسم Entrypoint و این عنصر باعث میشه بتونیم Bindingها رو به خارج گراف هیلت ارائه بدیم (مثلا برای اینکه در دگر ازشون استفاده کنیم). از این لحاظ عملکرد Entrypoint شبیه Componentها ست. EntryPointها با انوتیشن EntryPoint@ مشخص میشن.

سوم Componentهای پیش فرض هیلت: در هیلت ی سری Component به صورت پیش فرض وجود داره که با کلاس های اندرویدی متناظر هستن و LifeCycle یکسانی دارن(با هم ساخته و پاک میشن).

مثلا FragmentComponent با Fragment متناظر هست. این Componentها که در زیر لیست شون اومده برای بسیاری از کاربردها کافیه و دیگه معمولا ما نیاز به نوشتن Custom Component نداریم. همچنین این Componentها Scopeهای متناظر‌شون رو هم دارن که تو جدول زیر لیست شدن:


جدول تطابق Android class به Component/Scope
جدول تطابق Android class به Component/Scope



در نتیجه ما دیگه در هیلت (معمولا) Component نمی‌نویسیم و فقط Module و EntryPoint می‌نویسیم و مشخص می‌کنیم که این Module یا EntryPoint مربوط به کدوم یکی از Componentهای جدول بالا هست. برای مثال برای اینکه ماژول زیر در SingletonComponent اضافه بشه به صورت زیر عمل می‌کنیم:

افزودن Bindingها به SingletonComponent
افزودن Bindingها به SingletonComponent


خوب تعاریف تموم شد. بریم که پروژه رو هیلتی کنیم :)

Let's Hilt this
Let's Hilt this




با فرض داشتن ی پروژه ی دگری که شامل ی سری Component که به هم Depend هستن شروع میکنیم و مراحل رو پیش می‌بریم.


1. افزودن Dependencyها

در مرحله اول دیپندنسی های هیلت رو اضافه میکنیم:

https://gist.github.com/DHosseiny/138a8d6b4e1a6d20dcdc23f2192cafea

اگر از kapt استفاده نمیکنید kapt رو با annotationProcessor جایگزین کنید.

اگر در گریدل-ماژولی که دارید روش کار میکنید کل کدهای اون ماژول رو به هیلت مایگریت خواهید کرد میتونید دپندنسی های دگر رو هم پاک کنید:

هر کدوم از این dependencyها رو که دارید پاک کنید.
هر کدوم از این dependencyها رو که دارید پاک کنید.


اگر در ماژول تون از Workerها استفاده میکنید این دپندنسی ها رو هم اضافه کنید:

https://gist.github.com/DHosseiny/8d5f6e2c1b03ec48d40e6b7d494523e3

اگر کامپوننت اندرویدی از جمله Activity, Service, Fragment یا BroadcastReceiver دارید plugin dagger.hilt.android.plugin رو هم به گریدل ماژول‌تون تو قسمت pluginها اضافه کنید:

https://gist.github.com/DHosseiny/22cff05017dd42c4771907d3b3005248

حالا میریم سراغ کلاس Application و انوتیشن HiltAndroidApp رو بالاش اضافه می‌کنیم:

https://gist.github.com/DHosseiny/f348e3e52809bfabdf88cefcd25d8ef6


اگر کلاس Applicationتون اینترفیس Configuration.Providerرو پیاده سازی میکنه و میخواید Workerی رو به Hilt مایگریت کنید به نکته آخر مقاله مراجعه کنید.



۲- مهاجرت کامپوننت

ما اینجا یک کامپوننت رو به هیلت مایگریت میکنیم و بعد می‌بینیم که چطور یک کامپوننت دگر ازش میتونه استفاده کنه.

نکته: فقط میتونید کامپوننت‌هایی رو به هیلت مایگریت کنید که dependency به ی کامپوننت دگری نداشته باشن(با استفاده از انوتیشین Component@ قسمت dependencies کامپوننت‌ها رو به هم depend می‌کنیم). اگر دارن اول باید اون dependency رو مایگریت کنید.

برای مثال کامپوننت زیر رو در نظر بگیرید:

مثال برای کامپوننت
مثال برای کامپوننت


در مرحله‌ی اول سراغ قسمت modules میریم. اگر در بین ماژول ها ماژول AndroidInjectionModule وجود داره پاکش کنید(در dagger-android ازش استفاده میشه).

بعد میریم سراغ تک تک moduleها و اونها رو هم به هیلت مایگریت می‌کنیم. مایگریت ماژول ها رو پایین‌تر در مرحله ۳ گفتم و اگر ماژولی رو میخواید مایگریت بدید همین حالا سراغ مرحله ی ۳ برید. ?


بعد مایگریت کردن همه ی ماژول‌ها می‌تونیم کل قسمت modules رو پاک کنیم:


modules حذف قسمت
modules حذف قسمت


در مرحله‌ی بعد میریم سراغ bindingهایی(متدهایی) که کامپوننت تعریف کرده، که اینجا دوتا هستند.

اگر کامپوننتی به این کامپوننت وابستگی داره، این متدها باید بمونن، اگر نه می‌تونیم حذفشون کنیم. تو این مثال فرض می‌کنیم که همچین کامپوننتی وجود داره.

در مرحله بعد اگر کامپوننت اینترفیس AndroidInjector رو پیاده سازی میکنه اون رو هم حذف میکنیم:

AndroidInjector پاک میشه
AndroidInjector پاک میشه

در مرحله‌ی بعد انوتیشن Component@ و هر چیزی که داخلش هست رو حذف میکنیم.

حتی اگه Component@ قسمت dependencies داره اون رو هم حذف میکنیم. چون فرضمون این هست که کامپوننت‌های داخل قسمت dependencies قبلا به هیلت مایگریت شدن.


اگر کامپوننت انوتیشن Scope داره اون رو هم حذف میکنیم و بجاش دوتا انوتیشن جدید میذاریم:

انوتیشن EntryPoint@

و انوتیشن InstallIn@. در این انوتیشن قراره مشخص کنیم که این کامپوننت برای چه کلاس اندرویدیی کار می‌کنه. مثلا اگه این کامپوننت قرار فیلدهای ی Activity رو Inject کنه از ActivityComponent استفاده می‌کنیم. به همین منوال برای همه کلاس های اندرویدی دیگه هم component از پیش تعریف شده‌ای وجود داره که همه شون رو توی جدول پایین آووردم:


جدول کلاس های اندرویدی و کامپوننت متناظرشون
جدول کلاس های اندرویدی و کامپوننت متناظرشون


حالت قبل و بعد از تغییر انوتیشن ها رو زیر میبینیم:

قبل و بعد
قبل و بعد


حالا دیگه DiscountCodeComponent ی کامپوننت نیست و در واقع EntryPoint هست ولی همونطور که بالاتر گفتیم کاربردی مشابه کامپوننت داره. در نتیجه اگر در ی کامپوننت دگری ازش به عنوان dependency استفاده شده لازم نیست روی قسمت دگر کاری بکنید. دگر میتونه از Entrypoint جدیدمون مثل component قدیمی استفاده کنه. مثل بنز کار می‌کنه. حالا می‌تونید کامپوننت رو Renameش کنید و بجای Component، بنویسید EntryPoint.


۳- مهاجرت ماژول

خود حالا می‌خوایم ی ماژول رو مایگریت کنیم. برای مثال ماژول زیر رو در نظر بگیرید:

مثال Module
مثال Module


۱. اگر ماژول دارای متدهایی هست که ViewModelی رو محیا میکنن اون متد رو حذف می‌کنیم و میریم سر کلاس ViewModel انوتیشن HiltViewModel@ رو اضافه می‌کنیم:

حذف متد نشان شده
حذف متد نشان شده


۲. اگر ماژول دارای متدی هست که ViewModelFactoryی رو محیا میکنه اون رو هم پاک می‌کنیم:

حذف متد نشان شده
حذف متد نشان شده


۳. اگر ماژول دارای متدهایی هست که انوتیشن ContributesAndroidInjector@ دارند میریم سراغ کلاسی که داره return میکنه(Activity, Service, Fragment یا BroadCastReceiver) و کارهای زیر رو روش انجام میدیم:

۱. بالای کلاس انوتیشن AndroidEntryPoint@ می‌زاریم.

۲. اگر از ViewModelی در این کلاس استفاده شده که به هیلت مایگریت شده یا میخواد مایگریت بشه و برای ساختنش از ViewModelFactoryیی که Inject شده استفاده کردیم دیگه لازم نیست از ViewModelFactory استفاده کنیم. پس مثلا این:

قبل
قبل

میشه این:

بعد
بعد


به صورت پیش فرض برای ساختن ViewModelها از defaultViewModelProviderFactory استفاده میشه و خود هیلت defaultViewModelProviderFactory رو تغییر میده تا ViewModelها رو بتونه Inject کنه. اگر جایی به ViewModelFactory نیاز داشتیم(مثلا وقتی از StoreOwner غیر پیش فرض استفاده میکنیم)، میتونیم از defaultViewModelProviderFactory استفاده کنیم.


نهایتا متد دارای ContributesAndroidInjector@رو هم از ماژولی که روش کار می‌کردیم پاک می‌کنیم:

حذف متد نشان شده
حذف متد نشان شده


۳. اگر ماژول دارای متدی هست که Workerی رو می‌سازه مراحل زیر رو روش انجام میدیم:

۱. میریم سراغ Worker و بالاش انوتیشن HiltWorker@ می‌زاریم.

۲. اگر دارای اینترفیس با انوتیشن AssistedInject.Factory@ هست اینترفیس رو پاک میکنیم.

۳. متد مربوط به Worker در ماژول رو هم پاک میکنیم.

۴. انوتیشن AssistedModule@ رو از بالای ماژول پاک می‌کنیم.

تغییرات Worker (مراحل ۱ و۲)
تغییرات Worker (مراحل ۱ و۲)


برای ادامه ماژول زیر رو به عنوان مثال در نظر بگیرید:


۵. اگر بعد انجام مراحل قبل ماژول خالی از متد شد پاکش می‌کنیم. در غیر این صورت بالای ماژول بجز انوتیشن Module@ انوتیشن InstallIn@ رو با کامپوننت متناظرش اضافه می‌کنیم.

در ماژول بالا داریم ی سرویس Retrofit رو محیا میکنیم که نمیشه حذفش کرد. به این ماژول در Activity نیاز داریم، در نتیجه InstallIn@رو با پارامتر ActivityComponent بهش اضافه می‌کنیم:

افزودن InstallIn
افزودن InstallIn


۶. اگر ماژول دارای متدهای هست که Custom Scope داره Scopeش رو به Scope متناسب با کامپوننت داخل InstallIn@تغییر می‌دیم. Scope متناسب با ActivityComponent اسکوپ ActivityScoped@ هست(بالاتر تو جدول لیست componentها و scopeهای جفت باهاشون رو آووردیم) :

افزودن ActivityScoped
افزودن ActivityScoped

توجه داشتیم باشید که اگر از Scope اشتباهی استفاده کنید پروژه بیلد نمیشه. مثلا نباید وقتی از InstallIn(ActivityComponent::class)@ استفاده کردید، از اسکوپ FragmentScoped@ بالای متدی استفاده کنید.


۴- حذف Scope

همونطور که بالاتر در مثال کامپوننت دیدید ممکنه برای کامپوننت از Custom Scope استفاده کرده باشیم. مثلا اینجا از DiscountCodeScope استفاده شده:

Custom Scope کامپوننت دارای
Custom Scope کامپوننت دارای


گفتیم که در هیلت معمولا نیازی به Custom Scope نداریم. در نتیجه باید کارکردهای Custom Scope مورد نظر رو پیدا کرده و با Scope مناسب جابه جا کنیم. مثلا روی ی کلاس که با constructor Injection ساخته میشه هم از DiscountCodeScope استفاده کرده بودیم که به ActivityScoped تغییرش می‌دیم:

استفاده از scopeهای پیش فرض هیلت در constructor injection
استفاده از scopeهای پیش فرض هیلت در constructor injection


همه ی استفاده‌های Custom Scope مورد نظر رو پیدا می‌کنیم و با scope مناسب جابه‌جاشون می‌کنیم تا استفاده ای از اون نمونه. بعد می‌تونیم خود اون scope رو هم پاک کنیم.



5- دیگر نکات

دوتا نکته مونده که بهتون میگم:

۱- اگر در ی گریدل ماژول هم از دگر و هم از Hilt استفاده می‌کنید باید به Hilt بگید که با Moduleهای دگری کاری نداشته باشه و اونها رو پروسس نکنه. این کارو با اضافه کردن یک flag در فایل buildمون انجام می‌دیم:

https://gist.github.com/DHosseiny/03e6c172f91306b9d6ef11fb6bc64f0c


۲- اگر کلاس Applicationتون interface Configuration.Provider رو پیاده‌سازی می‌کنه و میخواید همه Workerها رو از دگر به هیلت مایگریت کنید باید کلاس HiltWorkerFactory رو inject کنید و در getWorkManagerConfiguration ازش استفاده کنید:

https://gist.github.com/DHosseiny/406443373d8c495fda09026711f293b0


ولی اگر میخواید قسمتی از workerها رو به هیلت مایگریت کنید و از ی همچین کلاسی برای WorkerFactory در دگر استفاده می‌کنید، باید WorkerFactoryیه هیلت رو هم با دگر ترکیب کنید. این کلاس رو برای این منظور نوشتم:

https://gist.github.com/DHosseiny/c3737d933133db10845bb2bd82de6770

حالا کلاس Application رو اینجوری تغییر بدید تا از AggregatorWorkerFactory استفاده کنه:

https://gist.github.com/DHosseiny/f13c0a2fd00b327912d3dcacaf4d7124


احتمال داره ی سری متد با انوتیشن Binds@ در ماژول هاتون باقی مونده باشه. من پیشنهادم اینه که از لایببری hilt-binder استفاده کنید که خودش براتون این متدهای Binds@ رو تولید میکنه. این لایببری ی Hilt-extention هست و با استفاده از انوتیشنی که شما سر کلاساتون میزنید متوجه میشه که باید برای اون کلاس ماژول و متد Binds@ تولید کنه تا کار شما راحتتر بشه.


و تمام :) تونستیم ی کامپوننت رو به هیلت مایگریت کنیم.




مطالعه بیشتر:

https://dagger.dev/hilt/migration-guide.html

https://developer.android.com/codelabs/android-dagger-to-hilt

daggermigration
شاید از این پست‌ها خوشتان بیاید