ساخت آداپتر واکنشی و ناهمگون در کاتلین – راهنمای کاربردی

۲۸۹ بازدید
آخرین به‌روزرسانی: ۲۷ شهریور ۱۴۰۲
زمان مطالعه: ۱۰ دقیقه
دانلود PDF مقاله
ساخت آداپتر واکنشی و ناهمگون در کاتلین – راهنمای کاربردیساخت آداپتر واکنشی و ناهمگون در کاتلین – راهنمای کاربردی

اگر در زمینه توسعه اندروید تجربه داشته باشید، احتمالاً با موقعیتی موجه شده‌اید که نیاز به پیاده‌سازی چندین نوع «نما» (View) در recyclerview بوده است، شاید رایج‌ترین موقعیتی که با این مسئله مواجه می‌شویم، در زمان پیاده‌سازی قابلیت «صفحه‌بندی» (paging) باشد. در این مقاله از این موقعیت برای ساخت آداپتر واکنشی و ناهمگون در کاتلین بهره می‌گیریم. در انتهای مقاله نیز یک ریپوی گیت‌هاب از کد نمونه مربوطه ارائه شده است.

997696

روش متعارف

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

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

آداپتر واکنشی و ناهمگون در کاتلین

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

اگر از الگوی فوق جهت ساخت لی‌آوت‌ها برای انواع مختلف نماها پیروی کنید، در نهایت با مشکل بزرگی مواجه خواهید شد. استفاده از بلوک‌های if/else یا switch در viewtypes و onCreateViweHolder تنها تا حدودی این مشکل را حل می‌کند. برای حل این مسئله به صورت کامل باید یک راه‌حل ژنریک پیاده‌سازی کنیم که برای هر viewtype بدون درگیر کردن بلوک‌های if/esle کار کند.

به عنوان یک توصیه خوب جمله زیر را از «اسکات مِیِر» (Scott Meyers) همیشه در خاطر خود داشته باشید:

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

یک روش بهتر و کارآمدتر

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

برای افرادی که نمی‌دانند DiffUtil چیست

DiffUtil یک کلاس کاربردی است که می‌تواند تفاوت بین دو لیست را محاسبه کرده و یک لیست از عملیات به‌روزرسانی ارائه کند که لیست اول را به لیست دوم تبدیل می‌کند.

بنابراین در زمان به‌روزرسانی با یک لیست جدید از DiffUtil استفاده می‌کنیم. DiffUtil بررسی می‌کند کدام آیتم‌ها از لیست موجود به لیست اخیراً ارسال شده تغییر یافته‌اند و یک لیست جدید با آن آیتم‌ها ایجاد می‌کند.

سپس آداپتر با لیست جدید طوری به‌روزرسانی می‌شود که تنها آیتم‌هایی که تغییر یافته‌اند، به‌روزرسانی شوند. این امکان بسیار جالبی است.

گام 1

ابتدا یک کلاس مجرد ایجاد می‌کنیم که DiffUtil.ItemCallback را بسط می‌دهد و منطق ایجاد و اتصال یک DiffUtil.ItemCallback را برای آن نوع آیتم کپسوله‌سازی می‌کند. به کد زیر نگاه کنید:

onViewRecycled و onViewDetachedFromWindow باز هستند، زیرا همه viewholder-ها نیازی به پیاده‌سازی آن‌ها ندارند. شما در ادامه با دلیل استفاده از این کلاس binder برای اتصال یک «نگه‌دارنده نما» (view holder) به آداپتر آشنا خواهد شد. فعلاً آن را به عنوان یک کلاس view holder مبنا تصور کنید.

گام 2

یک کلاس DiffUtil ژنریک پیاده‌سازی می‌کنیم که برای هر کلاس مدل به صورت زیر عمل می‌کند:

در کد فوق دو شیء typealias، یکی برای کلاس‌های مدل و دیگری برای کلاس مدل bind با viewholder اختصاصی به صورت جفت «کلید-مقدار» ساخته‌ایم. به صورت پیش‌فرض کلاس DiffUtil محتوای آیتم‌هایی را که تغییر یافته یا نیافته‌اند را با دو تابع areItemsTheSame و areContentsTheSame بررسی می‌کند. کدنویسی ژنریک را در FeedDiffCallback به طوری اجرا می‌کنیم که صرف نظر از کلاس مدل در هر حالتی کار کند.

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

گام 3

تا اینجا همه کارهایی که برای ایجاد آداپتر ناهمگون نیاز بود را انجام داده‌ایم. اینک کافی است یک آداپتر ایجاد کنیم و از viewbinder و DiffUtils سفارشی که قبلاً ساختیم بهره بگیریم.

به کد آداپتر زیر توجه کنید:

ابتدا یک map با همه کلاس‌های مدل و ViewHolder در سازنده چنان که در کد فوق دیده می‌شود، ارسال می‌کنیم. در گام بعدی شیوه انجام کار را می‌بینید. سپس یک map با نام viewTypeToBinders و با استفاده از تابع mapkeys روی viewbinders که در سازنده ارسال شده‌اند، می‌سازیم.

هدف از تابع mapkeys چیست؟

اگر هر یک از دو مدخل به کلیدهای یکسانی نگاشت شده باشند، مقدار دومی، مقدار مربوط به اولی را بازنویسی می‌کند. map بازگشتی ترتیب تکرار مدخل نقشه اصلی را حفظ می‌کند. بدین ترتیب این viewTypeToBinders هیچ مدخل تکراری از viewholder-ها نخواهد داشت. اکنون زمان آن رسیده که به بررسی تابع‌های override-شده مانند getItemViewType ،onCreateViewHolder و غیره بپردازیم. به طور کلی، این تابع‌ها یا مقادیر بازگشت می‌دهند و یا تابع‌های دیگر را بر اساس viewtype-ها تحریک می‌کنند.

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

getFeedItemType مقدار Int بازگشت می‌دهد، بنابراین viewHolder واقعی لی‌آوت را به صورت مستقیم ارسال می‌کند و از آن لی‌آوت به عنوان viewtype در آداپتری که برای همه viewtype-ها ژنریک است استفاده می‌کند. در ادامه طرز کار این آداپتر را با دو تابع توضیح می‌دهیم:

getItemViewType

این تابع نوع آیتم مورد بررسی را به منظور «بازیافت» (recycle) کردن «نما» (View) بازگشت می‌دهد. در اینجا از viewbinder-ی استفاده می‌کنیم که در سازنده برای بازیابی viewtype ارائه شده، استفاده می‌کنیم. به این منظور از یک آیتم والد که در موقعیت دریافت شده قرار دارد بهره می‌گیریم.

onCreateViewHolder

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

در ادامه onCreateViewHolder روی آن کلاس نگه‌دارنده نمای خاص فراخوانی می‌شود.

به طور خلاصه کاری که در این بخش انجام دادیم این است که همه کدهای مربوط به آداپتر از قبیل تمییز viewtype-ها، viewholder-ها و غیره از طریق یک کلاس مجرد به viewholder-های خاص منتقل می‌شود. این viewholder یک کلاس مدل و انواع viewholder را به عنوان پارامتر می‌گیرد. بنابراین به جای نوشتن همه کد، داده‌ها را از خود viewholder می‌گیریم.

گام 4

اینک زمان آن رسیده است که viewholder-هایی که در آداپتر پیاده‌سازی کردیم را بسازیم. ساختن یک viewholder برای این آداپتر ناهمگون نیازمند دو مرحله است. ابتدا باید یک «اتصال‌دهنده نما» (view binder) بسازیم که viewholder کنونی را به آداپتر وصل کند و سپس کلاس viewholder واقعی را بسازیم. به کد زیر توجه کنید:

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

شما می‌توانید به هر تعدادی که دوست دارید view holders بسازید و دیگر نیازی به انجام کاری در آداپتر ندارید. آداپتر طوری طراحی شده که نسبت به viewholder-ها ژنریک باشد. مورد دیگری که شاید نیاز به توضیح داشته باشد، تابع‌های مرتبه بالا هستند که در viewholder برای فراخوانی کلیک آیتم‌ها استفاده شده‌اند:

val block: (data: VeriticalImageModel) -> Unit

برای درک بهتر این بخش باید با تابع‌های مرتبه بالا آشنا باشید.

گام 5

در این بخش آداپتر را ایجاد کرده و به recyclerview انتساب می‌دهیم. برای ایجاد آداپتر باید یک map با کلاس مدل و viewbinder به صورت جفت کلید–مقدار ارسال کنیم. به کد زیر توجه کنید:

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

در این نقطه، می‌توانید هر تعداد از viewtype که می‌خواهید بدون افزودن هیچ کدی به آداپتر پیاده‌سازی کنید و زیبایی این روش در همین است. بدین ترتیب به پایان این بخش می‌رسیم. در بخش بعدی به بررسی recyclerview-های تودرتو می‌پردازیم.

Recyclerview‌-های تودرتو

آن FeedAdapter که در این مقاله ساخته‌ایم، چنان قدرتمند و ژنریک است که بدون هیچ تغییری در آداپتر حتی با Recyclerview‌-های تودرتو نیز کار می‌کند. بدین ترتیب می‌توانیم viewholder را با viewholder تودرتو بسازیم طوری که گویی یک viewholder نرمال ایجاد می‌کنیم.

به کد زیر توجه کنید:

همچنان که در کد فوق می‌بینید، تفاوت چندانی با کد viewholder نرمال ندارد. تنها تفاوت در این است که باید وهله آداپتر داخلی را ایجاد کنیم و یک map از viewholder-ها و کلاس‌های مدل ساخته و آن‌ها را به recyclerview انتساب دهیم. نتیجه به صورت زیر است:

آداپتر واکنشی و ناهمگون در کاتلین

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

کاری که باید انجام دهیم، این است که getLayoutManagerState مربوط به recyclerview تودرتو را ذخیره کنیم و زمانی که آیتم مجدداً به نما اتصال یافت، وهله را بازیابی کنیم. تنها تفاوت در این است که باید تابع‌های onViewRecycled و onViewDetachedFromWindow مربوط به FeedItemViewBinder را پیاده‌سازی کنیم.

بدین ترتیب به صورت داخلی زمانی که آداپتر روی recyclerview تودرتو تنظیم می‌شود، getLayoutManagerState مربوط به آن recyclerview را ذخیره می‌کنیم و زمانی که بعداً به آن recyclerview می‌رسیم، حالت قدیم را به layout manager بازیابی می‌کنیم. به کد زیر توجه کنید:

در کد فوق، هر زمان که recyclerview تودرتو از پنجره جدا و یا recycle می‌شود، حالت کنونی layoutmanager که به recyclerview داخلی الصاق یافته را ذخیره می‌کنیم. سپس هر زمان که نما دوباره الصاق یابد، موقعیت اسکرول را بازیابی می‌کنیم.

آداپتر واکنشی و ناهمگون در کاتلین

اکنون feed adapter که ساخته‌ایم با هر تعداد viewtype کار می‌کند و همچنین هر تعداد recyclerview تودرتو که باشند، بدون تغییر در هیچ چیز در آداپتر قابل پیاده‌سازی هستند. زیبایی این کد ارائه شده از سوی گوگل در آن است که هیچ چیز به صورت مستقیم در آداپتر مدیریت نمی‌شود، بلکه همه چیز در آداپتر عملیاتی می‌شود. به این ترتیب به انتهای این راهنما می‌رسیم. کد نمونه پروژه معرفی شده در این مقاله را می‌توانید در این ریپوی گیت‌هاب (+) ‌ملاحظه کنید.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۱ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
android-dev-hacks
دانلود PDF مقاله
نظر شما چیست؟

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