ساختار اپلیکیشن های اندروید

اکوسیستم توسعه ی آندروید با سرعت زیادی در حال پیشرفت است. بطوریکه هر هفته ابزارهای جدیدی ساخته شده، کتابخانه ها آپدیت شده، پست های بلاگ و صحبت های جدیدی ارائه میشوند. اگر بمدت یک ماه به تعطیلات بروید زمانیکه برمی گردید با ورژن جدیدی از پشتیبانی کتابخانه یا Play Services روبرو خواهید شد.
من بعنوان نویسنده ی این مقاله بیش از سه سال با تیم
ribot برای ساخت اپلیکیشن های آندروید همکاری داشتم. طی این مدت ساختار و تکنولوژی هایی که برای ساخت اپلیکیشن ها استفاده می شد دائما درحال رشد بود. در این مقاله شما را با آموخته ها، اشتباهات و علل بروز این تغییرات ساختاری آشنا کنیم.

گذشته

سال 2012 دیتابیس ما ساختاری ابتدایی داشت. ما از هیچ لایبرری نتورکینگ و Async Tasks استفاده نمیکردیم. تصویر زیر چگونگی ساختار ساخت اپلیکیشن در گذشته را نشان میدهد.
 

 

در گذشته ساختار کدها از دولایه تشکیل میشد: نخست لایه دیتا که مسئول بازیابی/ذخیره دیتا از REST APIs و پایداری دیتا استورها بود و دیگری لایه ویو که مسئولیت رسیدگی و نمایش دیتا در رابط کاربری را برعهده داشت.
متدهای عرضه شده ازسوی
APIProvider از طریق فعال کردن Activities و Fragments موجب سهولت تعامل با REST API میشوند. این متدها برای اجرای فراخوان های نتورک در یک thread جداگانه از URLConnection و Async Tasks استفاده میکنند.
همچنین
CachProvider نیز متدهایی دارد که دیتا را از SharedPrefrences یا دیتابیس SQLite بازیابی وذخیره میکند. و برای بازگرداندن نتیجه به اکتیویتی ها از کال بک ها استفاده میکند.

مشکلات

مشکل اصلی این رویکرد این بود که لایه ی ویو مسئولیت های زیادی داشت. یک سناریوی ساده و معمولی را تصور کنید که اپلیکیشن باید لیستی از پست های بلاگ بارگذاری کرده، آنها را در دیتابیس SQLite ذخیره(cache) و درنهایت آنها را در ListView نمایش دهد. در این رویکرد اکتیوتی باید موارد نامبرده در زیر را انجام دهد:
فراخوان یک متد
LoadPosts (کال بک) در APIProvider.
انتظار برای کال بک موفق
CacheProvider و نمایش پست ها در لیست ویو.
رسیدگی جداگانه به دو خطای احتمالی کال بک از
APIProvider و CacheProvider.

به یک مثال بسیار ساده توجه کنید. در یک سناریوی واقعی احتمال دارد REST API دیتای را آنطور که ویو به آن نیاز دارد بازنگرداند. بنابراین اکتیویتی قبل از نمایش دیتا آنرا بنحوی تغییر داده یا فیلتر میکند. مورد رایج دیگر زمانی رخ میدهد که متد LoadPosts پارامترهایی را بگیرد که باید از جای دیگری آورده شوند مثلا آدرس ایمیلی که توسط پلی سرویس های SDK تهیه شده است. در چنین شرایطی احتمال دارد SDK با استفاده از یک کال بک بطور غیرهمزمان ایمیل را برگرداند، به این مفهوم که سه مرحله از کال بک های تودرتو داریم. درصورتیکه پیچیدگی های بیشتری اضافه شود حاصل این رویکرد پدیده ای است که با عنوان callback hell(جهنم کال بک) شناخته میشود. بطور خلاصه:
اکتیویتی ها و
Fragments بیش از حد بزرگ شده و حفظ آنها دشوار میشود.
افزایش کال بک های تودرتو به معنای زشت شدن کدها و دشوار شدن فهم آنها بوده و درنتیجه ایجاد تغییر یا افزودن فیچرهای جدید سخت میشود.
آزمایش واحد(
unit testing) اگر غیرممکن نشود قطعا چالش برانگیز خواهد شد زیرا لاجیک لایوهای بسیاری درون اکتیویتی ها و Fragments هستند که برای آزمایش واحد دشوارند.

ارائه ی یک ساختار جدید از سوی Rx Java

ما بمدت دو سال از روش نامبرده در بالا استفاده می کردیم. طی این دو سال به چند پیشرفت دست یافتیم که تنها اندکی مشکلات نامبرده را تقلیل دادند. بعنوان مثال چندین کلاس کمک کننده(هلپر) برای کاهش کدها در اکتیویتی ها و Fragments افزوده شد و از Volley در APIProvider استفاده شد. باوجود تمام این تغییرات کدهای اپلیکیشن ما همچنان تست فرندلی نبود و مشکل جهنم کال بک زیاد رخ میداد.
اواخر سال 2013 بود که شروع به مطالعه درباره
RxJava کردیم. بعد از امتحان آر ایکس جاوا روی چند نمونه پروژه متوجه شدیم عاقبت برای مشکل کال بک تودرتو راهکاری یافته ایم. آر ایکس جاوا به شما اجازه میدهد تا از طریق استریم های غیرهمزمان دیتا را مدیریت کرده و همچنین اپراتورهای بسیاری به شما میدهد که می توانید با بکارگیری آنها در استریم، دیتا را تغییر، ترکیب یا فیلتر کنید.
با درنظر گرفتن رنج هایی که سالهای پیش تجربه کردیم تصمیم گرفتیم به این مساله بیاندیشیم که ساختار یک اپلیکیشن جدید چگونه خواهد بود. نتیجه را در تصویر زیر مشاهده میکنید:

 

 

درست مشابه رویکرد نخست این ساختار نیز می تواند به دو لایه دیتا و ویو تقسیم شود. لایه دیتا شامل دیتا منیجر و مجموعه ای از کمک کنندها میشود. لایه ی ویو توسط کامپوننت ها ی فریم ورک آندروید از قبیل اکتیویتی ها، Fragments، ViewGroups و غیره تشکیل شده است.
کلاس های هلپر( ستون سوم در تصویر) مسئولیت های بسیار خاصی دارند و آنها را به شیوه ای فشرده و مختصر اجرا و تکمیل میکنند. بعنوان مثال بیشتر پروژه ها برای دسترسی
REST APIs هلپرهایی دارند که دیتا را ار دیتا بیس ها خوانده یا با SDK های شخص ثالث درتعاملند.
PreferenceHelper: دیتا را در SharedPreferences خوانده و سیو میکند.
دیتابیس هلپر: رسیدگی دسترسی به دیتابیس های
SQLite.
سرویس های
Retrofit: اجرای کال ها به REST APIs. بدلیل پشتیبانی برای آر ایکس جاوا بجای Volley از Retrofit استفاده شد.
بسیاری از متدهای عمومی درون کلاس های هلپر
RxJava Observables را باز می گردانند.
دیتامنیجر مغز متفکر این ساختار است که از اپراتورهای آر ایکس جاوا برای ترکیب، فیلتر و تغییر دیتای بازیابی شده ازکلاس های هلپر بسیار استفاده میکند. هدف دیتا منیجر کاهش وظایف اکتیویتی ها و
Fragment ها می باشد. این کار از طریق فراهم کردن دیتایی انجام میشود که برای نمایش آماده است و به هیچ تغییری نیاز ندارد.
برای آشنایی بیشتر با متد دیتامنیجر به کدهای زیر دقت کنید. نحوه کارکرد این متد:
فراخوان سرویس
Retrofit برای بارگذاری یک لیست از پست های بلاگ از REST API.
سیو پست ها در دیتابیس لوکال برای اهداف ذخیره سازی(
caching) با استفاده از دیتابیس هلپر.
فیلتر کردن بلاگ پست هایی که امروز نوشته شده زیرا آنها تنها پست هایی هستند که لایه ویو قصد نشان دادن آنها را دارد.


 

 

کامپوننت های موجود در لایه ویو مانند اکتیویتی ها یا Fragments این متد را فراخوانده و از Observable بازگشتی پشتیبانی میکنند. به محض تمام شدن پشتیبانی، پست های خروجی توسط Observable مستقیما به یک Adapter اضافه شده تا در RecyclerView یا مشابه آن نمایش داده شوند.
آخرین المان از این ساختار
event bus می باشد. این المان به ما امکان میدهد تا وقایعی که در لایه دیتا رخ میدهند را منتشر کنیم به این ترتیب کامپوننت های متعدد در لایه ویو می توانند این وقایع را پشتیبانی کنند. بعنوان مثال متد sign out() در دیتامنیجر می تواند هنگامیکه Observable کامل میشود یک رویداد را پست کند بنابراین آندسته از اکتیویتی هایی که از این رویداد پشتیبانی میکنند می توانند رابط کاربری خود را برای نمایش عبارت signed out تغییر دهند.

چرا این رویکرد بهتر است؟

اپراتورها و Observable های آر ایکس جاوا نیاز به کال بک های تودرتو را حذف میکنند.
دیتامنیجر مسئولیت هایی که قبلا برعهده ی لایه ویو بود را کنترل میکند. به این ترتیب اکتیویتی ها و
Fragment ها سبک تر میشوند.
انتقال کدها از اکتیویتی ها و
Fragment ها به دیتامنیجر و هلپرها موجب میشود تا نوشتن آزمایش های واحد راحت تر شود.
تفکیک واضح مسئولیت ها و داشتن دیتامنیجر بعنوان تنها نقطه تعامل با لایه دیتا این ساختار را تست فرندلی میکند. کلاس های هلپر یا دیتامنیجر به آسانی تست میشوند.

اما چه مشکلاتی هنوز باپرجا هستند؟

دیتا منیجر برای پروژه های بزرگ و بسیار پیچیده بیش از حد بزرگ شده و نگهداری آز آن دشوار میشود.
هرچند کامپوننت های لایه ویو از قبیل اکتیویتی ها و
Fragments سبک تر میشوند اما هنوز هم باید به مقدار قابل توجه ای از subscription های مدیریت جاوا آر ایکس،خطاها، آنالیزها و غیره رسیدگی کند.

تکمیل Model View Presenter

سال گذشته چندین الگوی ساختار از قبیل MVP یا MVVM میان جامعه آندروید رواج یافتند. بعد از بررسی این الگوها در یک نمونه پروژه و مقاله دریافتیم که الگوی MVP برای رویکرد فعلی ما پیشرفت های ارزشمندی به ارمغان می آورد. از آنجاییکه ساختار کنونی ما به دو لایه ی (ویو و دیتا) تقسیم میشود آفزودن الگوی MVP به آن طبیعی جلوه میکند. بدین منظور کافیست که یک لایه ی جدید از presenter به آن اضافه کرده و قسمتی از کدها را از لایه ویو به presenter ها منتقل کنیم.
 

 

لایه دیتا دست نخورده باقی می ماند اما به منظور سازگاری با اسم الگو، مدل(model) نامیده میشود.
مسئولیت بارگذاری داده ها از مدل برعهده ی
presenter ها بوده و بهنگام آماده بودن نتیجه متد مناسب را فرامی خوانند. آنها از طریق دیتا منیجر از Observable های بازگشتی پشتیبانی میکنند. بنابراین آنها باید به چیزهایی همچون schedulers و subscriptions رسیدگی کنند. علاوه براین آنها می توانند کدهای خطا را آنالیز کرده یا درصورت نیاز اپراتورهای بیشتری در دیتا استریم اعمال کنند. مثلا اگر نیاز داشتیم تا دیتایی را فیلتر کنیم و همین فیلتر در جای دیگر قابل استفاده نباشد اجرای آن در presenter منطقی تر است تا اینکه در دیتا منیجر انجام شود.
در تصویر زیر نمونه ای از یک متد عمومی در
presenter را مشاهده میکنید.این کد از طریق دیتا منیجر، Observable برگشتی را پشتیبانی میکند.

  

 

ویوی MVP کامپوننت ویویی است که این presenter به آن کمک میکند. معمولا ویوی MVP نمونه ای از یک اکتیویتی، Fragment یا View Group است.
همانند ساختار قبلی لایه ویو کامپوننت های فریم ورک استاندارد از قیبل
View Group، Fragment یا اکتیویتی ها را شامل میشود. تفاوت عمده ی آنها در کامپوننت هایی است که مستقیما از Observable ها پشتیبانی نمیکنند. درعوض آنها یک واسط MVP View را اجرا کرده و لیستی از متدهای مختصر همچون نمایش خطا یا نمایش شاخص پیشرفت تهیه میکنند. بعلاوه مسئولیت رسیدگی به تعاملات کاربری همچون رویدادهای کلیک برعهده ی کامپوننت های ویو بوده و از طریق فراخوان یک متد صحیح در presenter برطبق آنها عمل میکنند. بعنوان مثال اگر باتنی داشتیم که لیستی از پست ها را بارگذاری می کرد اکتیویتی ما presenter.load Today Posts() را فرا می خواند.

چرا این رویکرد بهتر است؟

اکتیویتی ها و Fragments بسیار سبک شده و تنها مسئولیت های ما آپدیت/set up واسط کاربری و رسیدگی به رویدادهای کاربری است. بنابراین نگهداری و حفظ آنها آسانتر میشود.

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

اگر دیتا منیجر زیاد بزرگ شود این مشکل را از طریق انتقال برخی از کدها به presenter حل می کنیم.

مشکلاتی که هنوز وجود دارند

داشتن تنها یک دیتا منجیر هنگامیکه پایگاه کدها خیلی بزرگ و پیچیده میشود می تواند هنوز یک مساله باشد. البته ما هنوز به جایی نرسیدیم که این مساله به مشکلی واقعی تبدیل شود اما می دانیم که در آینده به این مشکل برخورد خواهیم کرد.
لازم به ذکر است که یادآوی کنیم این ساختار کامل و بی عیب نبیست. درحقیت تصور اینکه رویکردی بی همتا و بی نقص وجود دارد که همه ی مشکلات را برای همیشه حل میکند تصوری خام و باطل است. اکوسیستم آندروید با گام های سریع به رشد و تکامل خود ادامه و ما برای یافتن شیوه های بهتر برای ساختن اپلیکیشن های بی نظیر آندروید باید به کاوش، مطالعه و آزمایش ادامه دهیم.

 


 

ساختار اپلیکیشن های اندروید
شنبه 25 شهریور 1396 - 09:34:37 122 آخرین بازدید : دوشنبه 1 آبان 1396 - 10:07:57 0
*
*