ساخت اپلیکیشن اندروئید (اول)

بخش اول — معماری اندروید

درس اول
اندروید چطوری کار می‌کنه؟

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

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

  • Process فضای واقعی اجرای برنامه‌ست
  • Application نماینده سراسری برنامه در طول عمر همان Process محسوب می‌شه
  • Activity نقطه‌ایه که سیستم از طریق آن رابط کاربری برنامه رو میزبانی و مدیریت می‌کنه
  • Fragment ابزاریه که در معماری مبتنی بر View، رابط کاربری داخل Activity رو به بخش‌های مستقل‌تر تقسیم می‌کنه.

Process یعنی چی؟

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

فرض کن کاربر قیمت‌بان رو باز می‌کنه و فهرست کالاها از سرور دریافت می‌شه. نتیجه درخواست ممکنه داخل چند آبجکت Kotlin قرار بگیره، ViewModel فهرست رو نگه دارد، تصاویر در Cache حافظه قرار بگیرن و تعدادی متغیر وضعیت صفحه رو حفظ کنن. همه این چیزها داخل حافظه Process قیمت‌بان هستن. تا وقتی Process زنده‌ست، این آبجکت‌ها می‌تونن در حافظه باقی بمونن. البته به شرط اینکه Reference معتبری به آن‌ها وجود داشته باشه. اما اگر Process از بین بره، تمام محتوای RAM مربوط به آن هم از بین می‌ره. اینجا فرقی نمی‌کنه متغیر رو داخل Activity گذاشته باشی، ViewModel، یک Singleton یا داخل کلاس Application. اگر آن داده فقط در RAM بوده، با مرگ Process پاک می‌شه.

این موضوع یکی از مهم‌ترین تفاوت‌های برنامه‌نویسی اندروید با یک برنامه سروریه. روی سرور معمولاً خودت کنترل بیشتری روی طول عمر Process داری و اگر سرویس Node.js متوقف بشه، آن رو دوباره راه‌اندازی می‌کنی. اما در اندروید، مالک اصلی Process سیستم‌عامله. اندروید وقتی کدی از برنامه لازم داشته باشه Process رو می‌سازه و وقتی حافظه کم بشه و برنامه دیگر اهمیت بالایی برای کاربر نداشته باشه، ممکنه Process رو بکشه تا RAM آزاد شود. معمولاً Process برنامه‌ای که صفحه‌اش جلوی چشم کاربره اهمیت بیشتری داره و Process برنامه‌ای که مدت‌ها در پس‌زمینه بوده، زودتر قربانی می‌شه. خود برنامه نمی‌تونه فرض کند که Process همیشه زنده می‌مونه.

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

پس Process فقط یک تعریف تئوری نیست. مستقیماً روی معماری برنامه اثر می‌ذاره. داده‌ای که از بین رفتنش مشکلی ایجاد نمی‌کنه، مثل یک نتیجه موقت محاسبه، می‌تونه فقط در حافظه باقی بمونه. اطلاعاتی که برای بازسازی صفحه لازم داری، باید در سازوکاری مثل SavedStateHandle یا ذخیره وضعیت صفحه قرار بگیره. داده‌ای که باید حتی بعد از بستن کامل برنامه باقی بمونه، باید در دیتابیس، فایل، DataStore یا سرور ذخیره شود. Token ورود کاربر هم نباید فقط در یک Singleton حافظه‌ای باشه، چون با مرگ Process پاک می‌شه. در نتیجه، وقتی می‌گیم این اطلاعات داخل ViewModel باقی می‌مونه، منظور این نیست که برای همیشه باقی می‌مونه. ViewModel معمولاً در برابر بازسازی Activity به دلیل تغییراتی مثل چرخش صفحه مقاومت می‌کنه، اما در برابر مرگ Process حافظه‌ای جادویی نداره.

یک سوءبرداشت رایج هم اینه که خارج کردن برنامه از صفحه برنامه‌های اخیر دقیقاً مساوی کشتن Processه. این دو مفهوم رو نباید یکی بدونی. حذف Task از Recent Apps و پایان یافتن Process به رفتار سیستم، نسخه اندروید، سازنده گوشی و وضعیت اجزای برنامه بستگی داره. از طرف دیگر، ممکنه برنامه هنوز در فهرست Recent Apps دیده شود، ولی Process قبلی آن از مدت‌ها قبل کشته شده باشد. چیزی که برای معماری تو اهمیت داره اینه که هیچ‌وقت زنده بودن Process رو تضمین‌شده فرض نکنی.

Application چیه؟

بعد از ساخته شدن Process برنامه، اندروید یک نمونه از کلاس Application مربوط به آن Process ایجاد می‌کنه. البته در حالت معمول که برنامه یک Process اصلی داره. Application رو نباید با کل برنامه یا فایل APK یکی بدونی. Application یک آبجکت واقعی در حافظه‌ست که طول عمرش معمولاً تقریباً برابر با طول عمر Process برنامه‌ست. وقتی Process قیمت‌بان ساخته می‌شه، یک نمونه Application هم ایجاد می‌شه و تا وقتی همان Process زنده‌ست، آن نمونه باقی می‌مونه. وقتی Process از بین بره، Application هم همراه تمام آبجکت‌های حافظه از بین می‌ره. دفعه بعد که برنامه دوباره اجرا شود، Process تازه و Application تازه‌ای ساخته می‌شن.

مهم‌ترین ویژگی Application اینه که به یک صفحه خاص وابسته نیست. Activity ممکنه ساخته و نابود شود، Fragment ممکنه عوض شود، کاربر بین صفحه خانه، جست‌وجو و پروفایل حرکت کند، اما تا وقتی Process زنده‌ست، Application همان نمونه سراسری برنامه باقی می‌مونه. به همین دلیل از Application Context برای کارهایی استفاده می‌شه که به عمر یک صفحه خاص وابسته نیستن. مثلاً اگر یک دیتابیس Room، کلاینت شبکه یا Dependency Container باید برای تمام برنامه قابل‌دسترسی باشه، ممکنه ساخت یا راه‌اندازی اولیه آن به Application وابسته شود. البته در پروژه حرفه‌ای معمولاً این وابستگی با ابزارهایی مثل Hilt مدیریت می‌شه، نه اینکه همه سرویس‌ها رو به‌صورت دستی داخل Application بریزی.

برای قیمت‌بان تصور کن چند صفحه مختلف نیاز دارن به API سرور وصل بشن: صفحه ورود، فهرست کالاها، جزئیات کالا و تنظیمات حساب. منطقی نیست هر Activity یا Fragment یک Retrofit کاملاً جدید بسازد و تنظیمات پایه شبکه رو دوباره انجام بده. بهتره یک زیرساخت مشترک برای کل Process وجود داشته باشه و اجزای مختلف آن رو دریافت کنن. Application می‌تونه نقطه شروع آماده‌سازی چنین زیرساختی باشه. همین موضوع درباره دیتابیس محلی، ثبت گزارش Crash یا راه‌اندازی بعضی کتابخانه‌ها هم صدق می‌کنه.

اما از این توضیح نباید نتیجه بگیری که Application یک انبار عمومی برای هر نوع متغیره. چون دسترسی به آن ساده‌ست، بعضی‌ها تمام داده‌های برنامه رو داخلش قرار می‌دن: کاربر فعلی، فهرست محصولات، وضعیت صفحه، Context یک Activity و ده‌ها متغیر دیگر. این کار معمولاً معماری رو خراب می‌کنه. Application طول عمر زیادی داره و هر چیزی که به آن وصل کنی ممکنه ناخواسته تا پایان Process در حافظه باقی بمونه. بدتر از آن، اگر یک Activity یا View رو داخل Application نگه داری، ممکنه Activity نابود شده باشد، اما به دلیل Reference موجود در Application از حافظه پاک نشود. این همان Memory Leak معروفه.

Application همچنین محل مناسبی برای انجام کارهای سنگین و زمان‌بر در شروع برنامه نیست. متد onCreate آن در مسیر آغاز Process اجرا می‌شه و اگر آنجا دیتابیس سنگین باز کنی، فایل‌های بزرگ بخونی یا درخواست شبکه منتظر بماند، راه‌اندازی برنامه کند می‌شه. بنابراین Application بیشتر جای راه‌اندازی سبک و سراسری محسوب می‌شه، نه جایی که تمام منطق کسب‌وکار برنامه رو اجرا کنی.

یک نکته بسیار مهم اینه که Application حافظه دائمی نیست. فرض کن بعد از ورود کاربر، آبجکت پروفایلش رو داخل Application قرار دادی. تا وقتی Process زنده باشه شاید همه‌چیز درست به نظر برسه، اما بعد از مرگ Process این آبجکت پاک می‌شه. اگر برنامه با این فرض نوشته شده باشه که currentUser همیشه وجود داره، در اجرای بعدی ممکنه با مقدار null یا وضعیت ناقص روبه‌رو شود. برای همین اطلاعات ورود باید از یک منبع پایدار مثل DataStore یا فضای ذخیره‌سازی امن بازیابی شود و Application فقط می‌تونه در مدت زنده بودن Process به دسترسی سریع‌تر کمک کند. نه اینکه جای ذخیره دائمی رو بگیرد.

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

Activity یعنی چی؟

Activity یکی از اجزای اصلی اندرویده و یک نقطه ورود برای تعامل کاربر با رابط کاربری محسوب می‌شه. تفاوت مهمش با یک کلاس معمولی اینه که تو مالک چرخه زندگی آن نیستی. معمولاً خودت با Activity یا new Activity آن رو ایجاد نمی‌کنی. اندروید براساس Manifest، Intent و وضعیت برنامه نمونه Activity رو می‌سازه و متدهای چرخه زندگی آن رو صدا می‌زنه. به همین دلیل Activity بیشتر از اینکه صرفاً یک صفحه باشه، یک جزء تحت مدیریت سیستم‌عامله که پنجره و رابط کاربری برنامه رو میزبانی می‌کنه. در اندروید برخلاف مدل‌هایی که برنامه از تابع main شروع می‌شه، سیستم اجرای کد رابط کاربری رو از طریق Callbackهای Activity آغاز می‌کنه.

در آموزش‌های ابتدایی معمولاً گفته می‌شه هر Activity یک صفحه‌ست. این تعریف برای فهم اولیه بد نیست، اما امروز دقیق نیست. در معماری قدیمی ممکن بود صفحه ورود یک Activity، صفحه فهرست یک Activity و صفحه جزئیات Activity دیگری باشه. اما معماری مدرن اندروید معمولاً به سمت Single Activity رفته. یعنی کل برنامه یک Activity اصلی داره و صفحه‌های مختلف داخل همان Activity با Navigation و Compose یا Fragment جابه‌جا می‌شن. بنابراین در قیمت‌بان ممکنه MainActivity تنها Activity اصلی باشه، اما داخلش صفحه ورود، خانه، جست‌وجو، جزئیات کالا و پروفایل نمایش داده بشن. پس Activity رو بهتره میزبان رابط کاربری و پل ارتباطی با سیستم‌عامل بدونی، نه همیشه معادل یک صفحه. مستندات فعلی اندروید هم معماری تک‌Activity رو برای برنامه‌های مدرن رایج می‌دونن.

چرا اصلاً اندروید به Activity نیاز داره؟ چون رابط کاربری موبایل دائماً تحت تأثیر اتفاق‌های بیرونی قرار می‌گیره. تماس تلفنی میاد، صفحه خاموش می‌شه، کاربر برنامه دیگری رو باز می‌کنه، پنجره مجوز روی صفحه ظاهر می‌شه، اندازه نمایشگر تغییر می‌کنه یا برنامه وارد حالت چندپنجره‌ای می‌شه. سیستم‌عامل باید راهی داشته باشه تا به بخش قابل‌مشاهده برنامه بگه اکنون ساخته شدی، در حال دیده شدنی، آماده تعامل هستی، موقتاً تمرکز رو از دست دادی یا دیگر قابل‌مشاهده نیستی. Activity از طریق چرخه زندگی خودش همین ارتباط رو فراهم می‌کنه.

وقتی Activity ساخته می‌شه، اندروید onCreate رو صدا می‌زنه. اینجا معمولاً رابط کاربری آماده می‌شه و وابستگی‌های اولیه صفحه شکل می‌گیرن. سپس Activity به وضعیت Started و بعد Resumed می‌رسه. در حالت Resumed معمولاً کاربر می‌تونه مستقیم با آن تعامل کند. اگر یک پنجره دیگر جلوی آن قرار بگیره یا کاربر برنامه دیگری رو باز کند، Activity به Pause و سپس شاید Stop برود. اگر دوباره برگردد، Start و Resume اتفاق می‌افتن. اگر Activity واقعاً کنار گذاشته شود، onDestroy هم ممکنه اجرا شود. نکته مهم اینه که نباید منطق برنامه رو بر این فرض بنا کنی که همیشه تمام Callbackها به شکل دلخواهت اجرا می‌شن. مثلاً اگر کل Process ناگهانی کشته شود، فرصت تمیزکاری کامل همیشه تضمین‌شده نیست. چرخه زندگی Activity کمک می‌کنه هر کار رو در زمان مناسب انجام بدی و از مصرف بی‌دلیل منابع یا از دست رفتن تجربه کاربر جلوگیری کنی.

برای مثال، فرض کن در قیمت‌بان اسکن بارکد با دوربین فعال شده. زمانی که Activity یا صفحه مربوطه جلوی کاربره، استفاده از دوربین منطقیه. اگر کاربر تماس دریافت کند و برنامه به پس‌زمینه برود، نباید بی‌دلیل دوربین یا منابع حساس همچنان فعال بمونن. چرخه زندگی به برنامه علامت می‌ده که چه زمانی استفاده از چنین منابعی رو شروع یا متوقف کند. یا اگر صفحه‌ای ویدئو داشت، هنگام خروج کاربر می‌شه پخش رو متوقف کرد. بنابراین Lifecycle فقط چند متد حفظی نیست. قراردادیه که میان سیستم‌عامل و رابط کاربری برنامه برقرار شده.

یکی از مهم‌ترین نکته‌ها اینه که Activity قابل نابودی و بازسازی‌ست. حتی وقتی Process هنوز زنده‌ست، خود Activity ممکنه به دلیل تغییر پیکربندی دوباره ساخته شود. مثال معروفش چرخش گوشی از عمودی به افقیه، چون منابع و Layout مناسب ممکنه تغییر کنن. در چنین حالتی نمونه قبلی Activity نابود و نمونه تازه‌ای ایجاد می‌شه. اگر فهرست نتایج جست‌وجوی قیمت‌بان رو فقط داخل یک متغیر معمولی Activity گذاشته باشی، ممکنه با این بازسازی از دست بره. برای همین داده و منطق اصلی نباید به Activity گره بخورن. راهنمای معماری اندروید هم تأکید می‌کنه که نقش اصلی Activity میزبانی UI است و به دلیل ماهیت موقتی‌اش جای امنی برای نگهداری داده اصلی برنامه نیست.

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

در پروژه قیمت‌بان، Activity بهتره کارهای سطح بالای مربوط به UI رو انجام بده: میزبانی Navigation، اتصال اولیه Compose، مدیریت بعضی رویدادهای سیستمی، دریافت Intent اولیه و هماهنگی کلی رابط کاربری. اما نباید خودش مستقیم مسئول نوشتن Queryهای دیتابیس، تحلیل پاسخ API، ذخیره Token و اجرای تمام قوانین کسب‌وکار باشه. اگر همه‌چیز رو داخل Activity بریزی، چیزی به وجود میاد که به آن God Activity می‌گن. کلاسی بسیار بزرگ که هم رابط کاربریه، هم شبکه، هم دیتابیس، هم منطق تجاری و هم مدیریت State. چنین کدی با هر تغییر کوچکی شکننده‌تر می‌شه.

Fragment یعنی چی؟

Fragment یک بخش قابل‌استفاده مجدد از رابط کاربریه که داخل یک Activity یا گاهی Fragment دیگری میزبانی می‌شه. خودش به‌تنهایی نمی‌تونه مستقل روی صفحه زندگی کنه و باید Host داشته باشه. Fragment می‌تونه Layout خودش رو مدیریت کند، رویدادهای ورودی رو دریافت کند و چرخه زندگی خودش رو داشته باشد. در معماری مبتنی بر View، Fragment راهی بود تا رابط کاربری یک Activity به بخش‌های مستقل‌تر تقسیم شود و جابه‌جایی بین صفحه‌ها بدون ساخت Activityهای متعدد انجام بگیرد.

فرض کن قیمت‌بان با سیستم قدیمی XML و View ساخته شده. یک MainActivity داری و داخل آن صفحه خانه، جست‌وجو، جزئیات کالا و حساب کاربری رو نمایش می‌دی. هرکدام از این صفحه‌ها می‌تونن Fragment جداگانه‌ای باشن. وقتی کاربر از خانه وارد جزئیات کالا می‌شه، FragmentManager می‌تونه Fragment خانه رو با Fragment جزئیات جایگزین کند و این تغییر رو در Back Stack نگه دارد. وقتی کاربر Back رو می‌زند، Fragment قبلی دوباره نمایش داده می‌شه. به این شکل Activity نقش ظرف اصلی رو داره و Fragmentها محتوای داخل ظرف رو عوض می‌کنن.

Fragment فقط برای کم کردن تعداد خطوط Activity ساخته نشده. سه مسئله مهم رو حل می‌کنه: بخش‌بندی رابط کاربری، مدیریت مستقل‌تر چرخه زندگی هر بخش و امکان استفاده از ترکیب‌های متفاوت روی اندازه‌های مختلف صفحه. مثلاً روی گوشی ممکنه فهرست کالاها و جزئیات آن‌ها در دو صفحه جدا نمایش داده بشن، اما روی تبلت هر دو هم‌زمان کنار هم قرار بگیرن. Fragmentها اجازه می‌دن بخش فهرست و بخش جزئیات رو مستقل طراحی کنی و بعد براساس اندازه صفحه آن‌ها رو به شکل‌های مختلف کنار هم بذاری.

اما Fragment از Activity ساده‌تر نیست. در بعضی بخش‌ها حتی پیچیده‌تره. هر Fragment چرخه زندگی خودش رو داره، اما اگر از View استفاده کند، View آن هم چرخه زندگی جداگانه‌ای داره. یعنی ممکنه خود آبجکت Fragment هنوز زنده باشه، ولی View آن نابود شده باشد. این حالت مثلاً زمانی اتفاق می‌افته که Fragment در Back Stack باقی مونده، اما رابط کاربری‌اش دیگر روی صفحه نیست. اگر Reference مربوط به ViewBinding، RecyclerView یا Adapter رو بعد از onDestroyView نگه داری، ممکنه Memory Leak یا خطا ایجاد شود. مستندات اندروید صریحاً چرخه زندگی Fragment و View آن رو جدا می‌دونن و توصیه می‌کنن کارهای وابسته به View فقط تا زمانی انجام بشن که View Lifecycle زنده‌ست.

این تفاوت رو با یک مثال قیمت‌بان می‌شه روشن کرد. فرض کن ProductListFragment روی صفحه فهرست محصولات قرار داره و ViewBinding آن به RecyclerView متصله. کاربر وارد صفحه جزئیات می‌شه و Fragment فهرست در Back Stack نگه داشته می‌شه. ممکنه خود Fragment هنوز وجود داشته باشه تا State و Navigation حفظ شود، اما View قدیمی آن نابود شده باشد تا حافظه آزاد شود. اگر یک پاسخ دیرهنگام API برسد و کد تلاش کند RecyclerView قدیمی رو تغییر بده، با View‌ای روبه‌رو می‌شه که دیگر معتبر نیست. برای همین Observing داده باید به viewLifecycleOwner متصل شود و Referenceهای View در onDestroyView آزاد شوند. این از آن نکته‌هاییه که بدون فهم تفاوت عمر Fragment و عمر View، کاملاً گیج‌کننده به نظر می‌رسه.

حالا ممکنه بپرسی اگر قرار است قیمت‌بان رو با Jetpack Compose بسازیم، Fragment چه اهمیتی داره؟ در یک پروژه کاملاً Composeمحور، معمولاً می‌تونی یک Activity اصلی داشته باشی و صفحه‌ها رو به شکل Composable Destination مدیریت کنی. در این حالت ممکنه اصلاً به Fragment نیاز نداشته باشی. بااین‌حال دانستن Fragment هنوز مفیده، چون پروژه‌های قدیمی فراوانی از آن استفاده می‌کنن و بعضی کتابخانه‌ها یا بخش‌های ترکیبی View و Compose همچنان با Fragment کار دارن. بنابراین برای پروژه جدید، Fragment الزام همیشگی نیست. بیشتر باید بدونی چه مسئله‌ای رو حل می‌کرده و در چه معماری‌هایی حضور داره. معماری مدرن اندروید معمولاً Activity رو ظرف صفحه‌ها یا مقصدهای Compose در نظر می‌گیره.

این چهار مفهوم چطور به هم وصل می‌شن؟

حالا کل مسیر اجرای قیمت‌بان رو کنار هم بذاریم. کاربر روی آیکون برنامه می‌زنه. اگر Process قیمت‌بان از قبل وجود نداشته باشه، اندروید Process جدیدی ایجاد می‌کنه. داخل این Process، نمونه Application ساخته و زیرساخت‌های عمومی برنامه آماده می‌شن. سپس اندروید Activity مقصد رو براساس Manifest و Intent ایجاد می‌کنه. Activity پنجره رابط کاربری رو تحویل می‌گیره و اگر برنامه با Compose ساخته شده باشه، محتوای Compose و Navigation رو میزبانی می‌کنه. اگر برنامه مبتنی بر View و Fragment باشه، Activity می‌تونه Fragment مناسب، مثلاً صفحه خانه، رو داخل خودش قرار بده.

کاربر مدتی با برنامه کار می‌کنه و بعد به تلگرام می‌ره. Activity قیمت‌بان از حالت فعال خارج می‌شه، اما ممکنه هنوز Activity، Application و کل Process در RAM باقی بمونن. اگر کاربر سریع برگرده، برنامه تقریباً از همان وضعیت ادامه پیدا می‌کنه. اگر حافظه گوشی کم شود، اندروید ممکنه Process قیمت‌بان رو بکشه. در این حالت Application، Activity، Fragmentها، ViewModelها، Singletonها و تمام متغیرهای حافظه‌ای یک‌جا از بین می‌رن. اما کارت ظاهری برنامه ممکنه هنوز در Recent Apps دیده شود. کاربر روی آن می‌زند، اندروید Process و Application تازه‌ای می‌سازد و تلاش می‌کند Activity و مسیر قبلی رو بازسازی کند. اگر معماری رو درست چیده باشی، اطلاعات ضروری از دیتابیس، DataStore، Saved State یا سرور دوباره بارگیری می‌شن و کاربر اختلال شدیدی نمی‌بینه. اگر همه‌چیز رو فقط به حافظه سپرده باشی، برنامه با صفحه خالی، وضعیت اشتباه یا Crash برمی‌گرده.

به همین دلیل در اندروید نباید UI رو منبع اصلی حقیقت بدانی. Activity و Fragment اجزای موقتی رابط کاربری هستن و سیستم می‌تونه آن‌ها رو بازسازی کند. ViewModel برای نگهداری و آماده‌سازی State صفحه در برابر بعضی بازسازی‌ها کمک می‌کنه، Repository دسترسی به منابع داده رو هماهنگ می‌کنه و دیتابیس یا سرور اطلاعات ماندگار رو نگه می‌دارن. Application زیرساخت سراسری Process رو آماده می‌کنه و Process هم ظرف موقتی تمام این اجزاست. وقتی جای هرکدام رو درست بفهمی، معماری اندروید دیگر مجموعه‌ای از اسم‌های جدا از هم نیست. یک سیستم منسجم می‌شه که برای زنده ماندن برنامه در محیط محدود و غیرقابل‌پیش‌بینی موبایل طراحی شده.

در یک جمله، Process محیط زنده اجرای قیمت‌بانه. Application آبجکت سراسری هم‌عمر آن محیطه. Activity میزبان تحت کنترل سیستم برای رابط کاربریه. Fragment هم در معماری مبتنی بر View یک بخش مستقل از رابط کاربری داخل Activity محسوب می‌شه. مهم‌تر از همه، هیچ‌کدام جای ذخیره دائمی اطلاعات نیستن و نباید بقای آن‌ها رو تضمین‌شده فرض کنی.


درس دوم

اولین قدم برای ورود به دنیای برنامه‌نویسی اندروید

از نصب Android Studio تا اجرای اولین برنامه

قبل از نصب، باید بدونی داری چی نصب می‌کنی

اولین باری که اسم Android Studio رو می‌شنوی، ممکنه فکر کنی فقط یک نرم‌افزار مثل VS Code یا PhpStormـه که قراره داخلش کد بنویسی. اما واقعیت اینه که Android Studio فقط یک ویرایشگر کد نیست. در واقع مرکز کنترل تمام ابزارهای توسعه اندرویده.

وقتی Android Studio رو نصب می‌کنی، در حقیقت چند ابزار مختلف کنار هم قرار می‌گیرن تا بتونی یک برنامه اندرویدی رو از مرحله نوشتن کد تا اجرا روی گوشی مدیریت کنی. اگر فقط خود Android Studio وجود داشت، نمی‌تونستی برنامه رو کامپایل کنی، Emulator اجرا کنی یا حتی فایل APK بسازی. به همین خاطر گوگل مجموعه‌ای از ابزارها رو کنار هم قرار داده که Android Studio فقط رابط گرافیکی اون‌هاست.

به خاطر همین، وقتی موقع نصب حجم دانلود چند گیگابایت می‌بینی، بخش زیادی از اون مربوط به خود Android Studio نیست. مربوط به ابزارهاییه که پشت صحنه بهش احتیاج داری.


Android Studio دقیقاً چیه؟

اگر بخوام خیلی ساده بگم، Android Studio همون محیطیه که برنامه‌نویس بیشتر روزش رو داخل اون می‌گذرونه. داخلش کد می‌نویسی، فایل‌ها رو مدیریت می‌کنی، خطاها رو می‌بینی، برنامه رو اجرا می‌کنی، دیباگ می‌کنی، Emulator رو باز می‌کنی، Layout رو طراحی می‌کنی و در نهایت نسخه نهایی برنامه رو می‌سازی. اما خودش برنامه اندرویدی تولید نمی‌کنه. در واقع Android Studio فقط ابزارهای دیگه رو مدیریت می‌کنه.


Android SDK چیه؟

اینجا یکی از مفاهیمیه که خیلی‌ها اشتباه متوجهش میشن. فرض کن می‌خوای با یک نفر ژاپنی صحبت کنی. اگر زبان ژاپنی بلد نباشی، هرچقدر هم برنامه‌نویس خوبی باشی، نمی‌تونی باهاش ارتباط برقرار کنی. Android SDK دقیقاً همین نقش رو داره. SDK مخفف Software Development Kit هست. یعنی مجموعه‌ای از کتابخانه‌ها، ابزارها و فایل‌هاییه که اجازه میدن برنامه تو با سیستم‌عامل اندروید حرف بزنه. مثلاً وقتی می‌نویسی:

  • دوربین رو باز کن
  • GPS رو روشن کن
  • فایل ذخیره کن
  • اینترنت وصل شو

در واقع این دستورها رو Android SDK به زبان قابل فهم برای اندروید تبدیل می‌کنه. اگر SDK وجود نداشت، Android Studio فقط یک ادیتور متن بود و هیچ راهی برای ساخت برنامه اندرویدی نداشت.


Android Emulator چیه؟

طبیعتاً هر بار که کدی می‌نویسی، نمی‌خوای گوشی رو از جیبت دربیاری، کابل وصل کنی و برنامه رو اجرا کنی. برای همین گوگل Emulator رو ساخته. Emulator در واقع یک گوشی اندرویدی مجازیه که داخل کامپیوتر اجرا میشه.

این گوشی تقریباً مثل یک موبایل واقعیه. برنامه نصب می‌کنه، اینترنت داره، حافظه داره، دوربین مجازی داره، GPS شبیه‌سازی می‌کنه و حتی می‌تونه تماس یا پیامک رو هم شبیه‌سازی کنه. البته Emulator هیچ‌وقت کاملاً جای گوشی واقعی رو نمی‌گیره. چون سرعت، دوربین، سنسورها، عملکرد گرافیکی و حتی بعضی باگ‌ها فقط روی دستگاه واقعی مشخص میشن. به همین خاطر تقریباً همه برنامه‌نویس‌های اندروید هم Emulator دارن و هم یک گوشی واقعی برای تست نهایی.


اولین پروژه رو می‌سازی

بعد از نصب Android Studio، اولین پروژه رو ایجاد می‌کنی. اینجا چند گزینه می‌بینی که شاید اولش عجیب باشن، اما تقریباً مهم‌ترین تنظیمات اولیه پروژه هستن.

Name

اسم برنامه‌ایه که فعلاً برای پروژه انتخاب می‌کنی.


Package Name

در نگاه اول شبیه آدرس اینترنتیه. مثلاً:

com.priceban.app

اما در واقع شناسه یکتای برنامه‌ست. اندروید برنامه‌ها رو با Package Name می‌شناسه، نه با اسم ظاهری برنامه. اگر بعداً Package Name رو عوض کنی، از دید گوگل‌پلی تقریباً یک برنامه جدید ساخته‌ای.


Minimum SDK

این گزینه مشخص می‌کنه برنامه از چه نسخه‌ای از اندروید به بعد قابل نصب باشه. اگر Minimum SDK رو خیلی پایین انتخاب کنی، برنامه روی گوشی‌های بیشتری نصب میشه، ولی باید با محدودیت‌های نسخه‌های قدیمی کنار بیای. اگر خیلی بالا انتخابش کنی، توسعه راحت‌تر میشه، ولی بخشی از کاربران دیگه نمی‌تونن برنامه رو نصب کنن.

برای پروژه‌های جدید معمولاً نسخه‌های نسبتاً جدید انتخاب میشن، چون سهم نسخه‌های خیلی قدیمی اندروید هر سال کمتر میشه.


Language

اینجا Kotlin رو انتخاب می‌کنیم. چون گوگل الان Kotlin رو زبان اصلی توسعه اندروید می‌دونه و تقریباً تمام آموزش‌های جدید بر پایه Kotlin نوشته میشن.


UI Toolkit

اینجا Jetpack Compose رو انتخاب می‌کنیم. Compose نسل جدید ساخت رابط کاربری اندرویده و نسبت به XML ساده‌تر، مدرن‌تر و قابل نگهداری‌تره.


بعد از ساخت پروژه چه اتفاقی میفته؟

وقتی روی Finish کلیک می‌کنی، Android Studio فقط چند فایل ایجاد نمی‌کنه. در پشت صحنه Gradle شروع می‌کنه پروژه رو آماده کردن. کتابخانه‌های اولیه دانلود میشن، ساختار پروژه ساخته میشه، وابستگی‌ها بررسی میشن و در نهایت پروژه برای اولین Build آماده میشه. به همین خاطر اولین اجرای پروژه معمولاً چند دقیقه طول می‌کشه، اما دفعات بعد خیلی سریع‌تر میشه.


اولین اجرای برنامه

حالا وقتشه اولین برنامه اجرا بشه. اگر Emulator روشن باشه یا گوشی رو با USB Debugging وصل کرده باشی، با زدن دکمه Run، Android Studio پروژه رو کامپایل می‌کنه، فایل APK رو می‌سازه، روی دستگاه نصبش می‌کنه و برنامه اجرا میشه. ممکنه اولین Build چند دقیقه طول بکشه. این کاملاً طبیعیه، چون Gradle برای اولین بار خیلی از فایل‌ها و کتابخانه‌ها رو دانلود و آماده می‌کنه.

وقتی اولین صفحه برنامه روی Emulator یا گوشی ظاهر میشه، در واقع اولین برنامه اندرویدی خودت رو ساختی. حتی اگه فعلاً فقط یک متن ساده روی صفحه نمایش بده.


جمع‌بندی

در این درس فقط Android Studio نصب نکردی. فهمیدی که برای ساخت یک برنامه اندرویدی چند ابزار مختلف کنار هم کار می‌کنن. Android Studio محیط توسعه‌ست، Android SDK زبان مشترک برنامه و سیستم‌عامله، Emulator نقش گوشی مجازی رو بازی می‌کنه و Gradle هم مسئول ساخت و آماده‌سازی پروژه‌ست. از اینجا به بعد، هر بار که برنامه رو اجرا می‌کنی، همه این ابزارها پشت صحنه با هم همکاری می‌کنن تا کدی که نوشتی به یک اپلیکیشن قابل اجرا روی گوشی تبدیل بشه.


چرا از یک نسخه جدید اندروید برای ساخت برنامه استفاده می‌کنیم؟

وقتی داخل ساخت پروژه اندروید می‌بینی نوشته شده Compile SDK = 36، معنی‌اش این نیست که برنامه فقط روی اندروید 36 اجرا میشه. این عدد فقط میگه ما برای ساخت برنامه، از جدیدترین ابزارها و قابلیت‌های اندروید استفاده می‌کنیم. یعنی اندروید استودیو و SDK به ما اجازه میدن از APIها، کتابخانه‌ها و امکانات جدیدتر استفاده کنیم و کد تمیزتر و مدرن‌تری بنویسیم.

مثل این می‌مونه که یک زبان برنامه‌نویسی جدید رو برای نوشتن کتاب انتخاب کنی. قرار نیست فقط آدم‌هایی که آخرین نسخه اون زبان رو دارن بتونن کتاب رو بخونن. فقط ابزار ساخت تو جدیدتر و قدرتمندتر میشه.


چرا Minimum SDK را روی نسخه‌های قدیمی نمی‌گذاریم؟

در مقابل، Minimum SDK مشخص می‌کنه پایین‌ترین نسخه اندرویدی که برنامه ما اجازه نصب روی آن را دارد چیست. مثلاً اگر Minimum SDK را روی Android 8 (API 26) بگذاریم، یعنی گوشی‌هایی که اندرویدشان پایین‌تر از 8 است نمی‌توانند برنامه را نصب کنند.

اوایل شاید وسوسه بشی Minimum SDK را خیلی پایین بگذاری تا تعداد بیشتری کاربر بتوانند برنامه را نصب کنند، اما مشکل اینجاست که باید دائم محدودیت‌های نسخه‌های قدیمی را تحمل کنی. خیلی از قابلیت‌ها، کتابخانه‌ها و روش‌های مدرن اندروید برای نسخه‌های جدید طراحی شده‌اند و پشتیبانی از نسخه‌های خیلی قدیمی باعث می‌شود کد پیچیده‌تر و نگهداری پروژه سخت‌تر شود.

امروز دیگر اندرویدهای خیلی قدیمی مثل Android 6 یا 7 سهم بسیار کمی از کاربران دارند. به همین دلیل در پروژه‌های جدید معمولاً یک تعادل انتخاب می‌کنیم: با Compile SDK جدید (مثلاً API 36) برنامه را مدرن می‌سازیم، ولی Minimum SDK را روی یک نسخه نسبتاً قدیمی و رایج مثل API 26 قرار می‌دهیم تا هم کاربران زیادی پوشش داده شوند، هم مجبور نباشیم محدودیت‌های سال‌های قبل را تحمل کنیم.


تفاوت Debug و Release در خروجی APK

وقتی از پروژه APK می‌گیریم، معمولاً دو نوع خروجی داریم:

Debug APK نسخه‌ای است که برای زمان توسعه و تست خودمان ساخته می‌شود. این نسخه برای برنامه‌نویس است. قابلیت دیباگ دارد، سریع‌تر ساخته می‌شود و برای نصب روی گوشی خودمان و بررسی برنامه استفاده می‌شود.

Release APK نسخه نهایی برنامه است که قرار است به دست کاربر برسد. در این حالت برنامه بهینه‌تر ساخته می‌شود، اطلاعات دیباگ حذف می‌شود و معمولاً باید با یک کلید امنیتی (Signing Key) امضا شود تا بتوانیم آن را در فروشگاه‌ها منتشر کنیم.

پس خلاصه:

  • Debug = برای خودمان، تست و توسعه
  • Release = برای انتشار واقعی و کاربران نهایی

یک نکته هم درباره فولدرها: اگر داخل app/build/outputs/apk بروی، معمولاً:

  • debug/app-debug.apk → همان نسخه تستی
  • release/app-release.apk → نسخه انتشار نهایی

و فولدر androidTest که دیدی، مربوط به APK تست‌های خودکار اندروید است، نه خود برنامه اصلی.


Gradle Daemon چیه؟

وقتی توی اندروید استودیو Build می‌گیری، پشت صحنه یه ابزار به اسم Gradle شروع می‌کنه به انجام دادن کارها. مثل کامپایل کردن کدها، پیدا کردن کتابخونه‌ها و ساختن فایل APK.

اما اگر قرار بود هر بار که Build می‌گیریم، Gradle از صفر شروع بشه، خیلی زمان می‌برد. برای همین چیزی به اسم Gradle Daemon داریم. دیمون در واقع یه نسخه از Gradle هست که در پس‌زمینه باز می‌مونه و آماده است تا دفعه بعدی سریع‌تر کار کنه.

مثلاً دفعه اولی که پروژه رو Build می‌کنی ممکنه کمی طول بکشه، ولی دفعات بعد چون Gradle Daemon از قبل آماده است، فرآیند Build سریع‌تر انجام میشه.

گاهی هم ممکنه همین دیمون به مشکل بخوره. مثلاً همون خطایی که دیدیم:

Could not connect to Kotlin compile daemon

یعنی اندروید استودیو نتونسته به پردازشی که مسئول کامپایل کدهای Kotlin هست وصل بشه. معمولاً با Restart کردن، پاک کردن Cache یا تغییر تنظیمات Gradle مشکل حل میشه.

به زبان ساده:

Gradle = کارگری که پروژه رو می‌سازه
Gradle Daemon = همون کارگره که همیشه آماده کنار پروژه ایستاده تا دفعه بعد سریع‌تر کار کنه.


فایل‌های Kotlin با پسوند .kt و .kts چه هستند؟

در پروژه اندروید بیشتر کدهایی که ما می‌نویسیم با زبان Kotlin هستند و معمولاً پسوندشان .kt است. مثل فایل‌هایی که داخل آن‌ها Activity، Composable، کلاس‌ها و منطق برنامه را می‌نویسیم.

اما فایل‌های .kts هم با زبان Kotlin نوشته می‌شوند، با این تفاوت که معمولاً برای تنظیمات Gradle استفاده می‌شوند. مثلاً فایل:

build.gradle.kts

با Kotlin نوشته شده، ولی به جای منطق برنامه، تنظیمات ساخت پروژه داخل آن قرار دارد. مثل نسخه اندروید، کتابخانه‌ها و تنظیمات Build.

خلاصه:

  • .kt → کد اصلی برنامه
  • .kts → تنظیمات ابزار ساخت پروژه (Gradle)

فایل AndroidManifest.xml چیست؟

فایل AndroidManifest.xml مثل شناسنامه‌ی برنامه است. اندروید قبل از اینکه برنامه را اجرا کند، اول این فایل را می‌خواند تا بفهمد این برنامه چیست و چه چیزهایی دارد.

داخل آن اطلاعات مهمی مثل نام برنامه، Activityهای اصلی، دسترسی‌ها (Permissionها)، سرویس‌ها و تنظیمات کلی اپلیکیشن قرار می‌گیرد.

در واقع کد Kotlin به اندروید می‌گوید برنامه چطور کار کند، ولی Manifest به اندروید می‌گوید این برنامه چه ساختاری دارد و سیستم‌عامل چطور باید با آن رفتار کند.


Windows Hypervisor Platform چیست؟

وقتی می‌خوایم Emulator اندروید رو اجرا کنیم، در واقع داریم یک گوشی مجازی داخل کامپیوتر می‌سازیم. برای اینکه این گوشی مجازی با سرعت خوبی اجرا بشه، نیاز داریم از قابلیت مجازی‌سازی پردازنده استفاده کنیم.

Windows Hypervisor Platform یکی از قابلیت‌های ویندوز هست که به Android Emulator اجازه می‌ده مستقیم از قدرت CPU استفاده کنه و اجرای شبیه‌ساز سریع‌تر و روان‌تر باشه.

اگر این قابلیت فعال نباشه، ممکنه با خطایی مثل:

Windows Hypervisor Platform is not enabled

مواجه بشیم. یعنی ویندوز اجازه استفاده از امکانات مجازی‌سازی رو به Emulator نداده.

برای حل این مشکل باید از بخش Windows Features ویندوز، گزینه‌ی Windows Hypervisor Platform رو فعال کنیم. همچنین داخل BIOS هم باید قابلیت مجازی‌سازی پردازنده روشن باشه. در پردازنده‌های AMD معمولاً با نام SVM Mode و در پردازنده‌های Intel با نام VT-x شناخته میشه.

به زبان ساده:

Emulator یعنی یک گوشی اندرویدی داخل کامپیوتر.
Hypervisor یعنی موتوری که کمک می‌کنه این گوشی مجازی با سرعت واقعی اجرا بشه.


چرا از Pixel 8a در Emulator استفاده می‌کنیم؟

وقتی در Android Studio یک شبیه‌ساز می‌سازیم، در واقع داریم یک گوشی مجازی ایجاد می‌کنیم. Pixel 8a یکی از دستگاه‌های پیشنهادی گوگل برای تست برنامه‌هاست، چون گوشی‌های Pixel مستقیماً توسط گوگل توسعه داده می‌شوند و معمولاً زودتر از بقیه دستگاه‌ها نسخه‌های جدید اندروید را دریافت می‌کنند.

به همین دلیل وقتی می‌خواهیم ببینیم برنامه روی یک دستگاه استاندارد اندروید چطور رفتار می‌کند، Pixel انتخاب خوبی است. چون مشکلاتی که ممکن است از رابط‌های اختصاصی شرکت‌هایی مثل شیائومی یا سامسونگ ایجاد شود، کمتر وجود دارد.


تفاوت Pixel 8a با گوشی واقعی ما چیست؟

Pixel 8a و گوشی واقعی ما فقط از نظر مدل دستگاه و مشخصات صفحه‌نمایش متفاوت هستند. مثلاً ممکن است نسبت تصویر، اندازه صفحه، تراکم پیکسل یا حتی نسخه اندرویدشان فرق کند.

این موضوع مهم است چون یک برنامه باید روی اندازه‌ها و نسبت‌های مختلف صفحه درست نمایش داده شود. برای همین بهتر است علاوه بر Emulator، گاهی برنامه را روی گوشی واقعی خودمان هم تست کنیم.


آیا برای هر مدل گوشی باید یک Emulator جدید دانلود کنیم؟

نه، لازم نیست برای هر دستگاه یک Emulator جداگانه نصب کنیم. در Android Studio می‌توانیم از یک دستگاه مجازی، چند Profile مختلف بسازیم.

مثلاً می‌توانیم همان Pixel 8a را کپی کنیم و فقط مشخصاتش را تغییر دهیم. مثل:

  • اندازه صفحه
  • رزولوشن
  • مقدار RAM
  • نسخه اندروید

به این ترتیب بدون دانلود دوباره فایل‌های سنگین، می‌توانیم برنامه را روی حالت‌های مختلف آزمایش کنیم.

به زبان ساده:

Emulator مثل یک گوشی آزمایشی داخل کامپیوتر است.
Device Profile فقط ظاهر و مشخصات آن گوشی مجازی را تغییر می‌دهد.


چرا Kotlin مسیر سرراست‌تری نسبت به React Native است؟

وقتی با ابزارهایی مثل Flutter یا React Native کار می‌کنیم، بخش زیادی از کارها راحت‌تر می‌شود، اما همیشه یک فاصله بین کد ما و خود سیستم‌عامل اندروید وجود دارد.

مثلاً حدود ۸۰ تا ۸۵ درصد کارهای معمولی را خود فریم‌ورک انجام می‌دهد، اما وقتی به بخش‌هایی مثل دوربین، بلوتوث، سنسورها، سرویس‌های پس‌زمینه یا قابلیت‌های خاص گوشی می‌رسیم، گاهی باید سراغ Plugin و Dependencyهای مختلف برویم.

مشکل اینجاست که همین Pluginها می‌توانند بعداً دردسر درست کنند. ممکن است یک کتابخانه با نسخه جدید اندروید سازگار نباشد، یک Dependency با دیگری تداخل داشته باشد یا مجبور شویم زمان زیادی صرف پیدا کردن راه‌حل کنیم.

اما وقتی با Kotlin و Android Native کار می‌کنیم، مستقیم با خود ابزارهای رسمی اندروید کار می‌کنیم. دیگر یک لایه اضافی بین ما و سیستم‌عامل وجود ندارد و برای قابلیت‌های اصلی گوشی، همان APIهایی را استفاده می‌کنیم که خود اندروید در اختیار توسعه‌دهنده‌ها گذاشته است.

برای همین، اگر هدف ما شناخت عمیق اندروید و ساخت برنامه‌هایی باشد که بیشترین هماهنگی را با خود سیستم‌عامل دارند، مسیر Native با Kotlin معمولاً ساده‌تر و قابل پیش‌بینی‌تر است.

به زبان ساده:

در Flutter و React Native، گاهی اول باید مشکل فریم‌ورک و Plugin را حل کنیم تا به اندروید برسیم.
در Kotlin Native، مستقیم سراغ خود اندروید می‌رویم.


درس سوم

اولین قدم برای کدنویسی به زبان Kotlin

از JavaScript به Kotlin

چرا Kotlin به وجود اومد؟

قبل از اینکه حتی یک خط Kotlin بنویسی، خوبه بدونی اصلاً چرا این زبان ساخته شد. چون Kotlin قرار نبود یک زبان کاملاً جدید باشه که همه چیز رو از اول اختراع کنه. هدفش این بود که مشکلات Java رو برطرف کنه.

سال‌ها برنامه‌های اندروید فقط با Java نوشته می‌شدن. Java زبان قدرتمندی بود، اما یک مشکل بزرگ داشت. برای انجام خیلی از کارهای ساده باید کد زیادی می‌نوشتی. علاوه بر اون، یکی از رایج‌ترین دلایل کرش کردن برنامه‌های اندرویدی، خطاهای مربوط به NullPointerException بود. یعنی برنامه سعی می‌کرد به متغیری دسترسی پیدا کنه که هیچ مقداری نداشت. شرکت JetBrains که سازنده IntelliJ IDEA بود، تصمیم گرفت زبانی طراحی کنه که هم با Java کاملاً سازگار باشه، هم خواناتر باشه، هم کدنویسی رو کوتاه‌تر کنه و هم جلوی خیلی از خطاهای رایج رو قبل از اجرای برنامه بگیره. نتیجه این پروژه شد Kotlin. گوگل هم بعد از مدتی Kotlin رو به عنوان زبان اصلی توسعه اندروید معرفی کرد. امروز تقریباً تمام پروژه‌های جدید اندروید با Kotlin نوشته میشن و اکثر آموزش‌ها و کتابخونه‌های جدید هم بر پایه همین زبان هستن.


آیا باید Java هم بلد باشی؟

یکی از سؤال‌هایی که تقریباً همه موقع شروع برنامه‌نویسی اندروید می‌پرسن اینه که آیا قبل از Kotlin باید Java یاد بگیرن؟ جواب کوتاه اینه: نه.

چند سال پیش جواب این سؤال بله بود، چون تقریباً همه منابع آموزشی و پروژه‌های اندرویدی با Java نوشته می‌شدن. اما امروز شرایط کاملاً فرق کرده. اگر هدفت ساخت اپلیکیشن‌های جدید اندرویده، یاد گرفتن Kotlin کاملاً کافیه. البته ممکنه گاهی داخل اینترنت یا پروژه‌های قدیمی به کدهای Java برخورد کنی، اما چون Kotlin و Java کاملاً با هم سازگارن، کم‌کم در طول کار با ظاهر Java هم آشنا میشی. بنابراین لازم نیست قبل از شروع چند ماه وقت بذاری و Java رو کامل یاد بگیری.


اگر JavaScript بلدی، نصف راه رو اومدی

خبر خوب اینه که تو قرار نیست برنامه‌نویسی رو از اول یاد بگیری. تو از قبل با متغیر، شرط، حلقه، تابع، کلاس، آرایه و آبجکت آشنایی. فقط قراره یاد بگیری همین مفاهیم داخل Kotlin چه شکلی نوشته میشن. به همین خاطر این درس آموزش برنامه‌نویسی نیست. آموزش یک زبان جدیده.

مثلاً اگر قبلاً با JavaScript کار کردی، خیلی از چیزها برات آشنا به نظر میرسن. هنوز هم متغیر تعریف می‌کنی، هنوز هم تابع می‌نویسی، هنوز هم شرط و حلقه داری. فقط شکل نوشتنشون فرق کرده و در بعضی جاها Kotlin قوانین سخت‌گیرانه‌تری داره تا برنامه مطمئن‌تر و کم‌خطاتری نوشته بشه.


Kotlin با JavaScript چه فرقی داره؟

بزرگ‌ترین تفاوتی که از همون روز اول حس می‌کنی اینه که Kotlin نسبت به JavaScript خیلی کمتر اجازه حدس زدن میده. مثلاً در JavaScript ممکنه یک متغیر امروز عدد باشه، فردا رشته بشه و پس‌فردا هم null بگیره. این آزادی گاهی سرعت توسعه رو بالا می‌بره، اما اگر پروژه بزرگ بشه، پیدا کردن خطاها سخت‌تر میشه.

در Kotlin اوضاع فرق می‌کنه. این زبان از همون ابتدا سعی می‌کنه نوع هر متغیر رو مشخص کنه و قبل از اینکه برنامه اجرا بشه، خیلی از اشتباه‌ها رو بهت هشدار بده. شاید اولش این سخت‌گیری کمی عجیب به نظر برسه، اما بعد از مدتی می‌بینی باعث میشه موقع اجرای برنامه با خطاهای خیلی کمتری روبه‌رو بشی.


var و val
اولین تفاوتی که می‌بینی

اگر در JavaScript از let و const استفاده کردی، یاد گرفتن var و val خیلی ساده‌ست. var برای متغیرهاییه که قراره مقدارشون تغییر کنه. اما val برای متغیرهاییه که بعد از مقداردهی، دیگه نباید مقدار جدیدی بهشون بدی.

به طور کلی، برنامه‌نویس‌های Kotlin سعی می‌کنن تا جای ممکن از val استفاده کنن و فقط وقتی واقعاً لازم باشه سراغ var برن. این کار باعث میشه کد قابل پیش‌بینی‌تر و احتمال بروز خطا کمتر بشه.


چرا Kotlin اینقدر روی null حساسه؟

یکی از مهم‌ترین ویژگی‌های Kotlin همین بخشه. در JavaScript خیلی راحت ممکنه با null یا undefined روبه‌رو بشی و اگر حواست نباشه، برنامه وسط اجرا خطا بده. اما Kotlin ازت می‌پرسه: آیا این متغیر ممکنه null باشه یا نه؟

اگر جواب نه باشه، خود زبان اجازه نمیده بعداً اشتباهی مقدار null داخلش قرار بگیره. به همین خاطر بخش بزرگی از خطاهایی که قبلاً موقع اجرای برنامه اتفاق می‌افتادن، حالا قبل از اجرا و موقع کامپایل پیدا میشن. این شاید مهم‌ترین دلیلیه که Kotlin نسبت به Java امنیت بیشتری داره.


لازم نیست همه Kotlin رو همین الان یاد بگیری

اشتباهی که خیلی از افراد موقع شروع اندروید انجام میدن اینه که اول میرن یک دوره ۵۰ ساعته Kotlin رو کامل می‌بینن و بعد تازه میخوان برنامه اندرویدی بنویسن. این روش معمولاً نتیجه خوبی نمیده، چون بخش زیادی از مطالب رو قبل از اینکه بهشون احتیاج پیدا کنی فراموش می‌کنی. روش بهتر اینه که Kotlin رو هم‌زمان با ساخت پروژه یاد بگیری. هر بار که به یک قابلیت جدید احتیاج داشتی، همون قسمت از زبان رو یاد بگیر و بعد بلافاصله داخل پروژه ازش استفاده کن. اینطوری هم یادگیری سریع‌تره و هم مطالب خیلی بهتر توی ذهنت می‌مونن.


جمع‌بندی

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


درس چهارم

اولین آشنایی با فایل‌ها و پوشه‌های پروژه

داخل پروژه اندروید دقیقاً چه خبره؟

اولین باری که پروژه رو باز می‌کنی

اگر تا حالا فقط با پروژه‌های HTML، PHP یا Node.js کار کرده باشی، اولین باری که یک پروژه اندروید رو باز می‌کنی احتمالاً کمی شوکه میشی. به جای چند فایل ساده، با ده‌ها پوشه و صدها فایل روبه‌رو میشی و شاید با خودت بگی: واقعاً باید همه اینا رو یاد بگیرم؟ خبر خوب اینه که نه.

در یک پروژه اندروید، شاید بیشتر از صد فایل وجود داشته باشه، اما برنامه‌نویس معمولاً فقط با تعداد محدودی از اون‌ها سر و کار داره. خیلی از فایل‌ها رو خود Android Studio یا Gradle مدیریت می‌کنن و شاید هیچ‌وقت لازم نباشه مستقیم بهشون دست بزنی. هدف این درس اینه که بفهمی هر بخش چه مسئولیتی داره، نه اینکه تک‌تک فایل‌های پروژه رو حفظ کنی.


Manifest
شناسنامه برنامه

اگر بخوام فقط یک فایل رو به عنوان مهم‌ترین فایل پروژه معرفی کنم، اون AndroidManifest.xml هست. اندروید قبل از اینکه حتی یک خط از برنامه‌ات رو اجرا کنه، اول Manifest رو می‌خونه. این فایل مثل شناسنامه برنامه عمل می‌کنه و اطلاعات پایه‌ای برنامه داخلش قرار داره.

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


Gradle
مدیر ساخت پروژه

یکی از چیزهایی که اول کار ممکنه گیجت کنه Gradle هست. خیلی‌ها فکر می‌کنن Gradle فقط برای دانلود کتابخونه‌هاست، در حالی که این فقط یکی از وظایفشه. Gradle در واقع مدیر ساخت پروژه‌ست. هر بار که دکمه Run رو میزنی، این Gradle هست که تصمیم می‌گیره چه فایل‌هایی کامپایل بشن، چه کتابخونه‌هایی دانلود بشن، فایل APK یا AAB چطور ساخته بشه و در نهایت برنامه چطور روی گوشی نصب بشه.

اگر پروژه رو به یک کارخانه تشبیه کنیم، Gradle مدیر خط تولیده. خودش محصول نیست، اما کل فرآیند تولید رو مدیریت می‌کنه. به همین خاطر هم اولین Build معمولاً زمان بیشتری طول می‌کشه، چون Gradle باید وابستگی‌ها رو بررسی و ابزارهای لازم رو آماده کنه.


Resources
همه چیزهایی که کد نیست

تقریباً هر چیزی که کد برنامه نباشه داخل پوشه res قرار می‌گیره. آیکن‌ها، رنگ‌ها، فونت‌ها، متن‌ها، تصاویر، انیمیشن‌ها، فایل‌های طراحی رابط کاربری و خیلی از منابع دیگه داخل این پوشه نگهداری میشن. این کار باعث میشه اطلاعات از هم جدا باشن. مثلاً اگر بعداً بخوای رنگ اصلی برنامه رو عوض کنی، لازم نیست کل پروژه رو بگردی. فقط مقدار همون رنگ داخل Resources تغییر می‌کنه.

یا اگر بخوای برنامه چندزبانه بشه، متن‌ها داخل همین بخش قرار می‌گیرن و لازم نیست متن‌ها رو داخل کدها تغییر بدی. هرچه پروژه بزرگ‌تر بشه، اهمیت این جداسازی بیشتر مشخص میشه.


Assets
فایل‌هایی که همون‌طور باید باقی بمونن

گاهی فایل‌هایی داری که اندروید نباید اون‌ها رو پردازش یا تغییر بده. مثلاً ممکنه یک فایل PDF، یک فایل JSON، یک مدل هوش مصنوعی، فونت اختصاصی یا هر فایل دیگه‌ای داشته باشی که برنامه باید دقیقاً همون فایل اصلی رو بخونه.

این فایل‌ها معمولاً داخل پوشه assets قرار می‌گیرن. تفاوت اصلی Assets با Resources اینه که فایل‌های داخل Resources هنگام Build پردازش و بهینه میشن، اما فایل‌های داخل Assets تقریباً بدون تغییر داخل برنامه قرار می‌گیرن و هر وقت لازم باشه خودت اون‌ها رو می‌خونی.

برخلاف پوشه‌ی res که از ابتدا وجود داره، پوشه‌ی assets به‌صورت پیش‌فرض ساخته نمی‌شه. هر زمان به فایل‌هایی مثل PDF، JSON، فونت یا مدل هوش مصنوعی نیاز داشته باشیم، خودمون این پوشه رو داخل app/src/main ایجاد می‌کنیم.

نکته: برخلاف res، فایل‌های داخل assets شناسه‌ی R ندارن. یعنی مثلاً نمی‌تونی بنویسی R.assets.myfile بلکه به این صورت باید بخونی.

assets.open(...)


Theme
ظاهر کلی برنامه

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

اگر Resources رو جعبه ابزار طراحی بدونیم، Theme کسیه که تصمیم می‌گیره از هر کدوم چطور استفاده بشه. به همین خاطر وقتی بخوای ظاهر کل برنامه رو تغییر بدی، معمولاً به جای اینکه صدها صفحه رو جداگانه ویرایش کنی، فقط Theme رو تغییر میدی.


لازم نیست همه فایل‌ها رو از روز اول بشناسی

یکی از اشتباه‌های رایج اینه که برنامه‌نویس تازه‌کار سعی می‌کنه از همون روز اول تک‌تک فایل‌های پروژه رو یاد بگیره. این کار نه لازم و نه حتی ممکنه. در عمل، هر بار که وارد یک مبحث جدید میشی، فقط با چند فایل مشخص کار داری. مثلاً وقتی درباره مجوزها صحبت کنیم، دوباره سراغ Manifest میایم. وقتی کتابخونه جدید نصب کنیم، دوباره Gradle رو می‌بینیم. وقتی درباره طراحی رابط کاربری حرف بزنیم، بیشتر وقتمون داخل Resources و Theme می‌گذره. بنابراین فعلاً فقط کافیه بدونی هر بخش چه مسئولیتی داره. جزئیات هر کدوم رو دقیقاً همون زمانی یاد می‌گیری که واقعاً بهش احتیاج پیدا کنی.


جمع‌بندی

هر پروژه اندرویدی ممکنه صدها فایل داشته باشه، اما قرار نیست همه اون‌ها رو حفظ کنی. فعلاً فقط بدون که Manifest شناسنامه برنامه‌ست، Gradle مسئول ساخت و آماده‌سازی پروژه‌ست، Resources محل نگهداری منابع برنامه‌ست، Assets برای فایل‌هایی استفاده میشه که باید بدون تغییر داخل برنامه باقی بمونن و Theme هم ظاهر کلی برنامه رو کنترل می‌کنه. وقتی این نقشه ذهنی رو داشته باشی، از این به بعد هر بار وارد پروژه بشی، می‌دونی هر فایل تقریباً کجای این پازل قرار می‌گیره.


درس پنجم

آشنایی با روش جدید ساخت رابط کاربری در اندروید

Jetpack Compose
چرا گوگل XML رو کنار گذاشت؟

Jetpack Compose چارچوب جدید گوگل برای ساخت رابط کاربری اندرویده. یعنی همون بخشی از برنامه که کاربر می‌بینه و باهاش تعامل می‌کنه. مثل متن‌ها، دکمه‌ها، فرم‌ها، لیست‌ها، کارت‌ها، منوها و صفحه‌های مختلف. در روش قدیمی، ظاهر برنامه داخل فایل‌های XML ساخته می‌شد و منطقش داخل Kotlin یا Java قرار می‌گرفت. اما در Jetpack Compose خود رابط کاربری هم با Kotlin نوشته میشه. پس Compose فقط یک ابزار طراحی نیست. روشی جدیده که ظاهر، رفتار و وضعیت صفحه رو داخل یک مدل واحد کنار هم قرار میده.

کلمه Compose به معنی ترکیب کردن یا کنار هم چیدنه. ایده اصلیش اینه که رابط کاربری از قطعه‌های کوچیک ساخته بشه. مثلاً صفحه اصلی قیمت‌بان می‌تونه از نوار جست‌وجو، فیلتر، لیست کالاها، کارت هر کالا و دکمه افزودن قیمت تشکیل بشه. هر کدوم از این قسمت‌ها یک قطعه مستقل هستن و بعد با ترکیب اون‌ها صفحه کامل ساخته میشه. این قطعه‌ها در Compose با توابعی به نام Composable تعریف میشن. یعنی به جای اینکه داخل XML یک View بسازی و بعد در Kotlin پیداش کنی، مستقیم در Kotlin توضیح میدی که چه چیزی باید روی صفحه دیده بشه.

علت ساخته شدن Jetpack Compose فقط این نبود که گوگل از XML خوشش نمی‌اومد. XML سال‌ها ابزار اصلی توسعه اندروید بود و هنوز هم خیلی از برنامه‌ها با همون ساخته شدن. مشکل از جایی شروع شد که رابط‌های کاربری پیچیده‌تر و پویاتر شدن. در روش قدیمی، برنامه‌نویس باید خودش اجزای صفحه رو پیدا می‌کرد و هر تغییر رو به صورت دستوری انجام می‌داد. مثلاً اگر قیمت یک کالا تغییر می‌کرد، باید آیتم مربوط به اون کالا پیدا می‌شد، متن جدید داخلش قرار می‌گرفت، شاید رنگش تغییر می‌کرد و بعد لیست هم دوباره به‌روزرسانی می‌شد. هرچه صفحه پیچیده‌تر می‌شد، تعداد این دستورها هم بیشتر می‌شد و احتمال اینکه رابط کاربری با داده واقعی هماهنگ نباشه بالا می‌رفت.

Jetpack Compose این مسئله رو با یک رویکرد توصیفی حل می‌کنه. یعنی به جای اینکه به برنامه بگی این TextView رو پیدا کن و مقدارش رو تغییر بده، فقط توضیح میدی که رابط کاربری در وضعیت فعلی باید چه شکلی باشه. مثلاً میگی اگر اطلاعات هنوز دریافت نشده، علامت Loading نمایش داده بشه. اگر دریافت اطلاعات موفق بود، لیست قیمت‌ها نمایش داده بشه. اگر خطا رخ داد، پیام خطا و دکمه تلاش مجدد دیده بشه. بعد هر وقت وضعیت برنامه تغییر کنه، Compose خودش رابط کاربری مناسب رو دوباره تولید می‌کنه.

این تفاوت خیلی بنیادیه. در روش XML معمولاً خودت Viewها رو مدیریت می‌کردی، اما در Compose وضعیت برنامه رو مدیریت می‌کنی. رابط کاربری نتیجه وضعیت فعلیه. یعنی اگر State تغییر کنه، Compose بررسی می‌کنه کدوم بخش صفحه به اون State وابسته بوده و فقط همون بخش رو دوباره اجرا و به‌روزرسانی می‌کنه. به این فرایند Recomposition گفته میشه. Recomposition به معنی این نیست که کل صفحه از صفر نابود و ساخته میشه. Compose تلاش می‌کنه فقط بخش‌هایی رو تغییر بده که واقعاً به‌روزرسانی شدن.

فرض کن در قیمت‌بان صفحه‌ای داری که قیمت امروز یک کالا، درصد تغییر و زمان آخرین به‌روزرسانی رو نشون میده. وقتی فقط قیمت تغییر می‌کنه، لازم نیست برنامه همه اجزای صفحه رو دوباره مدیریت کنه. State قیمت عوض میشه و Compose بخش‌هایی رو که به اون قیمت وابسته هستن به‌روزرسانی می‌کنه. بنابراین رابطه بین داده و ظاهر صفحه مستقیم‌تر و قابل پیش‌بینی‌تر میشه.

برای کسی که React بلده، منطق Jetpack Compose خیلی آشناست. در React هم رابط کاربری از Componentهای کوچیک ساخته میشه، State تغییر می‌کنه و رابط کاربری دوباره بر اساس State رندر میشه. Composable در Compose تقریباً نقشی شبیه Component در React داره. البته Compose کپی React نیست و روی معماری و محدودیت‌های خود اندروید ساخته شده، اما طرز فکر هر دو به هم نزدیکه: رابط کاربری باید تابعی از داده و وضعیت برنامه باشه.

یکی دیگه از مزیت‌های Jetpack Compose اینه که وابستگی بین فایل XML و فایل Kotlin رو کم می‌کنه. در روش قدیمی ممکن بود یک دکمه داخل XML تعریف بشه، شناسه داشته باشه، بعد در Activity یا Fragment به اون شناسه وصل بشی و در فایل دیگه رفتارش رو تعریف کنی. در پروژه‌های بزرگ این رفت‌وآمد بین فایل‌ها زیاد می‌شد. در Compose ساختار صفحه داخل Kotlin قرار می‌گیره و راحت‌تر می‌تونی ببینی هر قسمت چه داده‌ای می‌گیره و در چه شرایطی نمایش داده میشه.

البته این به معنی کنار هم ریختن بی‌قاعده ظاهر و منطق تجاری نیست. Compose اجازه میده ظاهر صفحه رو با Kotlin بنویسی، اما هنوز هم نباید درخواست شبکه، کار با دیتابیس یا منطق اصلی برنامه رو داخل Composableها قرار بدی. Composable باید بیشتر مسئول نمایش وضعیت و ارسال رویدادهای کاربر باشه. مثلاً صفحه قیمت‌بان اطلاعات رو از ViewModel می‌گیره، اون‌ها رو نمایش میده و وقتی کاربر روی دکمه جست‌وجو می‌زنه، فقط رویداد رو به ViewModel منتقل می‌کنه.

پس Jetpack Compose فقط XML با شکل جدید نیست. Compose یک مدل جدید برای فکر کردن به رابط کاربریه. در این مدل، صفحه از توابع کوچیک و قابل ترکیب ساخته میشه، ظاهر با Kotlin نوشته میشه، State منبع اصلی حقیقته و رابط کاربری با تغییر State خودش به‌روزرسانی میشه. گوگل XML رو به‌طور کامل حذف نکرده، اما برای پروژه‌های جدید مسیر اصلی توسعه اندروید رو به سمت Compose برده، چون ساخت رابط‌های پویا با این مدل ساده‌تر، قابل پیش‌بینی‌تر و قابل نگهداری‌تره.


درس ششم

آشنایی با آجرهای سازنده رابط کاربری

Jetpack Compose صفحه‌ها رو چطور می‌سازه؟

بعد از اینکه فهمیدی Jetpack Compose چی هست، حالا وقتشه ببینی اصلاً یک صفحه داخل Compose چطور ساخته میشه.

اگر قبلاً HTML کار کرده باشی، احتمالاً عادت داری صفحه رو از تگ‌های مختلف بسازی. یک <div>، داخلش چند <div> دیگه، بعد یک عکس، یک متن، یک دکمه و همین‌طور لایه‌به‌لایه جلو بری. Compose هم تقریباً همین ایده رو داره، اما به جای تگ‌های HTML، از Composableها استفاده می‌کنه. یعنی هر چیزی که روی صفحه می‌بینی، از کنار هم قرار گرفتن چند قطعه کوچیک ساخته شده. هیچ چیز جادویی وجود نداره. یک صفحه پیچیده فروشگاه یا اینستاگرام هم در نهایت از تعداد زیادی قطعه ساده تشکیل شده که روی هم قرار گرفتن.

نکته مهم اینه که Compose خودش تصمیم نمی‌گیره اجزا کجا قرار بگیرن. این وظیفه برنامه‌نویسه که ساختار صفحه رو مشخص کنه. برای همین چند Composable پایه در اختیارمون قرار داده که تقریباً ستون فقرات تمام صفحه‌های اندروید هستن.

اولین و شاید پرکاربردترین اون‌ها Column هست. Column یعنی هر چیزی که داخلش قرار بدی، از بالا به پایین چیده میشه. فرض کن صفحه جزئیات یک کالا در قیمت‌بان رو می‌سازی. اسم کالا، قیمت، نمودار، دکمه ثبت قیمت و نظرات کاربران همگی زیر هم قرار می‌گیرن. این دقیقاً کاریه که Column انجام میده. خودش چیز خاصی نمایش نمیده. فقط میگه فرزندهای من به صورت عمودی کنار هم قرار بگیرن.

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

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

برای حل این مشکل LazyColumn به وجود اومده. کلمه Lazy یعنی تنبل. البته منظور از تنبلی اینجا مثبت و هوشمندانه‌ست. LazyColumn تا وقتی لازم نباشه چیزی نمی‌سازه. اگر روی صفحه فقط ده کالا دیده میشه، فقط همون ده تا ساخته میشن. وقتی کاربر اسکرول می‌کنه، آیتم‌های جدید ساخته میشن و آیتم‌هایی که از صفحه خارج شدن، آزاد میشن یا دوباره مورد استفاده قرار می‌گیرن. به همین خاطر تقریباً هر لیستی که تعداد آیتم‌هاش مشخص نیست یا ممکنه زیاد باشه، با LazyColumn ساخته میشه. در عمل، هر وقت بخوای لیستی از داده‌های دیتابیس یا اینترنت رو نمایش بدی، احتمال خیلی زیاد از LazyColumn استفاده خواهی کرد، نه Column.

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

حالا سؤال مهمی پیش میاد. اگر بخوام این Card بزرگ‌تر باشه چی؟ اگر بخوام فاصله بیشتری از لبه صفحه داشته باشه چی؟ اگر بخوام رنگش عوض بشه یا گوشه‌هاش گردتر بشن چی؟

اینجاست که به مهم‌ترین مفهوم Compose می‌رسیم. Modifier. اگر Column، Row و Card رو مثل قطعات لگو در نظر بگیری، Modifier ابزاریه که شکل اون قطعات رو تغییر میده. تقریباً هر Composable در Compose یک Modifier می‌گیره. با Modifier مشخص می‌کنی اندازه یک عنصر چقدر باشه، کجا قرار بگیره، چقدر فاصله داخلی یا خارجی داشته باشه، پس‌زمینه‌اش چه رنگی باشه، گوشه‌هاش گرد باشه یا نه، قابل کلیک باشه یا نباشه و ده‌ها ویژگی دیگه.

به مرور متوجه میشی که شاید بیشتر از نصف زمانی که داخل Compose کدنویسی می‌کنی، در حال نوشتن Modifierها هستی. در واقع بسیاری از Composableهای Compose عمداً ساده طراحی شدن و بیشتر ظاهر و رفتار خودشون رو از Modifier می‌گیرن. به همین دلیل، اگر کسی Modifier رو خوب یاد بگیره، بخش بزرگی از طراحی رابط کاربری Compose رو یاد گرفته.

نکته جالب اینه که این پنج مفهوم تقریباً در تمام صفحه‌های اندروید تکرار میشن. مهم نیست داری صفحه ورود می‌سازی، صفحه تنظیمات، فروشگاه، اینستاگرام یا قیمت‌بان. تقریباً همیشه چند Column و Row برای چیدمان، یک یا چند LazyColumn برای نمایش لیست‌ها، تعدادی Card برای گروه‌بندی اطلاعات و تعداد زیادی Modifier برای شکل دادن به ظاهر اجزا خواهی دید. به همین خاطر یاد گرفتن این پنج مفهوم، در واقع یاد گرفتن ستون فقرات طراحی رابط کاربری در Jetpack Compose هست.


درس هفتم

از یک صفحه به صفحه بعد

Navigation
اندروید چطور بین صفحه‌ها جابه‌جا میشه؟

تقریباً هیچ برنامه‌ای فقط یک صفحه نداره. حتی ساده‌ترین اپلیکیشن‌ها هم حداقل چند صفحه دارن. مثلاً صفحه ورود، صفحه اصلی، صفحه تنظیمات، صفحه پروفایل یا صفحه جزئیات. بنابراین بعد از اینکه یاد گرفتی چطور یک صفحه بسازی، سؤال بعدی اینه که کاربر چطور از یک صفحه به صفحه دیگه میره؟

اگر بخوای خیلی ساده به موضوع نگاه کنی، شاید بگی خب، روی یک دکمه کلیک می‌کنیم و صفحه بعدی باز میشه. اما در واقع پشت همین اتفاق ساده، یکی از مهم‌ترین بخش‌های معماری اندروید قرار داره که بهش Navigation میگن.

در نسخه‌های قدیمی اندروید معمولاً هر صفحه یک Activity جداگانه بود. فرض کن قیمت‌بان رو می‌ساختی. صفحه اصلی یک Activity بود، صفحه جست‌وجو یک Activity دیگه، صفحه تنظیمات هم یک Activity سوم. هر بار که کاربر وارد صفحه جدید می‌شد، در واقع Activity جدیدی ساخته می‌شد. این روش برای برنامه‌های کوچک قابل قبول بود، اما هرچه تعداد صفحه‌ها بیشتر می‌شد، مدیریت Activityها هم سخت‌تر می‌شد. انتقال داده بین صفحه‌ها، مدیریت دکمه Back و هماهنگ نگه داشتن وضعیت برنامه کم‌کم پیچیده می‌شد.

به همین دلیل، امروزه بیشتر برنامه‌های اندروید از معماری Single Activity استفاده می‌کنن. یعنی معمولاً کل برنامه فقط یک Activity اصلی داره و بقیه صفحه‌هایی که کاربر می‌بینه، داخل همون Activity نمایش داده میشن. در این مدل، چیزی که عوض میشه خود Activity نیست. فقط محتوای داخلش تغییر می‌کنه.

اینجاست که Navigation وارد ماجرا میشه. Navigation مسئول این نیست که صفحه‌ها رو طراحی کنه. وظیفه‌اش اینه که مشخص کنه در هر لحظه کدوم صفحه باید نمایش داده بشه و مسیر حرکت کاربر داخل برنامه چطور باشه.

فرض کن وارد قیمت‌بان شدی. اول صفحه اصلی رو می‌بینی. بعد روی یک کالا کلیک می‌کنی و وارد صفحه جزئیاتش میشی. بعد از اون وارد صفحه ثبت قیمت میشی و در نهایت به صفحه تنظیمات میری. Navigation تمام این مسیر رو در حافظه نگه می‌داره. بنابراین وقتی دکمه Back گوشی رو فشار میدی، اندروید حدس نمی‌زنه باید کجا برگرده. Navigation دقیقاً می‌دونه آخرین صفحه‌ای که باز شده چی بوده و باید به کدوم صفحه قبلی برگردی.

می‌تونی Navigation رو مثل یک پشته از صفحه‌ها تصور کنی. هر بار که وارد صفحه جدید میشی، اون صفحه روی بالای پشته قرار می‌گیره. وقتی Back رو فشار میدی، آخرین صفحه از روی پشته برداشته میشه و صفحه قبلی دوباره دیده میشه. به همین خاطر دکمه Back در بیشتر برنامه‌های اندروید رفتار طبیعی و قابل پیش‌بینی داره.

یکی دیگه از وظایف Navigation انتقال اطلاعات بین صفحه‌هاست. فرض کن در صفحه اصلی روی کالایی با شناسه ۲۳۵ کلیک کردی. صفحه جزئیات باید بدونه دقیقاً کدوم کالا انتخاب شده تا اطلاعات همون رو نمایش بده. Navigation این امکان رو فراهم می‌کنه که هنگام جابه‌جایی، اطلاعات مورد نیاز هم همراه صفحه جدید ارسال بشه. بنابراین صفحه مقصد لازم نیست حدس بزنه کاربر از کجا اومده یا چه چیزی رو انتخاب کرده.

نکته مهم اینه که Navigation فقط مخصوص دکمه‌ها نیست. ممکنه بعد از ورود موفق کاربر، برنامه به‌طور خودکار وارد صفحه اصلی بشه. یا اگر توکن ورود منقضی شده باشه، مستقیماً صفحه لاگین نمایش داده بشه. حتی باز کردن یک اعلان (Notification) یا کلیک روی یک لینک اینترنتی هم می‌تونه باعث بشه برنامه مستقیماً وارد یک صفحه خاص بشه. مدیریت تمام این مسیرها هم بر عهده Navigation هست.

در Jetpack Compose، گوگل کتابخانه‌ای به نام Navigation Compose ارائه کرده که دقیقاً برای همین کار ساخته شده. در این کتابخانه، هر صفحه به عنوان یک مقصد (Destination) تعریف میشه و Navigation بین این مقصدها حرکت می‌کنه. تو فقط مشخص می‌کنی چه صفحه‌هایی وجود دارن و از هر صفحه میشه به کجا رفت. خود Navigation مسئول جابه‌جایی، مدیریت دکمه Back و نگهداری مسیر حرکت کاربر میشه.

برای پروژه‌ای مثل قیمت‌بان هم تقریباً از همان روز اول با Navigation سروکار خواهی داشت. صفحه ورود، صفحه اصلی، صفحه جست‌وجو، صفحه جزئیات کالا، صفحه ثبت قیمت و صفحه تنظیمات همگی مقصدهای مختلف برنامه هستن. هر بار که کاربر بین این صفحه‌ها حرکت می‌کنه، در واقع Navigation داره تصمیم می‌گیره چه صفحه‌ای نمایش داده بشه، چه اطلاعاتی همراهش منتقل بشه و اگر کاربر دکمه Back رو زد، دقیقاً به کجا برگرده.

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


درس هشتم

معماری‌ای که پروژه رو قابل نگهداری نگه می‌داره

چرا نباید همه کدها داخل Activity باشه؟

وقتی تازه برنامه‌نویسی اندروید رو شروع می‌کنی، تقریباً طبیعی به نظر میاد که همه چیز رو داخل Activity بنویسی. بالاخره Activity هم صفحه رو نمایش میده، هم به کلیک‌های کاربر دسترسی داره، هم می‌تونه از اینترنت اطلاعات بگیره، هم دیتابیس رو بخونه و هم نتیجه رو روی صفحه نشون بده. بنابراین در نگاه اول شاید با خودت بگی: خب، چرا همه کدها رو همینجا ننویسم؟ واقعیت اینه که تقریباً همه برنامه‌نویس‌ها در ابتدای مسیر همین اشتباه رو انجام میدن. پروژه هم در ابتدا کاملاً سالم به نظر میاد. اما مشکل زمانی شروع میشه که برنامه بزرگ‌تر میشه.

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

اینجاست که Activity کم‌کم تبدیل به چیزی میشه که برنامه‌نویس‌ها به شوخی بهش میگن God Object یعنی کلاسی که همه کار انجام میده و تقریباً همه قسمت‌های برنامه به اون وابسته شدن. چنین کلاسی به مرور تبدیل به کابوس میشه. هر تغییری ممکنه بخش دیگه‌ای از برنامه رو خراب کنه، پیدا کردن باگ‌ها سخت میشه و حتی خودت هم بعد از چند ماه دیگه یادت نمیاد هر قسمت از کد دقیقاً چه کاری انجام میده.

اما مشکل فقط شلوغ شدن فایل نیست. مشکل اصلی اینه که Activity اصلاً برای انجام همه این مسئولیت‌ها ساخته نشده. وظیفه اصلی Activity اینه که رابط کاربری رو به سیستم‌عامل متصل کنه. یعنی صفحه رو نمایش بده، رویدادهای کاربر رو دریافت کنه و با چرخه حیات اندروید هماهنگ باشه. Activity نباید مسئول منطق اصلی برنامه، ارتباط با سرور یا مدیریت دیتابیس باشه، چون این کارها هیچ ارتباط مستقیمی با نمایش صفحه ندارن.

فرض کن فردا تصمیم بگیری علاوه بر نسخه اندروید، یک نسخه وب هم برای قیمت‌بان بسازی. منطق جست‌وجو، محاسبه قیمت‌ها یا مرتب‌سازی کالاها در هر دو نسخه یکسانه. اگر همه این کدها داخل Activity نوشته شده باشن، تقریباً هیچ بخشی قابل استفاده مجدد نیست، چون همه چیز به اندروید گره خورده. اما اگر این منطق از Activity جدا شده باشه، میشه همون قوانین رو در قسمت‌های مختلف برنامه استفاده کرد.

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

برای حل این مشکلات، معماری MVVM معرفی شد.
MVVM مخفف Model–View–ViewModel هست.
ایده اصلی این معماری خیلی ساده‌تر از چیزیه که اسمش نشون میده. هر بخش فقط باید یک مسئولیت مشخص داشته باشه.

در این معماری، View همون چیزی هست که کاربر می‌بینه. در پروژه‌های جدید اندروید، صفحه‌های Jetpack Compose نقش View رو دارن. View فقط اطلاعات رو نمایش میده و رویدادهای کاربر، مثل کلیک روی دکمه یا نوشتن داخل کادر جست‌وجو، رو دریافت می‌کنه. خودش تصمیم نمی‌گیره اطلاعات از کجا بیان یا چطور پردازش بشن.

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

در پایین‌ترین بخش، Model قرار داره. Model مسئول داده‌ها و منطق اصلی برنامه هست. ارتباط با سرور، دیتابیس، فایل‌ها یا هر منبع اطلاعاتی معمولاً در این قسمت انجام میشه. بنابراین ViewModel لازم نیست بدونه اطلاعات دقیقاً از کجا اومدن. فقط از Model درخواست می‌کنه و نتیجه رو دریافت می‌کنه.

اگر دوباره قیمت‌بان رو مثال بزنیم، وقتی کاربر نام یک کالا رو جست‌وجو می‌کنه، Compose فقط متن جست‌وجو رو از کاربر می‌گیره و به ViewModel میده. ViewModel تصمیم می‌گیره چه کاری انجام بشه و از Model می‌خواد کالاها رو جست‌وجو کنه. Model نتیجه رو برمی‌گردونه، ViewModel اون رو آماده نمایش می‌کنه و در نهایت Compose فقط نتیجه نهایی رو روی صفحه نشون میده. هر بخش دقیقاً یک وظیفه داره و وارد مسئولیت بخش‌های دیگه نمیشه.

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

در ادامه این جزوه هم تقریباً تمام پروژه‌هایی که می‌سازیم بر اساس همین معماری پیش میرن، چون امروزه MVVM یکی از رایج‌ترین و استانداردترین معماری‌ها برای توسعه اپلیکیشن‌های اندروید با Jetpack Compose هست.


درس نهم

چرا اطلاعات صفحه با چرخوندن گوشی از بین نمیرن؟

ViewModel دقیقاً چه مشکلی رو حل می‌کنه؟

بعد از اینکه با معماری MVVM آشنا شدی، احتمالاً اولین سؤالی که پیش میاد اینه که ViewModel دقیقاً چه کاری انجام میده؟ خیلی‌ها فکر می‌کنن ViewModel فقط یک کلاس برای مرتب‌تر کردن کدهاست، اما اگر فقط همین بود، گوگل هیچ‌وقت ViewModel رو به یکی از مهم‌ترین بخش‌های معماری اندروید تبدیل نمی‌کرد. ViewModel در واقع برای حل یکی از بزرگ‌ترین مشکلات Activity به وجود اومده.

یادت هست گفتیم Activity عمر دائمی نداره؟ ممکنه کاربر گوشی رو بچرخونه، اندازه صفحه تغییر کنه، زبان برنامه عوض بشه یا حتی اندروید به خاطر کمبود حافظه Activity رو از بین ببره و دوباره بسازه. از دید کاربر شاید فقط صفحه یک لحظه دوباره رسم بشه، اما از دید سیستم‌عامل، Activity قبلی کاملاً نابود شده و یک Activity جدید ساخته شده.

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

گوگل برای حل این مشکل ViewModel رو معرفی کرد. میشه ViewModel رو مثل یک واسطه بین رابط کاربری و منطق برنامه در نظر گرفت. اما مهم‌تر از اون، ViewModel عمرش از Activity بیشتره. یعنی اگر Activity دوباره ساخته بشه، معمولاً ViewModel همون قبلی باقی می‌مونه. در نتیجه اطلاعاتی که داخل ViewModel قرار داشتن از بین نمیرن و Activity جدید دوباره به همون ViewModel متصل میشه.

فرض کن در قیمت‌بان کاربر عبارت آیفون ۱۷ رو جست‌وجو کرده و نتایج روی صفحه نمایش داده شده. حالا گوشی رو می‌چرخونه. اگر این اطلاعات داخل Activity بودن، احتمالاً صفحه دوباره خالی میشد و همه چیز از اول بارگذاری میشد. اما اگر این اطلاعات داخل ViewModel باشن، Activity جدید فقط به ViewModel وصل میشه و همون اطلاعات قبلی دوباره روی صفحه نمایش داده میشن. از دید کاربر، انگار هیچ اتفاقی نیفتاده.

این موضوع فقط مربوط به چرخوندن گوشی نیست. فرض کن کاربر از صفحه اصلی وارد صفحه جزئیات کالا شده. دریافت اطلاعات هنوز کامل نشده که یک تماس تلفنی میاد و برنامه موقتاً به پس‌زمینه میره. چند ثانیه بعد دوباره برمی‌گرده. اگر وضعیت صفحه داخل Activity نگهداری شده باشه، ممکنه همه چیز دوباره از اول شروع بشه. اما اگر ViewModel مسئول نگهداری وضعیت باشه، صفحه دقیقاً از همون جایی ادامه پیدا می‌کنه که قبل از خروج کاربر قرار داشت.

البته اینجا یک نکته مهم وجود داره. خیلی‌ها فکر می‌کنن ViewModel جای مناسبی برای هر نوع اطلاعاتی هست، در حالی که این‌طور نیست. ViewModel قرار نیست دیتابیس باشه یا جای ذخیره دائمی اطلاعات. اگر کاربر برنامه رو کاملاً ببنده یا اندروید کل پردازه (Process) برنامه رو از حافظه خارج کنه، ViewModel هم از بین میره. بنابراین اطلاعات مهم و دائمی، مثل حساب کاربری، تنظیمات یا داده‌های اصلی، باید داخل دیتابیس یا منابع ذخیره‌سازی نگهداری بشن. ViewModel بیشتر برای نگهداری وضعیت فعلی صفحه ساخته شده.

یک وظیفه مهم دیگه ViewModel اینه که صفحه رو از منطق برنامه جدا نگه می‌داره. فرض کن کاربر روی دکمه به‌روزرسانی قیمت‌ها کلیک می‌کنه. صفحه فقط این کلیک رو به ViewModel اطلاع میده. بعد ViewModel تصمیم می‌گیره باید از اینترنت اطلاعات بگیره، خطاها رو مدیریت کنه، وضعیت Loading رو تغییر بده و در نهایت نتیجه رو به صفحه برگردونه. بنابراین صفحه فقط نمایش‌دهنده اطلاعات هست و تصمیم‌گیری‌های اصلی داخل ViewModel انجام میشن.

اگر با React کار کرده باشی، می‌تونی ViewModel رو تا حدی شبیه ترکیبی از State و منطق یک Component در نظر بگیری، با این تفاوت که عمر ViewModel به عمر صفحه وابسته نیست. حتی اگر رابط کاربری دوباره ساخته بشه، ViewModel معمولاً همون نمونه قبلی باقی می‌مونه و دوباره به صفحه جدید متصل میشه. همین ویژگی باعث میشه وضعیت صفحه در بسیاری از تغییرات سیستم‌عامل حفظ بشه.

به همین خاطر، ViewModel فقط یک کلاس برای مرتب کردن پروژه نیست. ViewModel حافظه موقت صفحه، مدیر وضعیت صفحه و محل اجرای منطق مربوط به رابط کاربری هست. هر چیزی که فقط به همان صفحه مربوط میشه، مثل وضعیت بارگذاری، متن جست‌وجو، فیلترهای انتخاب‌شده یا لیست نتایج، معمولاً جای مناسبی داخل ViewModel داره. همین جداسازی باعث میشه صفحه‌ها ساده‌تر بشن، Activity یا Composable فقط مسئول نمایش اطلاعات باشن و منطق برنامه در جایی قرار بگیره که هم قابل نگهداری‌تر باشه و هم با تغییرات چرخه حیات اندروید از بین نره.


درس دهم

چرا با تغییر یک عدد، صفحه خودش عوض میشه؟

State Management
راز زنده بودن رابط کاربری در Jetpack Compose

اگر فقط یک مفهوم در Jetpack Compose وجود داشته باشه که بشه گفت مهم‌ترین مفهوم کل این فریم‌ورک هست، بدون شک همون State هست. در واقع اگر State رو خوب بفهمی، بخش بزرگی از Compose رو فهمیدی. اگر هم State رو درست درک نکنی، تقریباً تمام رفتارهای Compose برات جادویی و عجیب به نظر می‌رسن.

برای فهمیدن State اول باید یک سؤال ساده بپرسیم. فرض کن داخل قیمت‌بان یک صفحه داری که قیمت دلار رو نمایش میده. روی صفحه نوشته شده:

قیمت دلار: ۹۵,۰۰۰ تومان
حالا چند ثانیه بعد قیمت میشه:
۹۵,۲۰۰ تومان
به نظر تو چه اتفاقی باید بیفته؟

در برنامه‌های قدیمی، خود برنامه‌نویس باید این تغییر رو مدیریت می‌کرد. یعنی باید TextView رو پیدا می‌کرد، متنش رو عوض می‌کرد و مطمئن می‌شد که همه قسمت‌های وابسته هم به‌روزرسانی شدن. اما Compose اصلاً این‌طوری فکر نمی‌کنه.

Compose میگه رابط کاربری فقط یک تصویر از وضعیت فعلی برنامه هست. بنابراین اگر وضعیت برنامه عوض بشه، رابط کاربری هم باید خودش رو با اون وضعیت جدید هماهنگ کنه. یعنی به جای اینکه مستقیماً ظاهر صفحه رو تغییر بدی، فقط وضعیت یا همان State رو تغییر میدی. بعد Compose خودش تصمیم می‌گیره چه بخش‌هایی از صفحه باید دوباره رسم بشن.

به همین خاطر میگن Jetpack Compose یک Declarative UI Framework هست. یعنی تو توضیح میدی صفحه در وضعیت فعلی چه شکلی باید باشه، نه اینکه مرحله‌به‌مرحله به برنامه دستور بدی هر قسمت رو چطور تغییر بده.

حالا سؤال بعدی اینه که این State کجا نگهداری میشه؟ اگر State فقط داخل یک تابع معمولی ذخیره بشه، هر بار که Compose صفحه رو دوباره رسم می‌کنه، اون مقدار هم از بین میره. این دقیقاً همون مشکلیه که باعث شده اولین ابزار مدیریت State در Compose به وجود بیاد. یعنی remember.

همون‌طور که از اسمش معلومه، remember یعنی یادت بمونه. وقتی یک مقدار رو داخل remember قرار میدی، Compose اون مقدار رو بین رسم‌های مختلف صفحه نگه می‌داره. فرض کن کاربر داخل کادر جست‌وجوی قیمت‌بان نوشته برنج. اگر صفحه دوباره رسم بشه، انتظار داریم متن داخل کادر همچنان برنج باقی بمونه، نه اینکه خالی بشه. remember دقیقاً برای همین ساخته شده. یعنی نگهداری وضعیت‌های ساده‌ای که فقط به همان صفحه مربوط هستن.

اما remember فقط نگهداری اطلاعات نیست. Compose باید بفهمه چه زمانی یک مقدار تغییر کرده تا دوباره رابط کاربری رو به‌روزرسانی کنه. اگر فقط یک متغیر معمولی Kotlin داشته باشی، Compose هیچ راهی برای فهمیدن تغییر اون نداره.

اینجاست که mutableState وارد ماجرا میشه. mutableState یک متغیر معمولی نیست. یک متغیر قابل مشاهده هست. یعنی هر وقت مقدارش تغییر کنه، Compose فوراً متوجه میشه که بخشی از رابط کاربری به این مقدار وابسته بوده و همون قسمت رو دوباره رسم می‌کنه.

Mutable: یعنی تغییر پذیر

این دقیقاً قلب Jetpack Compose هست. صفحه دائماً در حال گوش دادن به Stateهاست. هر وقت یکی از اون‌ها تغییر کنه، لازم نیست خودت چیزی رو Refresh کنی یا به TextView دستور تغییر بدی. Compose خودش این کار رو انجام میده.

اما تا اینجا فقط درباره اطلاعاتی صحبت کردیم که داخل خود صفحه زندگی می‌کنن. فرض کن در قیمت‌بان اطلاعات قیمت‌ها از اینترنت دریافت میشن. این اطلاعات داخل ViewModel قرار دارن، نه داخل خود صفحه. حالا سؤال اینه که صفحه از کجا بفهمه ViewModel اطلاعات جدید دریافت کرده؟ اینجاست که StateFlow به وجود میاد.

StateFlow رو میشه مثل یک کانال زنده در نظر گرفت. ViewModel آخرین وضعیت خودش رو داخل StateFlow نگه می‌داره و هر وقت این وضعیت تغییر کنه، تمام بخش‌هایی که به اون گوش میدن فوراً باخبر میشن.

فرض کن قیمت طلا از سرور دریافت میشه. ViewModel اطلاعات جدید رو داخل StateFlow قرار میده. صفحه هم در حال گوش دادن به همون StateFlow هست. بنابراین بدون اینکه صفحه مرتب از ViewModel سؤال بپرسه چیزی تغییر کرده؟ به محض تغییر داده، Compose صفحه رو با اطلاعات جدید دوباره رسم می‌کنه.

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

دقیقاً تفاوت اصلی همین آخریه. خیلی ساده اینجوری بهش نگاه کن:

  • remember یعنی این متغیر رو تا وقتی همین صفحه بازه، یادم بمونه.
  • mutableState یعنی اگر این متغیر عوض شد، UI هم خودش آپدیت بشه.
  • StateFlow یعنی همین State رو از داخل ViewModel به چند صفحه یا کل برنامه بفرست تا هر جا لازم بود، همه همزمان تغییراتش رو ببینن.

فرض کن تعداد سکه‌های بازی رو نگه می‌داری.

  • اگر فقط داخل همون صفحه نمایش داده میشه: remember + mutableState کافیه.
  • اما اگر تعداد سکه‌ها از سرور میاد، داخل ViewModel ذخیره میشه و چند تا صفحه باید همزمان اون رو ببینن StateFlow استفاده می‌کنی.

اگر بخوای این سه مفهوم رو کنار هم ببینی، میشه گفت هر کدوم برای یک سطح از مدیریت وضعیت ساخته شدن. remember برای نگهداری وضعیت‌های ساده داخل خود صفحه استفاده میشه. مثل باز بودن یک منو یا متن داخل یک کادر جست‌وجو.
mutableState به Compose اعلام می‌کنه که این مقدار قابل تغییره و هر وقت تغییر کرد، رابط کاربری باید خودش رو به‌روزرسانی کنه.
StateFlow هم همین ایده رو یک مرحله بزرگ‌تر می‌کنه و وضعیتی رو که داخل ViewModel قرار داره، به صورت زنده در اختیار رابط کاربری میذاره.

به همین خاطر، وقتی در Jetpack Compose می‌بینی فقط با تغییر یک مقدار، بخشی از صفحه بدون هیچ دستور اضافه‌ای تغییر می‌کنه، پشت این اتفاق جادو وجود نداره. تمام این رفتار نتیجه همکاری همین سه مفهوم هست. Compose دائماً State رو زیر نظر داره و هر وقت State تغییر کنه، رابط کاربری هم خودش رو با وضعیت جدید هماهنگ می‌کنه. همین طرز فکر، مهم‌ترین تفاوت Jetpack Compose با روش‌های قدیمی ساخت رابط کاربری در اندروید هست.


بخش دوم — ارتباط با Node.js

درس یازدهم

برنامه چطور با سرور حرف می‌زنه؟

Retrofit
چرا خودمون درخواست‌های اینترنتی رو ارسال نمی‌کنیم؟

تا اینجای مسیر، تقریباً همه چیز داخل خود گوشی اتفاق می‌افتاد. صفحه ساختیم، بین صفحه‌ها جابه‌جا شدیم، ViewModel رو شناختیم و وضعیت رابط کاربری رو مدیریت کردیم. اما یک اپلیکیشن واقعی فقط با اطلاعات داخل گوشی کار نمی‌کنه. تقریباً تمام برنامه‌هایی که هر روز ازشون استفاده می‌کنیم، مدام در حال صحبت کردن با یک سرور هستن. اینستاگرام پست‌ها رو از سرور می‌گیره، واتساپ پیام‌ها رو از سرور دریافت می‌کنه، دیجی‌کالا اطلاعات کالاها رو از سرور می‌خونه و قیمت‌بان هم قراره قیمت کالاها رو از API خودش دریافت کنه. اینجا یک سؤال مهم پیش میاد. اصلاً برنامه چطور با سرور ارتباط برقرار می‌کنه؟

اگر بخوای از صفر این ارتباط رو خودت پیاده کنی، باید اتصال اینترنت رو برقرار کنی، درخواست HTTP بسازی، آدرس API رو بنویسی، هدرها رو اضافه کنی، اطلاعات رو ارسال کنی، منتظر پاسخ بمونی، کدهای خطا رو بررسی کنی، متن پاسخ رو بخونی و در نهایت اون متن رو به آبجکت‌های Kotlin تبدیل کنی. تازه بعد از همه این‌ها باید خطاهای اینترنت، قطع شدن ارتباط، Timeout و ده‌ها حالت مختلف دیگه رو هم مدیریت کنی.

یعنی کاری که در ظاهر فقط یک خط به نظر میاد. مثلاً لیست کالاها رو از سرور بگیر، در واقع پشت صحنه ده‌ها مرحله مختلف داره.

به همین دلیل، گوگل و جامعه توسعه‌دهندگان اندروید از سال‌ها پیش به این نتیجه رسیدن که این کار نباید هر بار از صفر نوشته بشه. تقریباً همه برنامه‌ها یک کار مشابه انجام میدن. درخواست می‌فرستن و پاسخ دریافت می‌کنن. پس منطقی‌تره که یک کتابخانه این کار رو انجام بده و برنامه‌نویس فقط روی منطق برنامه تمرکز کنه. یکی از معروف‌ترین این کتابخانه‌ها Retrofit هست.

Retrofit کتابخانه‌ای هست که توسط شرکت Square ساخته شده و امروز تقریباً به استاندارد ارسال درخواست‌های HTTP در اندروید تبدیل شده. وظیفه Retrofit این نیست که اینترنت رو اختراع کنه یا پروتکل HTTP رو تغییر بده. اینترنت همچنان با همان قوانین HTTP کار می‌کنه. کاری که Retrofit انجام میده اینه که تمام پیچیدگی‌های ارتباط با سرور رو پشت یک رابط ساده پنهان می‌کنه.

فرض کن در قیمت‌بان می‌خوای لیست قیمت‌ها رو دریافت کنی. از دید تو فقط یک درخواست وجود داره: قیمت‌های امروز رو برام بفرست. اما Retrofit پشت صحنه درخواست HTTP رو می‌سازه، اون رو به آدرس API ارسال می‌کنه، منتظر پاسخ می‌مونه، پاسخ سرور رو دریافت می‌کنه و نتیجه رو در قالبی قابل استفاده به برنامه تحویل میده.

البته Retrofit به تنهایی همه کار رو انجام نمیده. معمولاً پاسخ سرورها به صورت JSON ارسال میشه. JSON فقط یک متن ساختاریافته هست و برنامه نمی‌تونه مستقیماً با اون کار کنه. بنابراین Retrofit معمولاً از کتابخانه‌هایی مثل Gson یا Kotlin Serialization کمک می‌گیره تا متن JSON رو به آبجکت‌های Kotlin تبدیل کنه. در نتیجه، وقتی ViewModel اطلاعات رو دریافت می‌کنه، دیگر با یک رشته طولانی از متن روبه‌رو نیست. بلکه مستقیماً با آبجکت‌هایی مثل Product یا Price کار می‌کنه.

اما نکته مهم‌تر اینه که Retrofit نباید مستقیماً داخل صفحه یا حتی ViewModel استفاده بشه. اگر یادت باشه در درس MVVM گفتیم هر بخش فقط باید یک مسئولیت داشته باشه. ViewModel نباید بدونه درخواست HTTP چطور ارسال میشه یا آدرس API کجاست. وظیفه ViewModel فقط اینه که بگه قیمت‌های امروز رو می‌خوام. این درخواست به Repository منتقل میشه و Repository با استفاده از Retrofit با سرور ارتباط برقرار می‌کنه. بنابراین اگر فردا تصمیم بگیری Retrofit رو با کتابخانه دیگه‌ای جایگزین کنی، لازم نیست ViewModel یا صفحه‌های برنامه رو تغییر بدی. فقط Repository تغییر می‌کنه.

یک نکته دیگه هم وجود داره که باعث شده Retrofit این‌قدر محبوب بشه. این کتابخانه فقط ارسال درخواست رو ساده نکرده، بلکه ساختار کد رو هم خواناتر کرده. به جای اینکه در هر قسمت از برنامه ده‌ها خط کد برای ساخت درخواست HTTP بنویسی، فقط مشخص می‌کنی چه APIهایی وجود دارن و هر کدوم چه اطلاعاتی دریافت یا ارسال می‌کنن. بعد Retrofit بقیه کارها رو خودش انجام میده. همین باعث میشه کدها کوتاه‌تر، خواناتر و نگهداریشون راحت‌تر بشه.

در پروژه قیمت‌بان هم تقریباً تمام ارتباط با سرور از همین مسیر انجام میشه. وقتی کاربر لیست قیمت‌ها رو باز می‌کنه، قیمت جدید ثبت می‌کنه، جست‌وجو انجام میده یا اطلاعات حساب کاربریش رو دریافت می‌کنه، در نهایت یک درخواست به API ارسال میشه. Retrofit مسئول این ارتباط هست، Repository از Retrofit استفاده می‌کنه، ViewModel فقط نتیجه رو دریافت می‌کنه و Jetpack Compose هم نتیجه نهایی رو روی صفحه نمایش میده.

به همین خاطر، نباید Retrofit رو صرفاً یک کتابخانه برای ارسال درخواست اینترنتی بدونی. Retrofit لایه‌ای هست که ارتباط بین دنیای اندروید و سرور رو ساده، قابل اعتماد و قابل نگهداری می‌کنه. اگر ViewModel مغز صفحه باشه و Repository مدیر داده‌ها، Retrofit مترجمی هست که زبان برنامه اندروید رو به زبان قابل فهم برای سرور تبدیل می‌کنه و پاسخ سرور رو دوباره به شکلی برمی‌گردونه که برنامه به راحتی بتونه از اون استفاده کنه.


درس دوازدهم

چرا سرور و اندروید زبان همدیگه رو می‌فهمن؟

JSON و Serialization
برنامه چطور اطلاعات رو ترجمه می‌کنه؟

در درس قبل دیدیم که Retrofit درخواست رو به سرور ارسال می‌کنه و پاسخ رو دریافت می‌کنه. اما یک سؤال مهم هنوز بی‌جواب مونده. سرور اصلاً اطلاعات رو با چه زبانی برمی‌گردونه؟

فرض کن قیمت‌بان از سرور می‌خواد قیمت امروز دلار رو دریافت کنه. آیا سرور یک آبجکت Kotlin برمی‌گردونه؟ نه. آیا سرور می‌دونه کلاس‌های Kotlin یا اندروید چی هستن؟ باز هم نه. در واقع سرور و اپلیکیشن اندروید اصلاً زبان مشترکی ندارن.

سرور ممکنه با Node.js، PHP، Python، Java یا هر زبان دیگه‌ای نوشته شده باشه. اپلیکیشن هم با Kotlin ساخته شده. بنابراین باید یک زبان مشترک بین این دو وجود داشته باشه. زبانی که هیچ وابستگی به هیچ زبان برنامه‌نویسی خاصی نداشته باشه. همین زبان مشترک JSON هست.

JSON فقط یک قالب استاندارد برای انتقال اطلاعاته. نه مخصوص اندرویده، نه مخصوص Node.js و نه مخصوص هیچ زبان دیگه‌ای. تقریباً تمام زبان‌های برنامه‌نویسی دنیا JSON رو می‌شناسن. به همین دلیل، وقتی سرور اطلاعاتی رو ارسال می‌کنه، اون رو به JSON تبدیل می‌کنه و وقتی اندروید اون اطلاعات رو دریافت می‌کنه، دوباره JSON رو به آبجکت‌های Kotlin تبدیل می‌کنه.

پس JSON خودش اطلاعات نیست. فقط ظرفی هست که اطلاعات داخل اون قرار می‌گیرن. فرض کن داخل قیمت‌بان یک کالا داریم که اسم، قیمت و تاریخ آخرین به‌روزرسانی داره. داخل برنامه این اطلاعات شاید به شکل یک کلاس Kotlin وجود داشته باشن. اما وقتی همین اطلاعات قراره از اینترنت عبور کنن، دیگه نمی‌تونن به شکل یک کلاس Kotlin ارسال بشن، چون سرور اصلاً چیزی به اسم کلاس Kotlin رو نمی‌شناسه. بنابراین قبل از ارسال، این اطلاعات به JSON تبدیل میشن. بعد از اینکه به گوشی رسیدن، دوباره به همان کلاس Kotlin برمی‌گردن. اینجاست که به مفهوم Serialization می‌رسیم.

Serialization یعنی تبدیل یک آبجکت برنامه به یک فرم قابل انتقال یا قابل ذخیره. یعنی چیزی که داخل حافظه برنامه وجود داره، به یک قالب استاندارد مثل JSON تبدیل میشه تا بشه اون رو از طریق اینترنت ارسال کرد، داخل فایل ذخیره کرد یا به برنامه دیگه‌ای فرستاد. در مقابل، وقتی JSON دوباره به آبجکت Kotlin تبدیل میشه، به این فرایند Deserialization میگن.

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

اگر بخوای یک مثال ساده‌تر تصور کنی، فرض کن دو نفر فارسی و انگلیسی صحبت می‌کنن. هیچ‌کدوم زبان همدیگه رو بلد نیستن. بنابراین یک مترجم وسط اون‌ها قرار می‌گیره. نفر اول حرفش رو به مترجم میگه، مترجم اون رو به زبان مشترک تبدیل می‌کنه و نفر دوم معنی رو دریافت می‌کنه. موقع برگشت هم دقیقاً همین اتفاق برعکس انجام میشه. در این مثال، آبجکت Kotlin مثل جمله فارسی هست، JSON مثل زبان مشترک و Serialization هم نقش مترجم رو بازی می‌کنه.

نکته مهم اینه که معمولاً خودت این تبدیل‌ها رو انجام نمیدی. اگر یادت باشه در درس قبل گفتیم Retrofit معمولاً کنار کتابخانه‌هایی مثل Kotlin Serialization یا Gson استفاده میشه. این کتابخانه‌ها مسئول انجام Serialization و Deserialization هستن. یعنی وقتی Retrofit پاسخ سرور رو دریافت می‌کنه، این کتابخانه‌ها JSON رو می‌خونن و به آبجکت‌های Kotlin تبدیل می‌کنن. بنابراین ViewModel مستقیماً با آبجکت‌های Kotlin کار می‌کنه و اصلاً درگیر متن JSON نمیشه.

در پروژه قیمت‌بان هم تقریباً تمام اطلاعات همین مسیر رو طی می‌کنن. وقتی کاربر قیمت جدیدی ثبت می‌کنه، ابتدا اطلاعات داخل کلاس‌های Kotlin قرار دارن. قبل از ارسال به سرور، این کلاس‌ها به JSON تبدیل میشن و از طریق Retrofit ارسال میشن. سرور هم بعد از پردازش، پاسخ رو دوباره به صورت JSON برمی‌گردونه. در نهایت، کتابخانه Serialization این JSON رو دوباره به کلاس‌های Kotlin تبدیل می‌کنه و ViewModel نتیجه رو دریافت می‌کنه.

اگر بخوای کل این مسیر رو یک‌جا ببینی، ترتیب اتفاق‌ها این شکله:

Kotlin Object → Serialization → JSON → اینترنت → سرور → JSON → Deserialization → Kotlin Object

به همین خاطر، JSON و Serialization دو مفهوم جدا از هم نیستن. JSON فقط یک قالب برای جابه‌جایی اطلاعات هست، اما Serialization فرایندی هست که اطلاعات رو بین دنیای آبجکت‌های Kotlin و این قالب استاندارد ترجمه می‌کنه. بدون JSON، برنامه و سرور زبان مشترکی نداشتن و بدون Serialization هم هر بار مجبور بودی خودت این ترجمه رو به صورت دستی انجام بدی. همین دو مفهوم باعث شدن ارتباط بین اپلیکیشن‌های امروزی و سرورها به کاری کاملاً استاندارد، ساده و قابل اعتماد تبدیل بشه.


درس سیزدهم

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

Authentication
برنامه از کجا می‌فهمه واقعاً تو هستی؟

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

اینجا یک سؤال مهم پیش میاد. برنامه از کجا فهمیده که هنوز همون کاربر قبلی هستی؟ مگر رمز عبورت رو داخل گوشی ذخیره کرده؟ جواب این سؤال، ما رو با مفهوم Authentication آشنا می‌کنه.

Authentication یعنی فرایند اثبات هویت. یعنی سرور باید مطمئن بشه کسی که درخواست ارسال کرده، واقعاً همون کاربری هست که ادعا می‌کنه. اما نکته جالب اینه که این کار تقریباً هیچ‌وقت با ارسال مداوم رمز عبور انجام نمیشه.

فرض کن برای اولین بار وارد قیمت‌بان میشی. ایمیل و رمز عبورت رو وارد می‌کنی و برنامه اون‌ها رو برای سرور ارسال می‌کنه. سرور اطلاعات رو بررسی می‌کنه و اگر همه چیز درست باشه، به جای اینکه بگه از این به بعد همیشه رمزت رو برام بفرست، یک کارت شناسایی موقت در اختیار برنامه قرار میده. به این کارت شناسایی JWT یا JSON Web Token میگن.

میشه JWT رو مثل کارت ورود یک شرکت در نظر گرفت. روز اول با کارت ملی وارد شرکت میشی، مسئول حراست هویتت رو بررسی می‌کنه و بعد یک کارت پرسنلی بهت میده. از فردا دیگه لازم نیست هر روز کارت ملی نشون بدی. فقط کارت پرسنلیت رو جلوی دستگاه می‌گیری و وارد میشی.

JWT دقیقاً همین نقش رو داره. بعد از اولین ورود، برنامه این توکن رو نگه می‌داره. از اون به بعد هر بار که درخواستی برای سرور ارسال می‌کنه، به جای رمز عبور، JWT رو همراه درخواست می‌فرسته. سرور هم توکن رو بررسی می‌کنه و اگر معتبر باشه، درخواست رو قبول می‌کنه.

این روش دو مزیت بزرگ داره. اول اینکه رمز عبور کاربر مدام روی اینترنت جابه‌جا نمیشه و احتمال سرقتش کمتر میشه. دوم اینکه سرور با سرعت بیشتری می‌تونه هویت کاربر رو بررسی کنه. اما اینجا یک مشکل جدید به وجود میاد.

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

حالا تصور کن داری با قیمت‌بان کار می‌کنی و یک ساعت گذشته. JWT منقضی شده. آیا باید دوباره صفحه ورود نمایش داده بشه و از کاربر رمز عبور بخوایم؟ اگر این اتفاق بیفته، تجربه کاربری واقعاً بد میشه. هیچ‌کس دوست نداره هر چند ساعت یک بار دوباره رمزش رو وارد کنه. برای حل همین مشکل، مفهوم Refresh Token به وجود اومده.

موقع اولین ورود، سرور معمولاً دو توکن به برنامه میده. یکی JWT که عمر کوتاهی داره و برای درخواست‌های روزمره استفاده میشه. یکی هم Refresh Token که عمرش خیلی بیشتره. شاید چند روز، چند هفته یا حتی چند ماه.

وقتی JWT منقضی میشه، برنامه قبل از اینکه کاربر متوجه چیزی بشه، Refresh Token رو برای سرور ارسال می‌کنه. سرور بررسی می‌کنه که این Refresh Token هنوز معتبر هست یا نه. اگر معتبر باشه، بدون اینکه کاربر دوباره رمزش رو وارد کنه، یک JWT جدید صادر می‌کنه و برنامه به کار خودش ادامه میده. از دید کاربر هیچ اتفاقی نیفتاده. فقط پشت صحنه کارت شناسایی موقتش با یک کارت جدید جایگزین شده. حالا می‌رسیم به چیزی که هر روز ازش استفاده می‌کنیم. Auto Login.

وقتی برنامه رو می‌بندی و فردا دوباره بازش می‌کنی، معمولاً مستقیم وارد حساب کاربریت میشی. دلیلش این نیست که برنامه رمز عبورت رو نگه داشته. معمولاً برنامه Refresh Token رو در حافظه امن گوشی ذخیره کرده. هنگام اجرای برنامه، اول بررسی می‌کنه آیا JWT هنوز معتبره یا نه. اگر معتبر باشه، همون رو استفاده می‌کنه. اگر منقضی شده باشه، با Refresh Token یک JWT جدید می‌گیره و بعد وارد صفحه اصلی میشه. اگر Refresh Token هم منقضی شده باشه یا سرور اون رو باطل کرده باشه، تازه اون موقع صفحه ورود نمایش داده میشه و کاربر باید دوباره ایمیل و رمز عبورش رو وارد کنه.

در پروژه قیمت‌بان هم دقیقاً همین اتفاق می‌افته. اولین بار که کاربر وارد حسابش میشه، سرور یک JWT و یک Refresh Token برمی‌گردونه. از اون به بعد تمام درخواست‌های دریافت قیمت‌ها، ثبت قیمت جدید یا ویرایش اطلاعات حساب با JWT انجام میشن. اگر JWT اعتبارش تموم بشه، برنامه به صورت خودکار با Refresh Token یک JWT جدید دریافت می‌کنه. بنابراین کاربر بدون اینکه متوجه این فرایند بشه، همچنان داخل حسابش باقی می‌مونه.

اگر بخوای کل این فرایند رو در یک جمله خلاصه کنی، میشه گفت Authentication فقط وارد کردن رمز عبور نیست. بلکه یک سیستم مدیریت هویت هست. JWT کارت شناسایی موقت کاربر هست، Refresh Token راهی برای گرفتن یک کارت شناسایی جدید بدون وارد کردن دوباره رمز عبور هست و Auto Login نتیجه همکاری این دو هست. یعنی کاربر احساس می‌کنه همیشه وارد حسابش مونده، در حالی که پشت صحنه برنامه و سرور دائماً در حال مدیریت اعتبار این توکن‌ها هستن.


درس چهاردهم

چطور برنامه همیشه می‌دونه تو وارد حسابت هستی؟

Session
بعد از ورود، برنامه وضعیت کاربر رو چطور مدیریت می‌کنه؟

در درس قبل درباره Authentication، JWT و Refresh Token صحبت کردیم. اما هنوز یک سؤال مهم باقی مونده. فرض کن کاربر وارد قیمت‌بان شده، JWT هم گرفته و همه چیز درست کار می‌کنه. حالا برنامه بسته میشه. چند ساعت بعد دوباره بازش می‌کنی. برنامه از کجا می‌فهمه باید مستقیم صفحه اصلی رو نشون بده یا صفحه ورود رو؟ آیا هر بار از سرور سؤال می‌کنه؟ آیا رمز عبور رو نگه داشته؟ یا اصلاً یک جایی نوشته که این کاربر وارد حسابش شده؟ همه این سؤال‌ها ما رو به مفهوم Session می‌رسونن.

خیلی‌ها فکر می‌کنن Session یعنی فقط کاربر وارد حسابش شده. اما در واقع Session یعنی وضعیت فعلی ارتباط بین کاربر و سیستم. فرض کن وارد یک هتل میشی. مسئول پذیرش بعد از بررسی مدارکت، یک کارت اتاق بهت میده. تا وقتی اون کارت دستت باشه، لازم نیست هر بار که می‌خوای وارد اتاقت بشی دوباره کارت ملی نشون بدی. هتل فقط بررسی می‌کنه که کارتت معتبر هست یا نه.

در دنیای نرم‌افزار هم تقریباً همین اتفاق می‌افته. بعد از اینکه هویت کاربر تأیید شد، برنامه وارد یک Session میشه. یعنی از این لحظه به بعد برنامه می‌دونه این کاربر قبلاً احراز هویت شده و لازم نیست دوباره فرایند ورود از اول انجام بشه. اما یک نکته مهم وجود داره. خود Session یک چیز فیزیکی نیست که داخل اندروید وجود داشته باشه. Session بیشتر یک مفهومه. یعنی وضعیت ورود کاربر.

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

این روش هنوز هم در بعضی سیستم‌ها استفاده میشه، اما یک مشکل بزرگ داشت. هرچه تعداد کاربران بیشتر می‌شد، سرور باید Session میلیون‌ها نفر رو داخل حافظه نگه می‌داشت. این کار هم حافظه زیادی مصرف می‌کرد و هم مدیریت چندین سرور رو سخت می‌کرد. به همین دلیل، بیشتر APIهای مدرن به سمت JWT رفتن.

در این روش، سرور دیگر لازم نیست Session همه کاربران رو داخل حافظه خودش نگه داره. اطلاعات لازم داخل خود JWT قرار داره و هر بار که درخواستی ارسال میشه، سرور فقط اعتبار همون توکن رو بررسی می‌کنه. به همین دلیل به این روش معمولاً Stateless Authentication هم میگن. یعنی سرور وضعیت هر کاربر رو بین درخواست‌ها نگه نمی‌داره. اما از دید برنامه اندروید، هنوز هم چیزی به اسم Session وجود داره. برنامه باید بدونه:

  • کاربر وارد شده یا نه
  • JWT هنوز معتبر هست یا نه
  • Refresh Token وجود داره یا نه
  • آیا باید صفحه Login نمایش داده بشه یا صفحه اصلی

مجموع این اطلاعات، Session برنامه رو تشکیل میدن. در قیمت‌بان، وقتی کاربر برای اولین بار وارد حسابش میشه، برنامه JWT و Refresh Token رو در یک محل امن ذخیره می‌کنه. دفعه بعد که برنامه اجرا میشه، قبل از اینکه حتی صفحه اصلی نمایش داده بشه، Session بررسی میشه. اگر همه چیز معتبر باشه، برنامه مستقیماً وارد صفحه اصلی میشه. اگر JWT منقضی شده باشه، با Refresh Token یک JWT جدید گرفته میشه. اگر هیچ‌کدوم معتبر نباشن، Session پایان پیدا کرده و کاربر دوباره به صفحه ورود هدایت میشه.

به همین دلیل معمولاً یک صفحه کوتاه در ابتدای برنامه می‌بینی که شاید فقط یک یا دو ثانیه نمایش داده بشه. این صفحه صرفاً در حال بررسی Session هست. یعنی تصمیم می‌گیره کاربر باید وارد برنامه بشه یا ابتدا دوباره احراز هویت انجام بده. اگر بخوای تفاوت مفاهیم این چند درس رو کنار هم ببینی، میشه این‌طور گفت:

  • Authentication پاسخ سؤال تو کی هستی؟ رو میده
  • JWT مدرکیه که ثابت می‌کنه قبلاً احراز هویت شدی
  • Refresh Token راهیه برای گرفتن یک JWT جدید بدون وارد کردن دوباره رمز عبور
  • Session هم وضعیت کلی ورود کاربر رو مدیریت می‌کنه و مشخص می‌کنه برنامه باید با کاربر مثل یک کاربر واردشده رفتار کنه یا نه

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


درس پانزدهم

اگر توکن‌ها داخل گوشی ذخیره میشن، چرا هر کسی نمی‌تونه اون‌ها رو بدزده؟

ذخیره Token
چرا امنیت بعد از Login تازه شروع میشه؟

تا اینجا مسیر ورود کاربر رو تقریباً کامل یاد گرفتیم. کاربر ایمیل و رمز عبورش رو وارد می‌کنه، سرور هویتش رو بررسی می‌کنه، یک JWT و یک Refresh Token برمی‌گردونه و برنامه هم با کمک اون‌ها Session کاربر رو مدیریت می‌کنه. اما اینجا تازه یک مشکل جدید به وجود میاد. مشکلی که اگر درست حل نشه، تمام زحمتی که برای Authentication کشیدی تقریباً بی‌فایده میشه.

فرض کن کاربر وارد قیمت‌بان شده و برنامه رو می‌بنده. فردا دوباره برنامه رو باز می‌کنه. اگر توکن‌ها رو جایی ذخیره نکرده باشی، برنامه هیچ راهی برای تشخیص هویت کاربر نداره و باید دوباره صفحه Login رو نمایش بده. این یعنی Auto Login عملاً از بین میره. پس مشخصه که توکن‌ها باید جایی داخل گوشی ذخیره بشن تا بعد از بسته شدن برنامه هم باقی بمونن.

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

برای درک بهتر، فرض کن کارت بانکی‌ات رو داخل کیف پول گذاشتی. رمز کارت رو هیچ‌کس نمی‌دونه، اما اگر خود کارت رو گم کنی، باز هم دردسر بزرگی برات ایجاد میشه. JWT هم تا حدی همین نقش رو داره. این توکن در واقع مدرکیه که به سرور ثابت می‌کنه قبلاً احراز هویت شدی. بنابراین اگر این مدرک دست فرد دیگه‌ای بیفته، ممکنه او هم بتونه ازش سوءاستفاده کنه.

پس سؤال اصلی این نیست که توکن رو ذخیره کنیم یا نه؟ سؤال درست اینه که چطور ذخیره‌اش کنیم که قابل سرقت نباشه؟ اینجاست که مفهوم Encrypted Storage وارد ماجرا میشه.

Encrypted Storage یعنی اطلاعات قبل از اینکه روی حافظه گوشی نوشته بشن، رمزنگاری میشن. فرض کن کسی به فایل ذخیره‌شده دسترسی پیدا کنه. چیزی که می‌بینه دیگر یک JWT قابل خواندن نیست، بلکه مجموعه‌ای از داده‌های به‌هم‌ریخته و غیرقابل فهمه. فقط برنامه‌ای که کلید رمزگشایی رو در اختیار داره، می‌تونه دوباره اون اطلاعات رو به حالت اصلی برگردونه و ازش استفاده کنه. اینجا ممکنه یک سؤال دیگه برات پیش بیاد. اگر اطلاعات رمزنگاری شدن، پس کلید رمزگشایی کجا نگهداری میشه؟ اگر کلید هم داخل همون فایل باشه که کل امنیت از بین میره.

این دقیقاً مشکلیه که اندروید سال‌ها پیش براش راه‌حل ارائه کرده. سیستم‌عامل اندروید یک بخش امنیتی به نام Android Keystore داره. Keystore مثل یک گاوصندوق داخلیه که کلیدهای رمزنگاری رو داخل خودش نگه می‌داره. نکته مهم اینه که برنامه معمولاً نمی‌تونه خود کلید رو بخونه یا از گوشی خارجش کنه. فقط می‌تونه از سیستم‌عامل بخواد که با استفاده از اون کلید، اطلاعاتی رو رمزنگاری یا رمزگشایی کنه. همین موضوع باعث میشه سرقت کلید بسیار سخت‌تر از نگهداری اون داخل یک فایل معمولی باشه.

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

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

در قیمت‌بان هم دقیقاً همین اتفاق می‌افته. بعد از اینکه کاربر برای اولین بار وارد حسابش میشه، JWT و Refresh Token داخل فضای رمزنگاری‌شده ذخیره میشن. دفعه بعد که برنامه اجرا میشه، ابتدا همین اطلاعات از فضای امن خونده میشن. اگر JWT هنوز اعتبار داشته باشه، برنامه مستقیماً وارد صفحه اصلی میشه. اگر منقضی شده باشه، با استفاده از Refresh Token یک JWT جدید از سرور گرفته میشه. اگر Refresh Token هم دیگر معتبر نباشه، Session پایان پیدا می‌کنه، اطلاعات ذخیره‌شده پاک میشن و کاربر دوباره صفحه Login رو می‌بینه.

اگر به مسیر این چند درس نگاه کنی، می‌بینی که همه آن‌ها مثل قطعات یک پازل به هم وصل میشن. Authentication هویت کاربر رو تأیید می‌کنه، JWT مدرک این هویت رو در اختیار برنامه قرار میده، Session وضعیت ورود کاربر رو مدیریت می‌کنه و Encrypted Storage هم از این مدرک ارزشمند محافظت می‌کنه تا بعد از بسته شدن برنامه هم هم امنیت حفظ بشه و هم کاربر مجبور نباشه هر بار دوباره وارد حسابش بشه. به همین خاطر، Encrypted Storage فقط یک محل برای ذخیره اطلاعات نیست. بلکه یکی از مهم‌ترین حلقه‌های زنجیره امنیت در اپلیکیشن‌های اندروید به حساب میاد.


درس شانزدهم

اگر همه چیز طبق برنامه پیش نرفت، برنامه باید چه کار کنه؟

مدیریت خطاها
چرا برنامه‌های حرفه‌ای هیچ‌وقت فرض نمی‌کنن همه چیز همیشه درست کار می‌کنه؟

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

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

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

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

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

نوع دیگری از خطا، Server Error هست. این بار اینترنت کاملاً سالمه، درخواست هم به سرور رسیده، اما خود سرور نتونسته اون رو پردازش کنه. شاید پایگاه داده از دسترس خارج شده، شاید برنامه سمت سرور باگ داشته یا شاید سرور بیش از حد شلوغ شده باشه. در این حالت، مشکل نه از کاربره و نه از گوشی. مشکل در سمت سروره. بنابراین برنامه باید به جای اینکه کاربر رو مقصر جلوه بده، پیام مناسبی مثل در حال حاضر سرور در دسترس نیست. چند دقیقه دیگه دوباره امتحان کنید. نمایش بده.

نکته جالب اینه که از نگاه برنامه، این سه اتفاق کاملاً با هم فرق دارن. در Timeout برنامه نمی‌دونه سرور بالا بوده یا نه. فقط می‌دونه پاسخ دیر رسیده. در قطع اینترنت، درخواست اصلاً از گوشی خارج نشده یا نتونسته به مقصد برسه. اما در Server Error، درخواست با موفقیت به سرور رسیده و خود سرور اعلام کرده که نتونسته عملیات رو انجام بده. اگر همه این حالت‌ها رو با یک پیام کلی مثل خطا رخ داد نمایش بدی، هم پیدا کردن مشکل برای خودت سخت میشه و هم تجربه کاربر ضعیف‌تر میشه.

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

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


بخش سوم — دیتابیس

درس هفدهم

چرا بعضی اطلاعات حتی بدون اینترنت هم داخل برنامه باقی می‌مونن؟

Room Database
چرا همه اطلاعات رو از سرور نمی‌گیریم؟

فرض کن قیمت‌بان رو باز می‌کنی تا قیمت‌های امروز رو ببینی. همه چیز درست کار می‌کنه و اطلاعات از سرور دریافت میشن. چند دقیقه بعد اینترنتت قطع میشه. دوباره برنامه رو باز می‌کنی. حالا به نظرت چه اتفاقی باید بیفته؟

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

اینجا یک سؤال مهم مطرح میشه. اگر این اطلاعات از سرور اومدن، پس چطور بعد از قطع اینترنت هنوز داخل گوشی وجود دارن؟ جواب این سؤال ما رو به مفهوم دیتابیس محلی می‌رسونه.

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

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

سال‌هاست که اندروید از دیتابیسی به نام SQLite استفاده می‌کنه. SQLite یک دیتابیس سبک و قدرتمنده که داخل خود گوشی اجرا میشه و نیازی به نصب سرور جداگانه نداره. اما کار کردن مستقیم با SQLite چندان ساده نیست. باید خودت Queryهای SQL بنویسی، نتیجه‌ها رو بخونی، اون‌ها رو به آبجکت‌های Kotlin تبدیل کنی و مراقب خطاهای مختلف هم باشی. برای پروژه‌های بزرگ، این کار هم زمان‌بره و هم احتمال اشتباه رو زیاد می‌کنه. به همین دلیل، گوگل کتابخانه‌ای به نام Room رو معرفی کرد.

Room در واقع یک لایه بالاتر از SQLite هست. یعنی خود دیتابیس همچنان SQLite هست، اما تو دیگر لازم نیست مستقیماً با اون کار کنی. Room بخش زیادی از کارهای تکراری رو انجام میده و اجازه میده با کلاس‌های Kotlin کار کنی، نه با جزئیات پیچیده دیتابیس.

فرض کن قیمت‌های امروز از سرور دریافت شدن. به جای اینکه فقط روی صفحه نمایش داده بشن و بعد از بسته شدن برنامه از بین برن، Repository اون‌ها رو داخل Room هم ذخیره می‌کنه. دفعه بعد که برنامه باز میشه، قبل از اینکه حتی درخواست جدیدی به سرور ارسال بشه، همین اطلاعات از Room خونده میشن و روی صفحه نمایش داده میشن. بعد اگر اینترنت در دسترس باشه، اطلاعات جدید از سرور دریافت میشن، جای اطلاعات قبلی رو می‌گیرن و دوباره داخل Room ذخیره میشن.

این روش دو مزیت بزرگ داره. اول اینکه برنامه حتی بدون اینترنت هم تا حد زیادی قابل استفاده باقی می‌مونه. دوم اینکه سرعت برنامه بیشتر میشه، چون خواندن اطلاعات از حافظه داخلی گوشی معمولاً چندین برابر سریع‌تر از ارسال درخواست به اینترنته.

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

در پروژه قیمت‌بان هم میشه دقیقاً از همین الگو استفاده کرد. وقتی کاربر برای اولین بار برنامه رو باز می‌کنه، قیمت‌ها از API دریافت میشن و داخل Room ذخیره میشن. اگر چند ساعت بعد اینترنت قطع باشه، برنامه همچنان آخرین قیمت‌های ذخیره‌شده رو نمایش میده. وقتی اینترنت دوباره برقرار بشه، Repository اطلاعات جدید رو از سرور می‌گیره، دیتابیس محلی رو به‌روزرسانی می‌کنه و رابط کاربری هم با استفاده از StateFlow تغییرات رو نمایش میده. از دید کاربر همه چیز کاملاً طبیعی به نظر میاد، در حالی که پشت صحنه سه بخش مختلف در حال همکاری هستن. سرور، Room و رابط کاربری.

اگر به مسیر این چند درس نگاه کنی، می‌بینی که معماری برنامه کم‌کم کامل‌تر میشه. Retrofit مسئول ارتباط با سروره، JSON زبان مشترک بین برنامه و API هست، Repository تصمیم می‌گیره اطلاعات از کجا خونده بشن، Room نسخه محلی اطلاعات رو نگه می‌داره و ViewModel نتیجه نهایی رو به Compose می‌رسونه. به همین خاطر، Room را نباید فقط یک دیتابیس بدونی. بلکه حافظه بلندمدت اپلیکیشنه. بخشی که باعث میشه برنامه سریع‌تر، پایدارتر و حتی بدون اینترنت هم تا حد زیادی قابل استفاده باقی بمونه.


درس هجدهم

چرا ViewModel نباید بدونه اطلاعات از کجا میان؟

Repository Pattern
مدیر داده‌های برنامه کیه؟

تا اینجا معماری برنامه کم‌کم شکل گرفته. ViewModel رو داریم، Retrofit برای ارتباط با سرور آماده است، Room هم اطلاعات رو داخل گوشی نگه می‌داره. اما حالا یک سؤال مهم به وجود میاد. فرض کن صفحه اصلی قیمت‌بان باز میشه و باید لیست قیمت‌ها نمایش داده بشه. این قیمت‌ها از کجا باید بیان؟ از سرور؟ از Room؟ اگر اینترنت قطع بود چی؟ اگر اطلاعات داخل Room قدیمی بود چی؟ اگر هم‌زمان هم اطلاعات محلی وجود داشت و هم اینترنت وصل بود، کدوم رو باید نمایش بدیم؟

ممکنه اولین ایده این باشه که همه این تصمیم‌ها رو داخل ViewModel بگیریم. یعنی ViewModel بررسی کنه اینترنت وصله یا نه، بعد تصمیم بگیره از API بخونه یا از دیتابیس، بعد اطلاعات رو ذخیره کنه و دوباره نتیجه رو به صفحه بفرسته.

در نگاه اول شاید این روش منطقی به نظر برسه، اما یک مشکل بزرگ داره. کم‌کم ViewModel تبدیل میشه به جایی که همه چیز داخلش قرار گرفته. ارتباط با سرور، کار با دیتابیس، مدیریت کش، بررسی اینترنت، مدیریت Session و حتی منطق برنامه. بعد از چند ماه، ViewModel به هزاران خط کد می‌رسه و هر تغییر کوچکی می‌تونه جای دیگه‌ای از برنامه رو خراب کنه.

در واقع دوباره به همان مشکلی برمی‌گردیم که قبل از معرفی MVVM داشتیم. فقط این بار به جای Activity، خود ViewModel بیش از حد شلوغ شده. برای حل همین مشکل، الگوی Repository Pattern به وجود اومد.

ایده Repository خیلی ساده است. ViewModel اصلاً نباید بدونه اطلاعات از کجا میان. تنها چیزی که براش اهمیت داره اینه که قیمت‌های امروز رو می‌خوام. اینکه این اطلاعات از سرور دریافت بشن، از Room خونده بشن، از حافظه موقت بیان یا حتی از یک فایل محلی، دیگر مسئولیت ViewModel نیست. این تصمیم‌ها همگی داخل Repository گرفته میشن.

میشه Repository رو مثل مدیر یک انبار بزرگ تصور کرد. فرض کن وارد یک فروشگاه میشی و از فروشنده یک کالا می‌خوای. فروشنده هیچ‌وقت بهت نمیگه الان برم ببینم داخل انبار شماره یک هست یا از انبار شماره دو بیارم یا از کارخانه سفارش بدم. تو فقط کالا رو درخواست می‌کنی و فروشنده خودش تصمیم می‌گیره بهترین راه برای تهیه اون کالا چیه. Repository هم دقیقاً همین نقش رو داره. ViewModel فقط درخواست داده می‌کنه و Repository تصمیم می‌گیره بهترین منبع اطلاعات کدومه.

مثلاً در قیمت‌بان، فرض کن کاربر صفحه قیمت برنج رو باز کرده. Repository ابتدا بررسی می‌کنه آیا اطلاعات داخل Room وجود داره یا نه. اگر وجود داشته باشه، همون رو سریع به ViewModel برمی‌گردونه تا صفحه بلافاصله نمایش داده بشه. بعد در پس‌زمینه یک درخواست به سرور ارسال می‌کنه. اگر اطلاعات جدیدی دریافت شد، Room رو به‌روزرسانی می‌کنه و چون ViewModel به تغییرات Room گوش میده، رابط کاربری هم به صورت خودکار اطلاعات جدید رو نمایش میده. تمام این فرایند بدون اینکه ViewModel بدونه دقیقاً چه اتفاقی افتاده انجام میشه.

حالا فرض کن چند ماه بعد تصمیم بگیری سرورت رو عوض کنی یا به جای Retrofit از کتابخانه دیگه‌ای استفاده کنی. اگر ViewModel مستقیماً با Retrofit کار کرده باشه، باید ده‌ها صفحه از برنامه رو تغییر بدی. اما اگر همه ارتباط‌ها از طریق Repository انجام شده باشه، فقط داخل Repository تغییر ایجاد می‌کنی و بقیه برنامه حتی متوجه این تغییر نمیشه. این یکی از مهم‌ترین مزیت‌های Repository هست. وابستگی بخش‌های مختلف برنامه رو کم می‌کنه.

مزیت دیگه اینه که Repository قوانین مربوط به داده‌ها رو در یک نقطه متمرکز می‌کنه. مثلاً اگر قانون پروژه این باشه که اطلاعاتی که کمتر از ده دقیقه از دریافتشون گذشته دوباره از سرور گرفته نشن، این قانون فقط یک بار داخل Repository نوشته میشه. اگر فردا این زمان به پنج دقیقه تغییر کنه، لازم نیست کل پروژه رو بگردی. فقط Repository رو تغییر میدی.

در پروژه قیمت‌بان، Repository در واقع مرکز تصمیم‌گیری تمام اطلاعات برنامه است. ViewModel فقط درخواست می‌کنه، Repository تصمیم می‌گیره، Retrofit در صورت نیاز با سرور ارتباط برقرار می‌کنه، Room اطلاعات رو ذخیره یا بازیابی می‌کنه و در نهایت نتیجه دوباره به ViewModel برمی‌گرده. به همین دلیل، ViewModel نه چیزی درباره HTTP می‌دونه، نه درباره SQL، نه درباره JWT و نه حتی درباره اینکه اطلاعات از اینترنت اومدن یا از حافظه گوشی.

اگر بخوای مسیر حرکت داده‌ها رو از اینجا به بعد تصور کنی، معماری برنامه خیلی تمیزتر شده. کاربر با رابط کاربری تعامل می‌کنه، Compose رویداد رو به ViewModel می‌فرسته، ViewModel فقط درخواست داده می‌کنه، Repository بهترین منبع اطلاعات رو انتخاب می‌کنه و نتیجه رو دوباره به ViewModel برمی‌گردونه. هر بخش فقط یک مسئولیت داره و دقیقاً همین موضوع باعث میشه پروژه با بزرگ‌تر شدن، همچنان قابل فهم و قابل نگهداری باقی بمونه.

به همین خاطر، Repository را نباید فقط یک کلاس واسطه بین ViewModel و Retrofit بدونی. Repository مغز مدیریت داده‌های برنامه است. جایی که تصمیم می‌گیره هر اطلاعات از کجا بیاد، چه زمانی به‌روزرسانی بشه، کجا ذخیره بشه و در نهایت چه چیزی به ViewModel تحویل داده بشه. شاید به همین دلیل باشه که در بیشتر پروژه‌های حرفه‌ای اندروید، Repository یکی از مهم‌ترین ستون‌های معماری به حساب میاد.


درس نوزدهم

چرا بعضی اطلاعات فوراً نمایش داده میشن، حتی قبل از اینکه اینترنت جواب بده؟

Cache
چرا برنامه لازم نیست هر بار همه چیز رو از سرور بگیره؟

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

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

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

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

حالا ممکنه این سؤال پیش بیاد که Cache با Room Database چه فرقی داره؟ چون در درس قبل گفتیم اطلاعات داخل Room هم ذخیره میشن. تفاوت اصلی در هدف اون‌هاست.

Room یک دیتابیس دائمیه. یعنی اطلاعات رو نگه می‌داره تا حتی اگر برنامه بسته شد یا گوشی خاموش شد، باز هم قابل استفاده باشن. اما Cache بیشتر یک استراتژی مدیریت داده است تا یک محل ذخیره‌سازی. یعنی تصمیم می‌گیره چه زمانی از اطلاعات قبلی استفاده کنیم و چه زمانی دوباره به سراغ سرور بریم.

در خیلی از پروژه‌های اندرویدی، خود Room نقش Cache رو هم بازی می‌کنه. یعنی اطلاعات داخل Room ذخیره میشن، اما Repository تصمیم می‌گیره آیا این اطلاعات هنوز قابل استفاده هستن یا باید دوباره از API دریافت بشن.

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

به این روش معمولاً Cache First میگن. یعنی اول از اطلاعات محلی استفاده می‌کنیم و فقط در صورت نیاز سراغ اینترنت میریم. البته همیشه هم نباید از Cache استفاده کرد. بعضی اطلاعات باید دقیقاً همان لحظه از سرور دریافت بشن. مثلاً موجودی حساب بانکی، پرداخت‌های مالی یا وضعیت یک تراکنش، اطلاعاتی نیستن که بشه ساعت‌ها نسخه قدیمی اون‌ها رو نمایش داد. بنابراین یکی از وظایف مهم Repository اینه که تشخیص بده کدوم اطلاعات می‌تونن Cache بشن و کدوم اطلاعات باید همیشه به صورت زنده از سرور دریافت بشن.

در پروژه قیمت‌بان، کش کردن اطلاعات می‌تونه تجربه کاربری رو کاملاً تغییر بده. قیمت کالاهایی که چند دقیقه قبل دریافت شدن، می‌تونن فوراً روی صفحه نمایش داده بشن و کاربر احساس کنه برنامه بسیار سریعه. هم‌زمان، در پس‌زمینه آخرین قیمت‌ها از API دریافت میشن. اگر تغییری وجود داشته باشه، Room به‌روزرسانی میشه و StateFlow هم تغییرات رو به Compose منتقل می‌کنه. کاربر نه صفحه خالی می‌بینه، نه منتظر دانلود اطلاعات می‌مونه و نه احساس می‌کنه برنامه کند شده. در حالی که پشت صحنه چندین بخش مختلف در حال همکاری هستن.

اگر به معماری برنامه تا اینجا نگاه کنی، می‌بینی هر قطعه نقش مشخصی داره. Retrofit اطلاعات رو از سرور می‌گیره، Room اون‌ها رو داخل گوشی نگه می‌داره، Repository تصمیم می‌گیره چه زمانی از اطلاعات محلی و چه زمانی از اطلاعات سرور استفاده بشه و Cache هم استراتژی‌ایه که باعث میشه این تصمیم‌ها هم به نفع سرعت باشن و هم به نفع مصرف اینترنت. به همین خاطر، Cache را نباید فقط ذخیره اطلاعات بدونی. بلکه روشی هوشمندانه برای اینه که برنامه بین سرعت، مصرف اینترنت و تازه بودن اطلاعات یک تعادل مناسب برقرار کنه.


درس بیستم

اگر اینترنت نباشه، آیا برنامه باید از کار بیفته؟

Offline First
چرا برنامه باید اول برای نبود اینترنت طراحی بشه؟

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

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

Offline First یعنی از همان روز اول برنامه رو طوری طراحی کنیم که انگار هیچ اینترنتی وجود نداره. بعد اگر اینترنت در دسترس بود، اون رو به یک مزیت تبدیل کنیم، نه به یک پیش‌نیاز. این شاید در نگاه اول عجیب به نظر برسه، اما دقیقاً همین طرز فکر باعث شده بسیاری از اپلیکیشن‌های مدرن این‌قدر روان و سریع باشن.

فرض کن قیمت‌بان رو باز می‌کنی. در معماری قدیمی، برنامه اول به سرور درخواست می‌فرستاد، چند ثانیه منتظر می‌موند و بعد اگر پاسخ می‌رسید، اطلاعات رو نمایش می‌داد. اما در معماری Offline First، برنامه اصلاً منتظر اینترنت نمی‌مونه. اول اطلاعاتی که داخل Room وجود دارن رو نمایش میده. یعنی کاربر تقریباً بلافاصله صفحه رو می‌بینه. بعد در پس‌زمینه بررسی می‌کنه که آیا اینترنت در دسترس هست یا نه. اگر اینترنت وجود داشته باشه، اطلاعات جدید رو دریافت می‌کنه، Room رو به‌روزرسانی می‌کنه و رابط کاربری هم خودش تغییر می‌کنه. اگر هم اینترنت وجود نداشته باشه، برنامه همچنان قابل استفاده باقی می‌مونه و فقط آخرین اطلاعات موجود رو نمایش میده.

نکته جالب اینه که در این معماری، Room دیگر فقط یک دیتابیس محلی نیست. بلکه منبع اصلی اطلاعات برای رابط کاربریه. یعنی Compose و ViewModel معمولاً مستقیماً با Room کار می‌کنن، نه با Retrofit. Retrofit فقط مسئول همگام‌سازی اطلاعات با سروره. این تغییر دیدگاه، یکی از مهم‌ترین تفاوت‌های معماری‌های قدیمی و مدرنه.

البته Offline First فقط درباره خواندن اطلاعات نیست، بلکه برای ارسال اطلاعات هم هست. فرض کن کاربر وقتی اینترنت نداره، یک قیمت جدید برای یک کالا ثبت می‌کنه. آیا باید بهش بگیم اینترنت وصل نیست، بعداً دوباره تلاش کن؟ در بسیاری از برنامه‌های حرفه‌ای، جواب منفیه. برنامه ابتدا ویرایش‌های کاربر که باید به سرور فرستادن بشن رو داخل دیتابیس محلی ذخیره می‌کنه و به کاربر نشون میده که عملیات انجام شده. بعد هر زمان اینترنت دوباره برقرار شد، همان تغییر به صورت خودکار برای سرور ارسال میشه. از دید کاربر همه چیز فوری انجام شده، در حالی که همگام‌سازی واقعی چند دقیقه بعد اتفاق افتاده.

البته این روش چالش‌های خودش رو هم داره. فرض کن کاربر در حالت آفلاین قیمت یک کالا رو تغییر داده، اما هم‌زمان شخص دیگری هم همان قیمت رو از یک دستگاه دیگه ویرایش کرده. حالا وقتی اینترنت وصل میشه، سرور با دو نسخه متفاوت از یک اطلاعات روبه‌رو میشه. اینجاست که برنامه باید قوانین مشخصی برای حل تعارض داشته باشه. مثلاً آخرین تغییر پذیرفته بشه، از کاربر سؤال پرسیده بشه یا قوانین پیچیده‌تری برای ادغام اطلاعات وجود داشته باشه. به همین دلیل، طراحی یک سیستم Offline First فقط اضافه کردن Room نیست. بلکه نیاز به فکر کردن درباره همگام‌سازی و مدیریت تعارض‌ها هم داره.

در پروژه قیمت‌بان هم میشه از همین معماری استفاده کرد. آخرین قیمت‌ها همیشه داخل Room نگهداری میشن و رابط کاربری مستقیماً از همون اطلاعات استفاده می‌کنه. هر زمان اینترنت برقرار باشه، Repository اطلاعات جدید رو از API دریافت می‌کنه و دیتابیس محلی رو به‌روزرسانی می‌کنه. اگر کاربر عملیاتی انجام بده که نیاز به ارسال به سرور داشته باشه، برنامه می‌تونه در صورت نبود اینترنت، اون درخواست رو موقتاً نگه داره و بعد از برقراری اتصال، به صورت خودکار ارسالش کنه. نتیجه اینه که برنامه حتی در شرایط نامناسب شبکه هم حس یک نرم‌افزار روان و قابل اعتماد رو حفظ می‌کنه.

اگر به چند درس اخیر نگاه کنی، می‌بینی که همه آن‌ها در نهایت به همین نقطه ختم میشن. Repository تصمیم می‌گیره اطلاعات از کجا بیان، Room نسخه محلی اون‌ها رو نگه می‌داره، Cache باعث میشه لازم نباشه همیشه به سرور مراجعه کنیم و Offline First همه این ایده‌ها رو کنار هم قرار میده تا اینترنت از یک وابستگی به یک امتیاز تبدیل بشه. شاید مهم‌ترین ویژگی یک اپلیکیشن مدرن همین باشه. برنامه نباید فقط وقتی اینترنت عالیه خوب کار کنه، بلکه باید حتی در بدترین شرایط هم تا حد ممکن تجربه‌ای روان و قابل اعتماد برای کاربر فراهم کنه.