پیاده سازی معماری Clean MVVM (اندروید)

قبل از شروع مقاله این نکته رو توجه کنید ، من در این مقاله بر یک سری مقاله های دیگه متکی بودم که قبلا در ویرگول گذاشتم ، دو مقاله در مورد Hilt و چند سری مقاله در مورد Coroutines ، همین طور باید MVVM رو بدونید ، البته در مورد MVVM مقاله ای نذاشتم منتها یک سری ویدئو در youtube گذاشتم (که البته تمام نشده) که حتما حتما حتما باید قبل از اینکه مقاله رو بخونید اون ها رو خونده باشید ، البته در حین مقاله هر جا که لازم شده رفرنس به اون مقالات دادم .لیست مقالات و ویدئوی هایی که باید قبلش خونده یا دیده باشید :مجموعه Coroutines : مقاله 1 ، مقاله 2 ، مقاله 3 ، مقاله 4 ، مقاله 5مجموعه Hilt : قسمت اول Hilt و قسمت دوم Hiltمجمعه MVVM : لینکدر این مقاله به شما آموزش میدم چطوری معماری MVVM تون رو حالت Clean در بیارید !اول از همه سوال اصلی اینه که اصلا ما چرا باید در برنامه نویسی مون از معماری ها استفاده کنیم ؟معماری نرم افزاریک مثالی که اکثرا می‌زنم اینه ، یک سری ها میرن مهندسی عمران می‌خونند ، یعنی معماری سازه ها رو پیاده می‌کنند ، یک سری مهندسی مکانیک می‌خونن ، یعنی معماری یک دستگاه (ماشین یا ...) رو پیاده سازی می‌کنند ، حالا یک مهندس کامپیوتر چی ؟ برای چی بهش می‌گن مهندس ؟ مهندس از واژه "هندسه" میاد ، یکی از شاخه های علم مهندسی نرم افزار برنامه نویسیه ، اگه شما صرفا بلد باشید به یه زبونی کد بزنید و یه فریم ورکی رو بلد باشید به شما نمیشه گفت مهندس ، بهتون میگن "کد زن" ، اما اگه بلد باشید مثل یک "مهندس" کدهاتون رو لایه بندی ، کامپوننت بندی و ... کنید و یک ساختار منظم و معماری خوبی رو برای کدهاتون بچینید اون وقته که می‌تونیم کلمه "مهندس" رو بهتون بگیم ! شما هم معماری می‌چینید اما برخلاف مهندس عمران ، مکانیک یا ... جنبه فیزیکی نداره و همه چیز رو به صورت مجازی پیاده می‌کنید.هدف از معماری سهولت خوانایی کد ، افزایش قابلیت نگه داری (Maintenance) ، کم کردن تلاش برنامه نویس برای تغییرات/افزودن فیچرها ، قابلیت تست پذیری و ... هست .معماری Clean https://media.giphy.com/media/3zn52gZBFbb21rk2wr/giphy.gif معماری Clean ربطی به MVVM نداره ، بلکه یک روش پیشنهادی از سمت Robert C. Martin (معروف به عمو باب) هست ، ما می‌تونیم این معماری رو با MVVM ترکیب کنیم . در مقاله آقای مارتین گفته شده که یکی از مشکلات برنامه نویسان separation of concerns هست و ما این امر رو از طریق لایه بندی عناصر ایجاد می‌کنیم ، ایشون این پنج مورد رو به عنوان مواردی که باید در نظر گرفت بررسی کردند :معماری شما به Framework وابسته نباشه ، یعنی در هر Framework ای بشه پیاده ش کردتست پذیره باشه بدون اینکه نیاز به UI ، دیتابیس و ... داشته باشهوابسته به UI نباشه (توضیح داده شده که طوری برنامه رو بنویسید که لایه UI جدا باشه که این طوری بشه به فرم های مختلف UI متصل بشه)وابسته به دیتابیس نباشه (دلیلش مثل مورد 3 هست ، بتونیم از هر نوع دیتابیسی استفاده کنیم و لایه دیتابیس جدا باشه)وابسته به external agency ها نباشه ، که در توضیح این نوشته شده لایه بیزینیس لاجیک شما (business rules) چیزی در مورد خارج از دنیای خودش ندونه (external agency باز هم همون مورد 3 و 4 و ... میشه) .حالا وقت اینه که در مورد لایه های مختلف بدونیم ، به شکل زیر نگاه کنید :شمای کلی معماری clean بر پایه مقاله رابرت مارتینمعماری Clean بر 4 لایه استوار شده ، لایه خای داخلی نباید در مورد لایه های بیرونی اطلاعی داشته باشن و نمی‌خوایم هیچ عاملی از لایه های بیرونی بر لایه های درونی اثر بذاره (یعنی مثلا UI از Controller اطلاع داره ولی برعکسش صادق نیست) .لایه Entities(زرد)طبق مقاله مرجع ، این لایه می‌تونه یک object به همراه توابع درونش باشه و یا یک ساختار کلی و توابع مربوط به اون. این لایه مستقل از Framework هست ، یعنی چی ؟ یعنی مثلا ما که توسعه اندروید انجام می‌دیم در این لایه صرفا از کاتلین و جاوا استفاده می‌کنیم و سعی ‌می‌کنیم از خودِ SDK اندروید در این لایه استفاده نکنیم .نکته : همه این مسائل Concept هستند ، یعنی چی ؟ یعنی هر کسی یک برداشتی از این قضیه می‌کنه و پیاده سازی های مختلفی که از Clean می‌بینید ممکنه با هم تفاوت هایی بکنند اما درصد بالایی از قضیه در همه شون یکیه !لایه Use Case(قرمز)این لایه در واقعی کانالی از لایه Entities به لایه بعدیه ، یعنی data flow (مسیر داده) رو مشخص می‌کنه . Use Case در قسمتی که ما پیاده می‌کنیم میشه interact (تعامل) های کاربر با اپلیکیشن شما که سر تایمش توضیح می‌دم .لایه Interface Adapters(سبز)وظیفه این لایه اینه که مدل ها و داده هایی که از طریق دو لایه قبلی میان رو به فرمی در بیاره که قابل فهم برای لایه بعدیش بشه . به طور کلی معنی کلمه Adapt یعنی "وفق دادن" که از معنیش همون جمله اول رو میشه برداشت کرد .لایه Frameworks (آبی)این لایه ، لایه نهایی که شامل دیتابیس ، UI و ... هست .خب این تعاریف بالا خیلی کلی هستن ، ما نیاز داریم این تیپ معماری رو با MVVM در اندروید تلفیق کنیم .در اکثر پیاده سازی هایی که Clean MVVM هستند و برای اندروید صورت گرفتند کدها به سه لایه اصلی (و چند لایه فرعی) تقسیم می‌شن ، . این لایه ها به ترتیب زیر هستند :لایه Data :ما در این لایه تعاریف انتزاعی مربوط به داده ها رو می‌نویسیم ، "مربوط" یعنی چی ؟ یعنی مثلا این دیتا از کجا میاد ؟ از سرور ؟ پس باید تعریف انتزاعی کدهای مربوط به سرور رو اینجا بذاریم ، تعریف انتزاعی عملیات Database و ... رو هم در این لایه قرار می‌دیم . یکی دیگه از چیزایی که اینجا نگه می‌داریم کلاس های Mapper برای تبدیل مدل سرور به دیتابیس هست (این لایه باید Pure Kotlin/Java باشد) .ما تو این لایه DataSource (منابع داده) رو داریم ، در این لایه تعریف شون رو داریم (یعنی Interface) و در کنارش کلاس های Repository رو خواهیم داشت که از اون DataSource ها استفاده خواهند کرد .لایه Domain :این لایه درونی ترین لایه ما حساب میشه و مثل همون Entity میشه . ما در این لایه بیشتر مدل هامون رو قرار می‌دیم . مثلا مدل دیتاهایی که قراره از سرور ، دیتابیس یا جاهای دیگه بیان ، این لایه هم باز Pure Kotlin/Java هست ، یعنی (ترجیحا) از SDK اندروید قرار نیست استفاده کنیم .لایه Use-Case :در این لایه با استفاده از کلاس های Repository لایه قبلی می‌آییم و Use Case ها رو می‌نویسیم . Use Case ها همون تعاملات کاربر با اپلیکیشن هست ، یعنی کاربر فلان کار رو اگه بکنه چی صدا زده بشه .لایه App :به این لایه Presentation هم می‌گن . این لایه همون لایه MVVM ما حساب میشه که توش View ها و View-Model هامون رو داریم .لایه های فرعی :یک سری چیز مثل Utils و ... که طبقه بندی خاصی نمیشه براشون ارائه داد اینجا هستند ، همین طور می‌تونیم برای DI هم یک لایه جدا در نظر بگیریم و کارایی از این قبیل .خب دیگه تعاریف تئوری بسه ، وقتشه کد بزنیم !!!نکته : یکی از کارهایی که در مقالات دیدم انجام می‌دن جدا کردن لایه Domain و Data به حالت یک ماژول (لایبری) بوده که اسمش رو core میذاشتن ، و ما هم همینکار رو می‌کنیم .خب طبق نکته بالا ابتدا یک ماژول جدید ایجاد می‌کنیم ، نکته ای که باید توجه کنیم اینه که این ماژول نباید کتاب خونه مخصوص اندروید باشه بلکه طبق عکس زیر باید گزینه Java/Kotlin Library رو انتخاب کنید (چون این دو لایه قراره pure kotlin یعنی کاتلین خالص باشند :حتما این گزینه رو انتخاب کنیدبعد از اون هم باید این ماژول رو به App اضافه کنید که یحتمل بلدید ولی اگر نیستید در gradle این طور این کار رو می کنیم :dependencies{// more codeimplementation project(':core')}قراره در این بخش سه لایه Domain ، Data و Use-Case (برخی منابع Instructor(تعامل کنند) هم می‌‎گن) رو ایجاد بکنیم :ما برای ارتباط با سرور از این API استفاده می‌کنیم : https://jsonplaceholder.typicode.com/از لیست API هایی که در اختیار داریم فقط با post و user کار داریم ، در لایه Domain که قراره لیست مدل دیتاهای ما باشن این دو کلاس رو ایجاد می‌کنیم : https://gist.github.com/sasssass/44b848b14ef9f69fb8647e7b4e6334b7 و https://gist.github.com/sasssass/3224013164cd6a488d619b065b0250de خب حالا نیاز به مفهومی به نام DataSource داریم ، یک DataSource طبق معنیش یعنی منبع داده و شامل یک Interface به ازای هر مدل هست که توش توابعی برای دسترسی به مدل قرار داده شده ، مثلا توابع مثل گرفتن مدل ، اضافه کردن (مثلا در دیتابیس یا ...) ، حذف و ...چون دو مدل داریم باید دو DataSource داشته باشیم ، این DataSource ها در لایه Data قرار می‌گیرن : https://gist.github.com/sasssass/3eee299940bca271496d6bdf08442920 و https://gist.github.com/sasssass/f0d5980fb2a824d8f7f4ce2c88b503a0 مفهوم دیگری که داریم Repository ها هستند ، به ازای هر DataSource یک Repository خواهیم داشت ، Repository کلاس هایی هستند که یک جنس از DataSource مورد نظر می‌گیرند (این DataSource در لایه Framework پیاده سازی خواهد شد) و توابعی دارند که از توابع DataSrouce استفاده می‌کنند ، به عنوان نمونه به PostRepository توجه کنید : https://gist.github.com/sasssass/1c3fa3a261072218403c54fc4bdaafa4 نکته : چرا از suspend استفاده شد ؟ اگر معنی این کلمه کلیدی و کاراییش رو نمی‌دونید به سری مقالات Coroutines بنده مراجعه کنید (مقاله 1 ، مقاله 2 ، مقاله 3 ، مقاله 4 ، مقاله 5) چون ما قراره ارتباط با سرورمون با Coroutines ترکیب بشه توابع رو suspend می‌کنیم .تمرین : UserRepository رو خودتون بنویسید .به این کاری که کردیم Repository pattern می‌گن ، این الگو یک نوع انتزاعی کردن دسترسی به داده هاست ، یکی از کاربردهاشم اینه که به وسیله اون میشه unit test ها رو پیاده کرد . چون Repository به DataSource وابسته است نه به DataSourceImp (Imp = implantation) پس جزئیات پیاده سازی DataSource رو نمی‌دونه و این از جهت جدا سازی کلاس های برنامه مفیده .خب حالا باید بریم سراغ Use-Case ، یکبار دیگه Use-Case رو این طور تعریف می‌کنیم : هر کلاسِ Use-Case معادل یک Action کاربر/سیستم در برنامه است . پس به ازای هر حرکت کاربر باید یک Use-Case تعریف کنید . برای مثال من می‌خوام Use-Case ای رو داشته باشم که با اون همه Post ها رو بگیرم ، در Use-Case باید به Repository مورد نظر دسترسی داشته باشیم ، مثلا این یک نمونه از Use-Case ها خواهد بود : https://gist.github.com/sasssass/c780e374568ce71aa76680aebcfffca9 در مورد invoke ، این لغت کلیدی در واقع یک اپراتور هست ، اگر ما به یک کلاسی invoke بدیم (override کنیم) ، می‌تونیم از اون کلاس به عنوان یک تابع استفاده کنیم ، مثلا به این نمونه دقت کنید :class Greeter(val greeting: String) { operator fun invoke(target: String) = println("$greeting $target!")}val hello = Greeter("Hello")hello("world") // Prints "Hello world!"برای تمرین GetPostByUserIDUseCase رو خودتون پیاده کنید .خب تا اینجای کار باید قسمت core به این شکل دراومده باشه (من همه Use-Case ها و ... رو پیاده نکردم) :الان کار ما در این قسمت تمومه و باید بریم سراغ قسمت App ، ما در این قسمت نیاز داریم که DataSource ها پیاده سازی بشن ، برای مثال من FromServerPostDataSource رو دارم که مشخص می‌کنه این پیاده سازی DataSource مربوط به Post هاست که قراره از Server خونده بشه (اسم خیلی جالب نیست و صرفا آموزشیه) ، ما در این قسمت به api هم نیاز داریم ، برای قسمت هایی که کامنت گذاشته شده نیاز داریم که یک DB هم اضافه کنیم که از این کار صرف نظر می‌کنیم ، ابتدا کد api رو می‌بینیم (با استفاده از Retorfit) و بعد کد DataSource رو : https://gist.github.com/sasssass/319677b9974ccefddbbab23f24e72496 و https://gist.github.com/sasssass/af3174e6f21dac882257c19955adf560 خب فعلا قسمت Clean تمام شده و باید با MVVM تلفیق کنیم ، در معماری MVVM نیاز به View-Model داریم ، در View-Model نیاز به Use-Case داریم و در Use-Case نیاز به Repository ، در Repository هم نیاز به DataSource ای داریم که همین الان کلاسشو رو ساختیم ، ما به جز View-Model بقیه اجزا رو در بالا معرفی کرده بودیم ، من برای راحتی کار تصمیم گرفتم DI رو هم در قضیه دخیل کنم و از Hilt استفاده کردم ، علاوه بر اون چون از ترکیب Retrofit و Coroutines هم استفاده کردیم پس نیازه این سه مقاله رو بخونید :قسمت اول Hiltقسمت دوم Hiltارتباط سرور و Coroutinesبا توجه به این سه مقاله من این چند کلاس رو اضافه می‌کنم ، RemoteErrorEmitter ، RetrofitBuilder و Api که Api رو قبلا دیدیم ، همین طور ماژول apiCall رو هم خواهیم داشت که در مقاله ارتباط سرور و Coroutines به تفضیل توضیح داده شده : https://gist.github.com/sasssass/2aad2872e04eb30bd783e47cbc62ceea و https://gist.github.com/sasssass/108c01d4e922568504b35e82497c6516 و https://gist.github.com/sasssass/0492db329a527bd7b90eae1cda223e93 حالا نیاز به DI داریم ، کاری که می‌کنیم اینه ، یک AppModule درست می‌کنیم که Repository و این جور چیزا رو برامون Provide کنه (ما می‌تونستیم از constructor injection استفاده کنیم ، ولی ترجیح دادم تک به تک provide بشن چون قرار بوده پارامترهایی که دارن provide میشن داخل خودشون از sdk اندروید استفاده نکنند البته hilt جزو sdk اندروید نیست ولی چون به Framework اندروید ربط داشت این تصمیم رو گرفتم ، ممکنه تصمیم جالبی نباشه و اگه خواستید می‌تونید با همون constructor injection برید) : https://gist.github.com/sasssass/293f9a8394c959c76429e48f161c4aed یک موردی که در مقاله های سری Hilt نیست اینه که به دلایلی به ما اجازه نمیده چیزایی که می‌خوایم رو تو ViewModel تزریق کنیم ، برای همین ما خودِ کلاس ViewModel رو بدون constructor injection درست می‌کنیم : https://gist.github.com/sasssass/3008f35e08f52aa568f1f084e88ad436 توضیحی که لازمه بدم اینه که طوری که من معماری رو چیدم بر پایه این شده که ما در View-Model به Use-Case متکی هستیم ، با استفاده از Use-Case ها درخواست های کاربر رو انجام می‌دیم .حالا باید ViewModel رو داخل Activity تزریق کنیم برای برای حل قضیه Inject کردن ViewModel نیاز به MyViewModelFactory داریم (رجوع کنید به این مقاله) : https://gist.github.com/sasssass/1290b4f5d2e0d394647d58947965cdc1 حالا به راحتی میشه View-Model رو داخل Activity تزریق کرد ، همچنین ما نیاز داریم Use-Case مورد نظر رو داخل Activity تزریق کنیم تا در ساخت View-Model ازش استفاده کنیم ، منتها برای اینکه اصول DI رو کامل رعایت کنیم خود این ViewModelFactory رو هم باید داخل Activity تزریق کنیم ، پس یک ماژول DI برای Activity می‌سازیم : https://gist.github.com/sasssass/bae6b62860f9689dfb380711068891fb و تمام ، حالا خودِ Activity رو ایجاد می‌کنیم : https://gist.github.com/sasssass/528a2ffe01d16cdc66c43fb328ea91ea وقتی که برنامه رو اجرا می‌کنید می‌بینید که Toast مورد نظر نشون داده میشه ، برای تمرین قسمت های دیگه برنامه رو خودتون کامل کنید تا دستتون بیاد به چه صورته ، مثلا بقیه Repo ها و DataSource ها و ... رو بسازید (پروژه به همین صورت تو گیت پوش شده تا بتونید خودتون کاملش کنید) :لینک گیت مقاله رو هم در این لینک می‌تونید ببینید .من رو در لینکدین ، اینستاگرام و یوتیوب دنبال کنید !!!اگه دوست داشتید می‌تونید به صفحه Spotify و SoundCloud بنده هم برید و موسیقی های منو گوش بدید !!!