ساخت اپ اندروید ماژولار
خیلی وقت بود که دوست داشتم روی اپهای ماژولار کار کنم، این دوست داشتن مقدمهی یه هفتهای مطالعه و تست بود که در نهایت خروجیش تبدیل به ریپوی AndroidModularization توی گیتهاب شد. در کنار هدف اصلی (اپ ماژولار) یه هدف فرعی هم داشتم که یه نمونه کد authentication با قابلیت رفرش شدن توکن برای وبسرویس moviesapi.ir درست کنم. اگر بخوام خلاصش کنم میشه این پروژه یه اپ ماژولار هست که از قابلیتهای وبسرویس moviesapi.ir استفاده میکنه.
توی توسعه این پروژه از کتابخونههایی مثل Rxjava ,Dagger2 ,Retrofit, Room , Databinding و … استفاده شده ولی تمرکز اصلی روی ساخت اپ ماژولار با استفاده از Dagger2 بوده. یعنی اگر میخواید RxJava یاد بگیرید، توی این پروژه نباید دنبال Best Practiceهاش باشید.
چرا اپ ماژولار خوبه؟
مهمترین دلیلی که خیلی از افراد و شرکتها به سمت اپهای ماژولار میرن، افزایش سرعت بیلد هست. توی پروژههای بزرگ این قضیه یه مشکل خیلی حاد میشه. مورد بعدی اینه که توسعهی پروژه رو در آینده خیلی راحتتر میکنه، بطور مثال هر ماژول رو یه تیم میتونه جداگونه توسعه بده و ویژگیهای جدید بهش اضافه کنه بدونه اینکه کاری به بقیه ماژولها داشته باشه.
چالشهای توسعه اپ ماژولار چیه؟
مهمترین چالشهایی که توی ساخت این پروژه بهش برخورد کردم این سه مورد بود:
۱- جا به جایی کاربر بین صفحاتی که در ماژولهای متفاوت هستند (اکتیویتی به اکتیویتی یا فرگمنت به فرگمنت یا …)
۲- استفاده از Dagger2
۳- مدیریت نسخهی کتابخونههایی که به عنوان dependency استفاده میشن
ساختار پروژه بهمراه وابستگیها در یک نگاه!
پروژه سه بخش اصلی داره:
۱- بخش Core: کلاسهایی که در کل اپ مشترک هستند. مثلا کلاسهای Base، وبسرویسهایی که توی کل پروژه نیاز هست، کلاسهای مرتبط با Navigation و … . البته درصورت نیاز میشه core رو تبدیل به ماژولهای جداگونه کرد.
۲- بخش Features: بین Core و App قرار دارند. هر کدوم از featureها میتونند برای خودشون درخواستهای وبسرویس جداگونه یا دیتابیس داشته باشند. برای نمونه توی این پروژه سه تا Feature داریم.
الف) authentication – داخلش صفحات ورود کاربر و ثبتنام هس
ب) main – داخلش صفحهی نمایش لیست فیلمها و جزییاتش هست
ج) search – داخلش صفحهی جستجو هست که تاریخچهی جستجوهای قبلی رو نشون میده. این Feature برای خودش دیتابیس داخلی داره.
۳- بخش App: ماژولی هس که باعث میشه apk ساخته بشه. تنها ماژولی هست که به همه ماژولهای دیگه وابستگی داره. توی اندروید حتما نیازه همچین ماژولی داشته باشیم که از نوع Application باشه.
حل کردن چالشها!
چالش جا به جایی بین صفحات (Navigation)
اولین نکته توی حل چالش Navigation اینه تصمیم بگیرید که آیا قراره Featureیی داشته باشید که فقط فرگمنت توش باشه یا نه؟ منظور اینه اگر همه Featureها حداقل یه اکتیویتی داشته باشند، نقطهی ورود به هر ماژول (EntryPoint) میتونه از اون طریق باشه. یعنی اگر کاربر از صفحهای توی Main میخواد به Search بره، توی Search یه اکتیویتی باشه تا کاربر به اون بره.
مزایا و معایب این صورت هست:
مزایا: توسعه پروژه خیلی راحتتر میشه. برای ارتباط بین Featureها میشه یه کلاس Naivgation درست کرد که با استفاده از deeplink بدونه وابستگی به ماژول خاصی کار انتقال کاربر بین Featureهارو انجام بده. همه Featureها به کلاس Naivgation وابسته هستند و خود Navigation به جایی وابسته نیست. در نتیجه درگیر circular dependency نمیشم.
معایب: مجبورید برای هر Feature یه اکتیویتی درست کنید که خیلی اصلا نیاز نیست.
همونجور که گفتم پیادهسازیش راحته مثل زیر میشه:
کد کلاس Navigation هم این مدلی میشه.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class MainNavigator { private Context context; public MainNavigator(Context context) { this.context = context; } public void openMain() { Intent startActivityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("dsvs://main/movies")); context.startActivity(startActivityIntent); } } |
چون uri استفاده میشه نیاز به این نیست navigation برای انتقال کاربر به اکتیویتی داخل Main به اون وابسته باشه. خود اندروید اکیتیویتی مناسب رو پیدا میکنه.
حالا اگر دوست داشته باشید Featureیی داشته باشید که فقط داخلش فرگمنت باشه قضیه پیچیدهتر میشه. همشم زیر سر circular dependencyهست!! توی این حالت کد Navigation این شکلی میشه:
1 2 3 4 5 6 7 8 9 10 |
public class MainNavigator { public MainNavigator() { } public Fragment openMain(String query) { return MoviesFragment.newInstance(query); } } |
این کد باعث میشه یه وابستگی بین navigation با ماژول اون فرگمنت پیش میاد که باعث circular dependency میشه.
با درست کردن یه interface که Search داخلش مشخص میکنه که بقیه ماژولها چطوری میتونن باهاش ارتباط داشته باشن (درواقع EntryPointهاشو تعیین میکنه) این circular dependency حل میشه.
البته پیاده سازیش به این تمیزی نیست. اگر برید توی کد ببینید متوجه میشید چی میگم D: چون تنهایی جایی که میشده این کلاسهارو new کنم و توی ماژولهای مختلف بهش دسترسی داشته باشم بدونه اینکه وابستگی مستقیم درست کنم، کلاس application بوده. یه اینترفیس EntryPointHolder دارم که کلاس application اونو پیاده میکنه، در نتیجه چون همه ماژولها به application context دسترسی دارن میتوننن آبجکتهای EntryPoint رو بگیرن.
غیر از circular dependency یه مشکل دیگم هست. فرض کنید یه اکتیویتی توی ماژول A داریم که داخل خودش یه فرگمنت از همون ماژول A نشون میده (شکل شماره ۱). کاربر روی دکمه میزنه و فرگمنت ۱ از ماژول B توی اکتیویتی ماژول A نشون داده میشه (شکل شماره ۲). حالا بدونه اینکه فرگمنت ۱ از ماژول B خبر داشته باشه (چون کلا نمیدونه توی کجا داره نمایش داده میشه) ما میخوایم از فرگمنت ۱ به فرگمنت ۲ همون ماژول B بریم! (شکل شماره ۳)
برای اینکار از همون ایده که توی SingleAcitivityPattern استفاده کردم، کمک گرفتم. یعنی هر Acitvity و Fragmentیی که داخل خودش میخواد یه سری فرگمنت رو مدیریت کنه باید اینترفیس HasNavigator رو پیاده سازی کنه. بعدا اگر جایی شبیه همین مثالی که گفتم گیر کردیم، فرگمنت ۱ ماژول B با استفاده از کلاس NavigationManager به نزدیکترین navigator که مسئول مدیریتش بوده (اینجا میشه naivgatorیی که توی Acitivity ماژول A هست) دسترسی پیدا میکنه. بعد به اون میگه الان باید فرگمنت ۲ از ماژول B نشون داده بشه. کد کلاس navigationManager رو ببینید متوجه میشید که این قضیه سلسله مراتبی چطوری هندل شده.
شکل شماره ۱
شکل شماره ۲
شکل شماره ۳
چالش Dagger2
مدلی که از dagger2 توی این پروژه استفاده کردم، این شکلی هست که یه application component داریم که توی ماژول core با اسکوپ singleton هست. ماژولهای Feature هم چون به core وابستگی دارن میتونن ازش استفاده کنند. اول میخواستم هر ماژول subcomponent اون application component بشه! اما باز مشکل circular dependency پیش اومد. چون subcomponent توی dagger2 اینجوریه که application component باید یه وابستگی با subcomponentش داشته باشه که توی این پروژه چون هرکدوم ماژول مختلف هستند، این circular dependency پیش میاد. واسه همین بین کامپوننتهای هر Feature با application component از component dependency استفاده کردم. ولی اگر مثلا توی خود Feautre یه اکتیویتی بود که یه سری فرگمنت داشت، رابطشون رو همون subcomponentیی درست کردم.
چالش مدیریت نسخهی کتابخونهها
از یه روش ساده استفاده کردم (راه حل بهترش استفاده از kotlin+buildsrc هست) یه فایل گریدل به اسم libraries.gradle ساختم و همه کتابخونههایی که نیاز دارمو اونجا نوشتم. بعد ماژولها اگر میخوان کتابخونهی رو به عنوان dependency استفاده کنند، بجای اینکه خودشون مستقیم نسخه و اسم کتابخونه رو بنویسه از libraries.gradle استفاده میکنه.
نتیجه نهایی
پروژهای که با این روش ساختم رو روی توی گیتهاب گذاشتم، از طریق لینک زیر میتونید بهش دسترسی داشته باشید. قرار هست از این کد با کمی تغییر به عنوان پایهی پروژهای استفاده کنم، حتما در آینده سعی میکنم در مورد اینکه چقدر بهم کمک کرده یا معایبی داشته یه مقاله بنویسم. اگرم کسی پیشنهاد یا سوالی داره، ممنون میشم بهم بگه تا در موردش بیشتر بحث کنیم.