ماژولار کردن پروژه اندروید

خب توی این مقاله می‌خوام توضیح بدم که چطور میتونید پروژه ی اندرویدتون رو ماژولار کنید.اولین سوالی که به ذهنتون احتمالا بیاد اینه که اصلا چرا باید پروژمون رو ماژولار کنیم ؟ شاید با خوندن این مقاله احساس کنید که کد زیادی برای پروژه می نویسید در حالی که میشه با کد کمتر این کار رو انجام داد ولی اگه پروژتون از یک حد بزرگ تر بشه و یا نیاز به اضافه کردن فیچر بشه یا این که با یه کس دیگه پروژه رو جلو ببرید بهتر متوجه میشید که چرا ماژولار کردن پروژه میتونه توی طولانی مدت براتون خوب باشه.ماژولار کردن باعث میشه سرعت بیلدتون زیاد بشهوقتی کدتون رو به چندین ماژول تقسیم میکنید موقع تغییر کد فقط ماژولی که کدش رو عوض کردید و ماژول هایی که بهش depend میکنن بیلد میشن. هچنین میتونید امکان gradle parallel رو فعال کنید تا ماژول هایی که مستقل از هم هستن موازی هم دیگه بیلد بشن، فقط کافیه این آپشن org.gradle.parallel=true رو داخل فایل gradle.properties بزارید.ماژولار کردن به صورت اتوماتیک جلوی خرابکاری و به هم زدن کد رو میگیرهتوی ماژولار کردن کد هایی که قرار نیست از هم دیگه خبر داشته باشن هم دیگه رو نمیبینن (کلاً بعضی ماژول ها به هم دیگه دسترسی ندارن توی ادامه ی پست بیشتر توضیح میدم) و این باعث میشه که الکی فقط به خاطر اینکه کد کمتری بنویسید یه کلاس رو هزار جا استفاده نکنید! شاید فکر کنید که هر چه کد کمتر بنویسیم بهتره ولی همه جا این حرف صادق نیست، مثلاً اگه یه کلاس دارید که state های صفحه ی اسپلش رو نگه میداره و توی صفحه ی خانه هم state ها دقیقاً مثل اسپلش هستند نباید از یک کلاس برای هردو استفاده کرد چون ممکنه بعداً یه state به خانه اضافه بشه که توی اسپلش نیست و مجبور میشین که کدتون رو ریفکتور کنید!(حالا فرض کنید که یه کلاسی با همین شرایط توی 10 صفحه استفاده بشه…)ماژولار کردن باعث میشه که توی تیم های چند نفره راحت‌تر کار کنیدچون ماژول ها بر اساس feature تقسیم میشن افراد به راحتی میتونند روی ویژگی‌هایی که قراره توسعه بدن کار کنند و چون معلوم دقیقاً توی کدوم ماژول کار میکنند احتمال کانفیلیکت خیلی کم میشه، البته وقتی ماژول های اصلی یا همون core رو تغییر بدید شاید کانفیلیکت به وجود بیاد ولی با میزان کانفیلیکت اینطور خیلی کمتر میشه!در این مقاله سعی بر این هست که فقط لایبرری و تکنولوژی هایی که لازمه رو استفاده کنیم، تا تمرکزمون بیشتر روی ماژولار کردن باشه، هرچند توی مقاله‌های بعدی تصمیم دارم همین پروژه رو تکمیل ترش کنم و بقیه چیز هارو (Compose,Test,Flow,…) بهش اضافه کنم، برای این مقاله کافیه پروژه رو کلون کنید و روی برنچ simple-modular چکآوت کنید! https://github.com/mobinyardim/Rick-and-Morty.git چیزهایی که قراره توی این پروژه استفاده کنیم:Jetpack Navigation ComponenetMVVM ArchitectureRepository Design patternPaging library 3HiltRoomLiveDataاصلاً ماژول چی هست؟ (اصلاً ماژول نمنه دی؟)توی ساده‌ترین تعریفی که میتونم بگم داخل فولدر پروژه تون پوشه ای هست به اسم app که بهش میگیم ماژول اپ که از چند قسمت تشکیل شده، حتماً توش یه فایل build.gradle هست که بهمون میگه این پروژه به کدوم لایبرری ها و ماژول ها وابستگی داره ( یا همون depend میکنه)، یه پوشه داریم که کد های جاوا یا کاتلینمون توش هست و یه پوشه رسورس که رسورس هامون توشه، اگه ماژول اندروید باشه توش فایل manifest هم داریم.برای اینکه یه ماژول جدید به برنامه مون اضافه کنیم کافیه که File>new>new module رو بزنیم توی پنجره ای که باز میشه، اطلاعات رو پر میکنیم و فنیش رو میزنیممعمولاً پترن من برای نام پکیج ماژول جدید به این شکل هست ${نام پکیج برنامه}.${نام ماژول}بعد از اینکه ماژول جدید باز کردیم:داخل پکیج myLibrary میتونید کدهاتون رو قرار بدید ولی سؤال اصلی اینه که بعد از اینکه کدی رو داخل این پکیج قرار دادیم چطور بهش توی ماژول اپ دسترسی پیدا کنیم؟خب کافیه فایل build.gradle ماژول اپ رو باز کنیم و داخل dependencies این کد رو قرار بدیم:dependencies{ implementation project(":mylibrary")}به این صورت ماژول اپ به ماژول mylibrary دیپند(depend) میکنه! یعنی توی کد های اپ میتونید کد های mylibrary رو ببینید.نکته: وقتی داخل ماژول A لایبرری X رو به این صورت ایمپلنت میکنیمimplementation "com.something:someLibs:x.x.x"اگه ماژول ‌B به ماژول A دیپند کنه، دیگه ماژول B کدهای لایبرری X رو نمی‌بینه، اگه می‌خواهید هر ماژولی که به A دیپند میکنه لایبرری X رو هم ببینه باید کدتون رو به شکل زیر تغییر بدیدapi &quotcom.something:someLibs:x.x.x&quotنحوه‌ی تقسیم‌بندی ماژول هااگه کم کم از بحث‌های اولیه ماژولار بیایم بیرون حتماً براتون سؤال شده که خب حالا قراره کد هامون رو بر چه اساسی تقسیم کنیم؟ماژول های ما به دو دسته تقسیم میشن:1.ماژول های فیچر (feature)2.ماژول های لایبرریماژول‌های فیچر همونطور که از اسمشون معلومه یک ويژگی خاصی از برنامه رو نگه میدارن مثلاً صفحه‌ی اصلی برنامه، صفحه‌ی لاگین و غیره….ماژول های لایبرری شامل کد هایی هست که به صورت اشتراکی بین تمام فیچرها مورد استفاده قرار میگیره مثلاً ماژول نتورک، ماژول charactersRepository و هر ماژول دیگه‌ای که بین فیچر ها یا حتی خود لایبرری ها به صورت اشتراکی استفاده میشه!نکته‌ی کلیدی این هست که ماژول های فیچر نباید هم دیگه رو ببینند یا همون اینکه بهم دیپند کنند!ماژول‌های لایبرری میتونن هم دیگه رو ببینند ولی نباید از فیچرها خبری داشته باشند.خب اینا رو به یاد داشته باشید که تا پروژه‌ی سمپلی که قراره باهم بنویسیم رو بهتون توضیح بدم و این نکته‌هایی که گفتم رو پیاده‌سازی کنیم.شرح پروژهتوی ‌پروژه مون از API های سایت rickandmortyapi.com استفاده میکنیم. توی صفحه‌ی اصلی برنامه قراره یه Viewpager با یه TabView باشه که دوتا صفحه توش داره یکی کل کارکتر هارو نشون میده با امکان جستجو و صفحه‌ی دوم کاراکتر هایی که سیو شدن رو نشون میده. روی هر کاراکتری که کلیک بشه میره به صفحه‌ای که جزئیات کارکتر رو نشون میده!توی این مقاله در نظر گرفته شده که mvvm, hilt بلد هستید. داخل پوشه اصلی پروژه پوشه ای به اسم features باز میکنیم و برای هر کدوم از صفحات Main,Characters,SavedCharacters,CharacterDetail یه ماژول میسازیم برای هر کدوم از صفحات فرگتمنت میسازیم و کد ui اش رو میزنیم.پیاده سازی لایه Dataیه ماژول برای نتوورک میسازیم، کار این ماژول مهیا کردن سینگلتون برای رتروفیت هست پس داخلش یه اینستنس از رتروفیت میسازیم و با استفاده از هیلت سینگلتونش میکنیم. https://gist.github.com/mobinyardim/562cda2a9f1f792399809418c4cac6a9 من برای json converter از کتابخانه‌ی moshi نسخه‌ی codegen استفاده میکنم دلیلشم این هست که moshi برخلاف gson برای هرکلاس یه آداپتر میسازه و با استفاده از اون آداپترها جیسون رو به مدل تبدیل میکنه ولی gson از reflection استفاده میکنه که ممکنه پرفورمنس خوبی نداشته باشه!یه ماژول به اسم models داخل پوشه libraries>core میسازیم تا مدل‌های domain رو توش بسازیم. به صورت خلاصه اگه بگم domain model مدلی از دیتا هست مستقل از دیتابیس،نتوورک یا هر سورسی که ازش دیتا میگیریم، دیتایی که از سورس های مختلف میگیریم رو آخر سر تبدیل به domain model میکنیم تا به شکلی درباید که بتونیم ازش استفاده کنیم. استفاده از این روش بهتون این مزیت رو میده که اگه با فرمت دیتایی که از سرور میاد حال نکردید یا با بیزینس لاجیکتون یکی نبود تبدیلش کنید به فرمتی که مناسب تر هست براتون. ضمنا موقع ریفکتور کردن کارتون رو هم آسون تر میکنه! مثلا در مورد مدل خودمون (character) چیزی که ما از سرور به عنوان جنسیت میگیریم به صورت string هست ولی ما تبدیلش میکنیم به enum. https://gist.github.com/mobinyardim/7bfc4f4bced54e38aedfe04fa560291f چون تنها موجودیت پروژمون تا حالا فقط کاراکتر هست حالا باید براش یه repository بسازیم، داخل پوشه‌ی libraries>core ماژول charactersRespository رو ایجاد میکنیم. برای repsitory هامون بهتره یه interface بسازیم سپس اون اینترفیس رو implement کنیم. با این کار بعدا موقع تست کارتون آسون میشه چون راحت میتونید ازش یه ورژن فیک بسازید و اونو به ویو مدلتون بدید تا موقع تست کردن، تستتون فقط به ویو مدلتون وابسته بشه! https://gist.github.com/7700d5ac6e18d015b32ac1c7f1b4a4ab.git برای سیو کردن کاراکترها لازمه که دیتابیس داشته باشیم به همین منوال داخل پوشه‌ی libraries>core یه ماژول هم برای دیتابیس میسازیم با یک جدول برای موجودیت کاراکتر. https://gist.github.com/mobinyardim/ac6d8a4eb0873f6d5723f12341df20be حالا وقتشه برگردیم به ماژول characterRepository اول از همه برای دسترسی پیدا کردن به کدهای ماژول نتورک باید ماژول characterRepository بهش دیپند کنه، پس به گریدل اضافه‌اش میکنیم و داخل ماژول یه اینترفیس برای گرفتن دیتا از اینترنت میسازیم که دوتا فانکشن داره یکی برای گرفتن تکی کارکتر یکی برای گرفتن لیست کاراکتر ها. https://gist.github.com/mobinyardim/c001f84799a1a330db33e4da82c596c4 کلاس PagingSource رو برای مدل کاراکتر میسازیم https://gist.github.com/c19df45734da72a510100c7e605da598.git الان وقتشه که برگردیم و اینترفیس CharacterRepository رو پیاده سازی کنیم. اگه تعریف ریپازیتوری رو مرور کنیم کارش اینه که دیتایی که میخواییم رو نسبت به پیاده سازیش از سورس های مد نظر بهمون بر گردونه پس هر کاری که روی دیتا قرار انجام بشه مسئولیت ریپازیتوری هست. پس تبدیل کردن داده به نوع domain و چک کردن اینکه این کاراکتر قبلا سیو شده یا نه باید توی پیاده سازی ریپازیتوریمون انجام بشه: https://gist.github.com/2fbee2fa0296a5786f7dd11db80a122e.git پیاده سازی لایه Presentationاولین چالشی که در نوشتن کدهای این قسمت دیده میشه اینه که فرض کنید کد های صفحه های characters و saved-characters رو نوشتید و میخوایید اونارو داخل یه تب ویو توی ماژول main نشون بدید. اول مقاله با هم قرار داد کرده بودیم که نباید ماژول های فیچر از هم خبر داشته باشن پس چطور میشه این مشکل رو حل کرد؟ (قبل از ادامه دادن میتونید خودتون رو به چالش بکشید و یکم فکر کنید)متودولوژی که برای حل این مسئله در نظر میگیریم اینه که ماژول main به برنامه میگه من یه صفحه میخوام که بتونم کاراکترهارو نشون بدم و برنامه بهش اون صفحه رو بر میگردونه! پس صفحه‌ی main میدونه ما یه صفحه داریم که میتونه کاراکتر هارو نشون بده ولی نمیدونه چطور پیاده سازی شده! ابتدا یه ماژول به اسم screens داخل libraries می‌سازیم و کدهای زیر رو توش قرارمیدیم: https://gist.github.com/cb0a8aa58a16e9e5a16762f539d5e645.git ماژول characters رو به screen دیپند میکنیم و سپس اینترفیس CharactersFragmentProvider رو پیاده سازی کرده و بهش bind میکنیم https://gist.github.com/5b1cfe949e7d27f7e71036df6089980d.git همین کار رو برای saved characters هم انجام میدیم. بر میگردیم ماژول main رو به screens دیپند میکنیم. با این کار ماژول main خبردار میشه که ما یه ویژگی داخل برنامه داریم که میتونه کاراکتر هارو نشون بده ولی نمیدونه که کی پیاده سازیش کرده. داخل کدها میتونیم هرجا که لازم شد اینترفیس CharactersFragmentProvider رو اینجکت کرده و با صدا زدن متود newInstance موجودیتی از فرگمنتی که این ویژگی رو پیاده کرده بگیریم و استفاده کنیم که کدش به صورت زیر میشه: https://gist.github.com/aa266ffc478feeaa60eae221c86b7bbd.git چالش دوم اینه که داخل هر کدام از صفحه‌های characters و savedCharacters ما باید با کلیک کردن روی هر آیتم بتونیم به صفحه‌‌ی character detail بریم و نباید به این ماژول دیپند کنیم نوع حل این مسئله دقیقا مثل مسئله قبل هست.چون صفحه‌ی chracters-detail یه صفحه‌ی جداگانه هست داخل ماژولش براش یه navigation ایجاد میکنیم و داخل naivagtion اصلی اونو ایمپورت میکنیم.برای navigate کردن به این صفحه نیاز داریم که آیدی صفحه رو بدونیم و اینو بدونیم که برای رفتن به این صفحه چه چیزی رو باید بفرستیم ؟یه ماژول به اسم route-contracts میسازیم و کد زیر رو توش قرار میدیم: https://gist.github.com/378610ff59c354e6a91035fe0b2d6b96.git این اینترفیس رو داخل ماژول character-detail پیاده سازی کرده و به اینترفیسش بایند میکنیم: https://gist.github.com/cd0d1054e90389f1a8874321804bbd21.git الان از هر صفحه‌ای که قراره به این صفحه navigate کنیم کافیه اینترفیس CharacterDetailRouteContract رو اینجکت کرده و با فراخوانی متد show به این صفحه navigate کنیم.این یه پروژ‌ه‌ی خیلی ساده ماژولار هست که کلی نواقص داره و فقط جهت آشنایی با اصول اولیه ماژوالار کردن پروژه هست! هر پیشنهادی که داشتید رو میتونید توی کامنت ها برام بنویسید و با لایک کردن و شیر کردن این پست میتونید بهم انرژی بدید تا این پروژه رو تکمیل ترش کنیم و از هم دیگه یاد بگیریم ✌?❤️