نگاهی عمیق‌تر به RxJava 2

آموزش اندروید آر ایکس اکس جاوا 2 rxjava 2 rxandroid rx reactive programming imperative programming برنامه نویسی ریاکتیو واکنشی android eventbus مقایسه
بررسی RxJava 2

سطح آموزش: #پیشرفته

مقدمه

برای شروع این بحث لازم هست که این پست و پیش‌نیازهاش رو مطالعه کرده باشید.

منابعی که برای نوشتن این پست ازشون استفاده کردم به طور عمده javadoc خودِ RxJava بوده. همچنین سخنرانی Jake Wharton در goto 2016. بعضی از تصاویری هم که استفاده می‌کنم از اسلایدهای وارتون استخراج کردم که میتونید کاملش رو اینجا ببینید.

synchronous و asynchronous چیست؟۱

synchronous یا سنکرون یعنی اینکه اتفاق‌ها رفتاری سینک شده و همزمان داشته باشن. توی برنامه‌نویسی این همزمانی بصورت ترتیبی اتفاق میفته. مثلا کد زیر رو نگاه کنید:

میدونیم که این کد خط به خط اجرا میشه و در خط آخر وقتی ما به a دو واحد اضافه می‌کنیم فقط a اضافه میشه. ما گفتیم c برابر مجموع a و b هست ولی در برنامه سنکرون تغییرات همگام هستن و مقدار c فقط جایی که بهش assign شده تغییر میکنه.

asynchronous یا آسنکرون رفتارهای غیر همزمان رو نشون میده. یک نمونه از رفتار آسنکرون میتونه وقتی باشه که ما از سرور چیزی رو میخونیم. توی مالتی تردینگ اساسا وقتی چندتا ترد دارن فعالیت میکنن ما دیگه همزمانی نداریم.

تو نسخه‌های قدیمیِ اندروید اگر یادتون باشه میتونستیم دیتا رو روی ترد ui هم بخونیم ولی در اینصورت ui قفل میشد تا داده آماده بشه. این سنکرون بود چون ما منتظر میموندیم کارِ network تموم بشه و بعد ادامه فعالیت‌ها دنبال میشد ولی وقتی در یک ترد جدا اطلاعات رو از اینترنت می‌خونیم کارمون آسنکرون هست. چون این‌ور ما میتونیم هرکار دیگه‌ای که میخوایم رو انجام بدیم، از طرفی وقتی دیتا آماده شد هم متوجه میشیم. کارِ asynctask هم (همینطور که از روی اسمش مشخصه) اینه که این عملیات آسنکرون رو با برنامه ما سینک کنه.

چرا Reactive؟

جیک وارتون تو جواب به این سوال میگه که:

Unless you can model your entire system synchronously, a single asynchronous source breaks imperative programming2.

همینطور هم هست دیگه. مثلا فرض کنید ما داریم یه چیزی رو از اینترنت میخونیم و گوشی کاربر زنگ میخوره و همزمان دیتای ما هم آماده میشه. اگر ما از قبل پیش‌بینی نکرده باشیم که چنین اتفاقی ممکنه بیفته با از بین رفتن اکتیویتی، وقتی دیتا برسه باعث کرش اپلیکیشن میشه.

در واقع مشکل برنامه‌نویسی سنکرون اینه که ما باید همه چیز رو handle کنیم. و این پیش‌بینی و handle کردن باعث میشه کدمون خیلی پیچیده بشه.

در یک برنامه سنکرون کد ما باید تمام رویدادها رو در نظر بگیره

توی این تصویر عوامل مختلفی رو نشون میده که به ما میگه اندروید پلتفرم سنکرونی نیست.

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

می‌بینیم که سیستم عامل، network و… همه به عنوان منابع آسنکرون نشون داده شدن و ما اگر بخوایم با یک ذهنیت سنکرون و تک نخی (single thread) برنامه‌نویسی کنیم باید با تمامی این منابع غیر همزمان خارجی دست و پنجه نرم کنیم. حتی خود کاربر هم از طریق ui یکی از منابع asynchronous محسوب میشه. پس ما باید طوری کد بزنیم که نه ترد اصلی مسدود بشه و نه اینکه بخشی از اطلاعات بیاد و ما آمادگیش رو نداشته باشیم.

مساله rotation در اندروید با استفاده از RxJava

من برنامه‌های ایرانی رو که نگاه می‌کنم عموما از rotation پشتیبانی نمی‌کنن. کافه‌بازار، دیجی‌کالا، اسنپ و… همه از بهترین شرکت‌های ایرانی هستن که اساسشون روی تکنولوژی هست ولی اپلیکیشن‌های همین شرکت‌های بزرگ هم از rotation پشتیبانی نمیکنه. دلیل عمده‌اش از نظر من بر میگرده به اینکه با rotate شدن گوشی اکتیویتی دوباره ساخته میشه و این اتفاق یک رفتار asynchronous هست چون هر زمانی ممکنه کاربر گوشیش رو بچرخونه. حالا اگر ما synchronous کد زده باشیم ناچاریم یک سطح جدید از پیچیدگی به کدمون اضافه کنیم تا بتونیم این رفتارِ کاربر رو پیش‌بینی کنیم و به درستی بهش واکنش نشون بدیم.

از اونجایی که یک اپلیکیشن به بزرگی دیجی‌کالا و کافه‌بازار به اندازه کافی پیچیدگی داره احتمالا بهتر دیدن به طور کل از پشتیبانی rotation صرف نظر کنن (بگذریم از اینکه طراحی‌های لایه‌ها هم باید برای حالت landscape بهینه بشه) اما خواهیم دید با استفاده از برنامه‌نویسی reactive دیگه نیازی نیست ما با تمام این منابع آسنکرون خارجی سر و کله بزنیم.

Android asynchronous programming benefits

در اصل ما کاری می‌کنیم که مثلا UI به اومدن دیتا از سرور واکنش نشون بده، یا به تغییرات سیستم‌عامل واکنش نشون بده. با این کار خیلی از کدهایی که ما برای هماهنگی این بخش‌ها میزدیم از بین میره. البته ما هنوز قراره کد بزنیما! ولی کمتر و بهینه‌تر کد می‌زنیم. چون جای اینکه سعی کنیم همه چیز رو مدیریت کنیم، این قطعه‌ها رو به هم وصل می‌کنیم.

چرا RxJava؟

RxJava کتابخونه‌ایه که به ما این امکان رو میده تا بصورت آسنکرون رفتار کنیم. هم بتونیم داده تولید کنیم و هم به داده‌های تولید شده واکنش نشون بدیم.

تولید داده میتونه کلیک کاربر باشه یا دریافت اطلاعات از سرور یا هرچیز دیگه. مثلا اگر در مثال اول آموزش مقدار a و b رو با RxJava به عنوان تولید کننده داده و c رو به عنوان واکنش‌نشون دهنده تعریف کنیم، با تغییر a و b مقدار c هم که مجموع این دوتاست تغییر می‌کنه.

گوش کردن به داده‌ها رو subscribe میگیم. یعنی c تغییرات a و b رو subscribe می‌کنه۳.

منابع داده

اینجا منظورم از منابع داده همون Observableها هست. توجه کنید که یک observable نوعی منبع داده حساب میشه. مثلا وقتی میگیم:

واضحه که Observable به عنوان یک منبع داده داره اعدادی رو منتشر (emit) می‌کنه.

منابع میتونن سنکرون یا آسنکرون باشن. یعنی با RxJava هم میتونیم داده سنکرون داشته باشیم.

منابع میتونن تک آیتمی، چند آیتمی یا تهی باشن. تک آیتمی مثلا وقتی که از سرور چیزی رو میخونیم یک پاسخ داریم در حالیکه مثلا منبع کلیک بی‌نهایت آیتم داره و مادامی که UI وجود داره کاربر میتونه کلیک انجام بده.

تهی هم به منبعی میگن که هیچ داده‌ای نداره فقط عمل موفق شدن و شکست خوردن رو داره. مثلا وقتی روی فایل می‌نویسیم چیزی برای return کردن وجود نداره فقط success و fail داریم.

fail به error منجر میشه -> onError

success به complete منجر میشه -> onComplete

منابع داده ممکنه هیچوقت complete نشن. مثل همون کلیک که هیچوقت تموم نمیشه. در واقع اگر onComplete رو برای یک کلیک صدا کنید دفعه‌های بعدی با کلیک روی اون آیتم اتفاقی نمیفته چون با complete شدن اون task تموم شده و منابعش هم آزاد میشه.

Observable  و Flowable

میدونیم که این منابع داده (Resources) که ازشون صحبت کردم همون Observable ها هستن. اما تو RxJava یک نوع شیء دیگه هم داریم به اسم Flowable که دقیقا مثل observable رفتار میکنه با یک تفاوت جزئی.

فرق Observable و Flowable چیست؟

flowable مفهومی تحت عنوان back pressure رو پشتیبانی می‌کنه در حالیکه observable نمیکنه.

back pressure چیست؟

back pressure مفهومیه که بر اساس اون میتونیم سرعت حرکت داده رو کنترل کنیم. مثلا فرض کنید داریم از دیتابیس اطلاعات رو میخونیم ولی نمیخوایم با تمام توان عمل خوندن انجام بشه، میخوایم با یک سرعت مشخص این اطلاعات رو دریافت کنیم اینجاست که از مفهوم back pressure استفاده می‌کنیم.

اما چرا دو تا type مختلف داریم که یکی back pressure v رو پشتیبانی می‌کنه و دیگری نمیکنه؟

دلیلی که وارتون میگه اینه که back pressure خیلی دیر به RxJava 1 اضافه شده برای همین تمامی تایپ‌ها این ویژگی رو پشتیبانی می‌کردن. از طرفی تمام سورس‌ها توقعش رو ندارن. ما شاید بتونیم به دیتابیس بگیم اطلاعات رو با این سرعت برامون بفرست اما نمیتونیم به کاربر بگیم با چه سرعتی روی صفحه کلیک کن.

برای استفاده از back pressure مثل ارث‌بری باید طراحی‌مون رو بر اساس اون انجام بدیم برای همین دو تا تایپ مختلف در نظر گرفته شده که جاهایی که اساسا امکانش نیست یا تمایلی نداریم از تایپی استفاده کنیم که امکان back pressure رو هم پشتیبانی نمی‌کنه. اینجوری ازخطرهای استفاده نادرست از back pressure که منجر به کرش میشه دور میشیم.

برای روشن‌تر شدن بحث به مثال زیر توجه کنید:

کد

نمونه اول برای خوندن touchهای کاربر روی صفحه نمایش هست و دومی برای خوندن اطلاعات از سرور. اگر هر دو از یک type استفاده می‌کردن (مثل RxJava 1) وقتی میخواستیم back pressure رو پیاده کنیم چون از اساس برای تاچ‌های کاربر چنین مفهومی معنی نداره برنامه کرش می‌کرد. اما حالا با جدا کردن این دو میتونیم از این مشکل جلوگیری کنیم.

هرجا که Observable داده‌ها رو انقدر سریع تولید می‌کنه که Observerها نمیتونن مصرفشون کنن از backpressure استفاده می‌کنیم. دایکومنت خودِ reactivex در این خصوص خیلی خوب توصیح داده میتونید از اینجا بخونید.

بالاتر گفتم Observable و Flowable سورس‌های داده هستن. و همچنین میدونیم هر سورس داده نیاز به یک نوع typeـی داره که به داده‌هایی که تولید می‌کنه گوش بده. تو پست‌های قبلی با Observer ها آشنا شدیم. مثل همین مفهوم برای flowableها هم هست که بهش subscriber میگن:

کد

می‌بینید که تمام متدهای اینترفیس‌هاشون مثل همدیگه هستن. از طرفی گفتیم مفهوم back pressure در subscriber وجود داره و در observer نیست. برای اینکه از این مساله سر در بیاریم نگاهی به تنها تفاوت این دو type یعنی ساختار onSubscribe می‌نداریم:

یکی از خوبی‌های RxJava گفتم اینه که خیلی راحت میتونیم منابع رو بعد از اینکه کارمون باهاشون تموم شد آزاد کنیم. متد onSubscribe بعد از اینکه یک observer یا subscriber رو به Observable یا Flowable متصل کرد یک شیء به ما برمی‌گردونه که با استفاده از اون میتونیم منابع رو آزاد کنیم.

در مورد observable این شیء Disposable هست و در مورد Flowable این شیء subscription هست. داخل disposable متد dispose رو داریم که از روی معنی لغویِ اون هم میشه حدس زد که کارش رها کردن منابعِ گرفته شده، هست. مثلا اگر Observable با سرور ارتباط برقرار میکنه با صدا کردن dispose میگیم که این ارتباط رو قطع کن.

subscription هم به طور مشابه یک متد cancel داره که همین کار رو میکنه. اما subscription متد دیگه‌ای به اسم request داره که با استفاده از اون میشه جریان ورودی اطلاعات رو کنترل کرد و با request به flowable میگیم که آیتم‌های بیشتری رو میخوایم.

پایان

۱-  به نظرم یک مقدار دید سخت‌افزاری اینجا به درک بهتر مساله خیلی کمک می‌کنه. اگر مفهوم synchronous و asynchronous رو متوجه نشدید مفهوم این دو واژه در مدارها و سیستم‌های سخت‌افزاری رو مطالعه کنید.

۲- In computer science, imperative programming is a programming paradigm that uses statements that change a program’s state. In much the same way that the imperative mood in natural languages expresses commands, an imperative program consists of commands for the computer to perform. (Wikipedia)

۳- میدونم واژه “subscribe میکنه” مثل خیلی از واژه‌های انگلیسی-فارسی که به کار می‌برم غلطه. واقعیت اینه که ترجمه کردن واژه‌ها باعث میشه فهمیدن آموزش سخت بشه و واقعا فایده‌ای هم نداره. از طرفی نمیشه همیشه دستور زبان فارسی رو با واژه‌های انگلیسی به درستی به کاربرد. مثلا subscribe فعل هست ولی من به جای اسم دارم ازش استفاده می‌کنم. ولی وقتی از علوم وارداتی استفاده می‌کنیم این تضادها هم اجتناب ناپذیر میشه. به هر حال هدف من اینجا اینه که طوری بنویسم که تا جای ممکن به ذهن برنامه‌نویسی نزدیک باشه و ترجیح میدم درگیر دستور زبان نشم. امیدوارم عذر من بابت این قصور پذیرفتنی باشه.

4 دیدگاه برای “نگاهی عمیق‌تر به RxJava 2

  1. در مورد استفاده از لغات انگلیسی به جای ترجمه فارسیشون در مورد واژه های کلیدی من هم به شدت با شما موافقم. بهترین کار رو می کنید. وقتی کتاب های ترجمه شده رو می خونم ازین مسئله که برای هر لغتی میخوان معادل فارسیش رو استفاده کنن که هم ناآشنا و هم خیلی وقت ها بی معنی هست رنج می برم.

پاسخ دهید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *