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

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

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

روش متعارف

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

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

1class ItemViewHolder extends RecyclerView.ViewHolder{
2    
3}
4class LoadingViewHolder extends RecyclerView.ViewHolder{
5   
6}
7override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
8    return if (viewType == LOADING) {
9        LoadingViewHolder(mLayoutInflater.inflate(R.layout.loading, parent, false))
10    } else {
11        ItemViewHolder(mLayoutInflater.inflate(R.layout.item_layout, parent, false))
12    }
13}

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

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

اگر از الگوی فوق جهت ساخت لی‌آوت‌ها برای انواع مختلف نماها پیروی کنید، در نهایت با مشکل بزرگی مواجه خواهید شد. استفاده از بلوک‌های 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 را برای آن نوع آیتم کپسوله‌سازی می‌کند. به کد زیر نگاه کنید:

1abstract class FeedItemViewBinder<M, in VH : ViewHolder>(
2    val modelClass: Class<out M>) : DiffUtil.ItemCallback<M>() {
3
4    abstract fun createViewHolder(parent: ViewGroup): ViewHolder
5    abstract fun bindViewHolder(model: M, viewHolder: VH)
6    abstract fun getFeedItemType(): Int
7
8    // Having these as non abstract because not all the viewBinders are required to implement them.
9    open fun onViewRecycled(viewHolder: VH) = Unit
10    open fun onViewDetachedFromWindow(viewHolder: VH) = Unit
11}

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

گام 2

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

1typealias FeedItemClass = Class<out Any>
2typealias FeedItemBinder = FeedItemViewBinder<Any, ViewHolder>
3
4internal class FeedDiffCallback(
5    private val viewBinders: Map<FeedItemClass, FeedItemBinder>
6) : DiffUtil.ItemCallback<Any>() {
7
8    override fun areItemsTheSame(oldItem: Any, newItem: Any): Boolean {
9        if (oldItem::class != newItem::class) {
10            return false
11        }
12        return viewBinders[oldItem::class.java]?.areItemsTheSame(oldItem, newItem) ?: false
13    }
14
15    override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean {
16        return viewBinders[oldItem::class.java]?.areContentsTheSame(oldItem, newItem) ?: false
17    }
18}

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

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

گام 3

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

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

1class FeedAdapter(
2    private val viewBinders: Map<FeedItemClass, FeedItemBinder>
3) : ListAdapter<Any, ViewHolder>(FeedDiffCallback(viewBinders)) {
4
5    private val viewTypeToBinders = viewBinders.mapKeys { it.value.getFeedItemType() }
6
7    private fun getViewBinder(viewType: Int): FeedItemBinder = viewTypeToBinders.getValue(viewType)
8
9    override fun getItemViewType(position: Int): Int =
10        viewBinders.getValue(super.getItem(position).javaClass).getFeedItemType()
11
12    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
13        return getViewBinder(viewType).createViewHolder(parent)
14    }
15
16    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
17        return getViewBinder(getItemViewType(position)).bindViewHolder(getItem(position), holder)
18    }
19
20    override fun onViewRecycled(holder: ViewHolder) {
21        getViewBinder(holder.itemViewType).onViewRecycled(holder)
22        super.onViewRecycled(holder)
23    }
24
25    override fun onViewDetachedFromWindow(holder: ViewHolder) {
26        getViewBinder(holder.itemViewType).onViewDetachedFromWindow(holder)
27        super.onViewDetachedFromWindow(holder)
28    }
29}

ابتدا یک 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 ارائه شده، استفاده می‌کنیم. به این منظور از یک آیتم والد که در موقعیت دریافت شده قرار دارد بهره می‌گیریم.

1override fun getItemViewType(position: Int): Int =
2    viewBinders.getValue(super.getItem(position).javaClass).getFeedItemType()

onCreateViewHolder

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

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

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

گام 4

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

1class VerticalImagesViewBinder(val block : (data: VeriticalImageModel) -> Unit) : FeedItemViewBinder<VeriticalImageModel, VerticalImagesViewHolder>(
2    VeriticalImageModel::class.java) {
3
4    override fun createViewHolder(parent: ViewGroup): VerticalImagesViewHolder {
5        return VerticalImagesViewHolder(
6            LayoutInflater.from(parent.context).inflate(getFeedItemType(), parent, false),block)
7    }
8
9    override fun bindViewHolder(model: VeriticalImageModel, viewHolder: VerticalImagesViewHolder) {
10        viewHolder.bind(model)
11    }
12
13    override fun getFeedItemType() = R.layout.adapter_vertical_image
14
15    override fun areContentsTheSame(oldItem: VeriticalImageModel, newItem: VeriticalImageModel) = oldItem == newItem
16
17    override fun areItemsTheSame(oldItem: VeriticalImageModel, newItem: VeriticalImageModel) : Boolean {
18        return oldItem.Image == newItem.Image
19    }
20}
21
22
23class VerticalImagesViewHolder(val view : View, val block : (data: VeriticalImageModel) -> Unit)
24    : RecyclerView.ViewHolder(view) {
25
26    fun bind(data: VeriticalImageModel) {
27
28        itemView.setOnClickListener {
29            block(data)
30        }
31
32        itemView.apply {
33            Glide
34                .with(itemView.context)
35                .load(data.Image)
36                .centerCrop()
37                .into(im_vertical)
38        }
39    }
40}

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

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

val block: (data: VeriticalImageModel) -> Unit

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

گام 5

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

1private var adapter: FeedAdapter? = null
2
3fun verticalImageClick(data : VeriticalImageModel){
4        ...
5}
6
7private fun showFeedItems(recyclerView: RecyclerView, list: ArrayList<Any>?) {
8    if (adapter == null) {
9        val viewBinders = mutableMapOf<FeedItemClass, FeedItemBinder>()
10        val verticalImagesViewBinder = VerticalImagesViewBinder { data : VeriticalImageModel ->
11            verticalImageClick(data)}
12        @Suppress("UNCHECKED_CAST")
13        viewBinders.put(
14            verticalImagesViewBinder.modelClass,
15            verticalImagesViewBinder as FeedItemBinder)
16        adapter = FeedAdapter(viewBinders)
17    }
18    recyclerView.apply {
19        layoutManager = LinearLayoutManager(this@MainActivity, LinearLayoutManager.VERTICAL,false)
20    }
21    if (recyclerView.adapter == null) {
22        recyclerView.adapter = adapter
23    }
24    (recyclerView.adapter as FeedAdapter).submitList(list ?: emptyList())
25}

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

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

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

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

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

1class HorizontalImagesListViewBinder(val block : (data: HorizontalImageModel) -> Unit)
2    : FeedItemViewBinder<HorizontalImageListModel, HorizontalImagesListViewHolder>(
3    HorizontalImageListModel::class.java) {
4
5    override fun createViewHolder(parent: ViewGroup): HorizontalImagesListViewHolder {
6        return HorizontalImagesListViewHolder(
7            LayoutInflater.from(parent.context).inflate(getFeedItemType(), parent, false),block)
8    }
9
10    override fun bindViewHolder(model: HorizontalImageListModel, viewHolder: HorizontalImagesListViewHolder) {
11        viewHolder.bind(model)
12    }
13
14    override fun getFeedItemType() = R.layout.adapter_recycleriew
15
16    override fun areContentsTheSame(oldItem: HorizontalImageListModel, newItem: HorizontalImageListModel) = oldItem == newItem
17
18    override fun areItemsTheSame(oldItem: HorizontalImageListModel, newItem: HorizontalImageListModel) : Boolean {
19        return oldItem.id == newItem.id
20    }
21}
22
23
24class HorizontalImagesListViewHolder(val view : View, val block : (data: HorizontalImageModel) -> Unit)
25    : RecyclerView.ViewHolder(view) {
26
27    fun bind(data: HorizontalImageListModel) {
28
29        var adapter : FeedAdapter? = null
30
31        itemView.apply {
32            val horizontalImagesViewBinder = HorizontalImagesViewBinder { horizontalImageModel : HorizontalImageModel ->
33                block(horizontalImageModel)}
34            val viewBinders = mutableMapOf<FeedItemClass, FeedItemBinder>()
35            @Suppress("UNCHECKED_CAST")
36            viewBinders.put(
37                horizontalImagesViewBinder.modelClass,
38                horizontalImagesViewBinder as FeedItemBinder)
39            adapter = FeedAdapter(viewBinders)
40            tv_horizontal_header?.text = data.title
41            adapter_recycllerview?.apply {
42
43                layoutManager = LinearLayoutManager(adapter_recycllerview?.context,
44                    LinearLayoutManager.HORIZONTAL,false)
45                if (adapter_recycllerview?.adapter == null) {
46                    adapter_recycllerview?.adapter = adapter
47                }
48                (adapter_recycllerview?.adapter as FeedAdapter).submitList(
49                    data.Images as List<Any>? ?: emptyList())
50            }
51        }
52    }
53}

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

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

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

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

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

1private lateinit var horizontalImagesViewBinder : HorizontalImagesListViewBinder
2
3companion object {
4        private const val BUNDLE_KEY_LAYOUT_HORIZONTAL_LIST_STATE = "horizontal_list_layout_manager"
5}
6
7override fun onSaveInstanceState(outState: Bundle) {
8        if (::horizontalImagesViewBinder.isInitialized) {
9            outState.putParcelable(
10                BUNDLE_KEY_LAYOUT_HORIZONTAL_LIST_STATE,
11                horizontalImagesViewBinder.recyclerViewManagerState
12            )
13        }
14        super.onSaveInstanceState(outState)
15}
16
17override fun onCreate(savedInstanceState: Bundle?) {
18        super.onCreate(savedInstanceState)
19        setContentView(R.layout.activity_main)
20
21        horizontalImagesViewBinder = HorizontalImagesListViewBinder ({ data : HorizontalImageModel ->
22            horizontalImageClick(data)}, savedInstanceState?.getParcelable(
23            BUNDLE_KEY_LAYOUT_HORIZONTAL_LIST_STATE
24        ))
25
26        addData()
27}
28
29class HorizontalImagesListViewBinder(val block : (data: HorizontalImageModel) -> Unit,
30                                     var recyclerViewManagerState: Parcelable? = null)
31    : FeedItemViewBinder<HorizontalImageListModel, HorizontalImagesListViewHolder>(
32    HorizontalImageListModel::class.java) {
33
34    override fun createViewHolder(parent: ViewGroup): HorizontalImagesListViewHolder {
35        return HorizontalImagesListViewHolder(
36            LayoutInflater.from(parent.context).inflate(getFeedItemType(), parent, false),block)
37    }
38
39    override fun bindViewHolder(model: HorizontalImageListModel, viewHolder: HorizontalImagesListViewHolder) {
40        viewHolder.bind(model,recyclerViewManagerState)
41    }
42
43    override fun getFeedItemType() = R.layout.adapter_recycleriew
44
45    override fun areContentsTheSame(oldItem: HorizontalImageListModel, newItem: HorizontalImageListModel) = oldItem == newItem
46
47    override fun areItemsTheSame(oldItem: HorizontalImageListModel, newItem: HorizontalImageListModel) : Boolean {
48        return oldItem.id == newItem.id
49    }
50
51    override fun onViewRecycled(viewHolder: HorizontalImagesListViewHolder) {
52        saveInstanceState(viewHolder)
53    }
54
55    override fun onViewDetachedFromWindow(viewHolder: HorizontalImagesListViewHolder) {
56        saveInstanceState(viewHolder)
57    }
58
59   // Saving the present instance of the recyclerview every time before the nested recyclerview is detached or the view get recycled
60    fun saveInstanceState(viewHolder: HorizontalImagesListViewHolder) {
61        if (viewHolder.adapterPosition == RecyclerView.NO_POSITION) {
62            return
63        }
64        recyclerViewManagerState = viewHolder.getLayoutManagerState()
65    }
66
67}
68
69
70class HorizontalImagesListViewHolder(val view : View, val block : (data: HorizontalImageModel) -> Unit)
71    : RecyclerView.ViewHolder(view) {
72
73    private var layoutManager: RecyclerView.LayoutManager? = null
74
75    fun bind(data: HorizontalImageListModel,layoutManagerState: Parcelable?) {
76
77        var adapter : FeedAdapter? = null
78
79        itemView.setOnClickListener {
80
81        }
82
83        itemView.apply {
84            val horizontalImagesViewBinder = HorizontalImagesViewBinder { horizontalImageModel : HorizontalImageModel ->
85                block(horizontalImageModel)}
86            val viewBinders = mutableMapOf<FeedItemClass, FeedItemBinder>()
87            @Suppress("UNCHECKED_CAST")
88            viewBinders.put(
89                horizontalImagesViewBinder.modelClass,
90                horizontalImagesViewBinder as FeedItemBinder)
91            adapter = FeedAdapter(viewBinders)
92            tv_horizontal_header?.text = data.title
93            adapter_recycllerview?.apply {
94
95                layoutManager = LinearLayoutManager(adapter_recycllerview?.context,
96                    LinearLayoutManager.HORIZONTAL,false)
97
98            }
99            layoutManager = adapter_recycllerview?.layoutManager
100            if (adapter_recycllerview?.adapter == null) {
101                adapter_recycllerview?.adapter = adapter
102            }
103            (adapter_recycllerview?.adapter as FeedAdapter).submitList(
104                data.Images as List<Any>? ?: emptyList())
105            if (layoutManagerState != null) {
106                // restoring the old instance so that scroll position is synced
107                layoutManager?.onRestoreInstanceState(layoutManagerState)
108            }
109        }
110    }
111
112    // Saving the present instance of the recyclerview
113    fun getLayoutManagerState(): Parcelable? = layoutManager?.onSaveInstanceState()
114
115}

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

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

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

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

==

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

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