পোস্টটি পড়া হয়েছে 1,610 বার
recylcerview multi type view in a single list

Android অ্যাপে একটা RecyclerView তে একাধিক টাইপের View দেখানো

Post updated on 2nd October, 2019 at 03:44 pm

একই লিস্টের মধ্যে অনেক সময়েই বিভিন্ন ধরনের ডেটা শো করানোর দরকার হয়। সেইসব ভিন্ন ভিন্ন ধরনের ডেটার ক্লিক ইভেন্টও ভিন্ন হয়। যেমন ধরেন, ফেসবুকের টাইমলাইনে আমরা টেক্সট টাইপের পোস্ট, ইমেজ টাইপের পোস্ট, ভিডিও টাইপের পোস্ট আবার কখনো বিজ্ঞাপনের পোস্ট দেখতে পাই। একেক ধরনের পোস্টে ক্লিক করলে কিন্তু একেক ধরনের অ্যাকশন হয়। আজকের এই টিউটোরিয়ালে দেখব কিভাবে একটা RecyclerView-তে একাধিক ধরনের ভিউ শো করানো যায়।

এই পোস্টটি বুঝার জন্য আপনাকে আগে থেকে RecyclerView কিভাবে কাজ করে বা ReclyeclerView দিয়ে কিভাবে একটা লিস্ট শো করতে হয় সে ব্যাপারে জানা থাকতে হবে। আপনি যদি আগে RecyclerView এর সাথে পরিচিত না হয়ে থাকেন তাহলে আমার এই ব্লগ পোস্টটি দেখতে পারেন। এখানে CardView ও RecyclerView ব্যবহার করে Horizontal ও Vertical scrollable লিস্ট কিভাবে জেনারেট করতে হয় সেটা বিস্তারিত ব্যাখ্যা সহ দেখানো হয়েছে। তাই আজকের এই পোস্টে একই বিষয়গুলো রিপিট করা হবে না। আর টিউটোরিয়ালটার স্যাম্পল কোডগুলো দেখানো হবে Kotlin programming language ব্যবহার করে।

RecyclerView multi type view
RecyclerView multi type view

Problem Description

উপরের ছবির মত একটা timeline feed বানাতে হবে। অনেকটা ফেসবুকের মত হবে টাইমলাইনটা। আপাতত এই টাইমলাইনে শুধু ২ ধরনের ভিউ দেখা যাবে। প্রথমত, শুধু টেক্সট টাইপের পোস্ট দেখা যাবে। দ্বিতীয়ত, ইমেজ সহ পোস্ট দেখা যাবে। আপনি চাইলে প্র্যাকটিসের জন্য ভিডিও টাইপ পোস্ট ও বিজ্ঞাপন দেখানোর জন্য আলাদা ভিউ রাখতে পারেন। টিউটোরিয়ালটা সিম্পল রাখার জন্য উভয় টাইপের ভিউতে ক্লিক করলে আলাদা আলাদা Toast message দেখাবে। উল্লেখ্য, এই টাইমলাইনটা বানাতে হবে একটা মাত্র RecyclerView ব্যবহার করে।

Sample APK ফাইলটি ডাউনলোড করে টেস্ট করে দেখতে পারেন। ডাউনলোড লিংক

Solution

আমরা সাধারণত RecyclerView এর জন্য একটা custom Adapter ও একটা custom ViewHolder class বানাই। Adapter class এর ভিতর আমরা ViewHolder এ ডেটা populate করি ও ক্লিক ইভেন্ট নিয়ে কাজ করি। এই way তে কাজ করা হয়ে থাকে, সাধারণত যখন একটা লিস্টে এক ধরনের ডেটাই শো করতে চাই এবং সকল আইটেমের ক্লিক ইভেন্টও একই রকমের হয়। কিন্তু আমাদের প্রবলেমটা ভিন্ন। আমাদের লিস্টের সকল ভিউ এক রকম নয়। তাদের ক্লিক ইভেন্টও এক রকম নয়। আমাদের দরকার ২ রকমের ভিউ দেখানো আর ২ ভাবে তাদের ক্লিক ইভেন্ট হ্যান্ডেল করা।

Data Class

Timeline এর প্রতিটা পোস্টের ডেটা hold করার জন্য একটা Kotlin Data Class বানাই।

data class PostData(
    val userName: String,
    val userProfilePhotoUrl: String,
    val timeStamp: String,
    val postDescription: String,
    val postImageUrl: String
)

RecylerView-তে PostData ক্লাসের একটা লিস্ট শো করা হবে। একই ডেটা মডেল দিয়ে আমরা টেক্সট টাইপ পোস্ট আর ইমেজ টাইপ পোস্ট দুইটাই হ্যান্ডেল করব। এজন্য লজিক ব্যবহার করছি এরকম যে, যদি postImageUrl স্ট্রিং এর ভ্যালু empty হয় তাহলে ধরে নিব সেটা টেক্সট টাইপের পোস্ট। আর যদি postImageUrl এর ভ্যালু empty না হয় তাহলে ধরে নিব এটা একটা Image type post. আপনার প্রোজেক্টর চাহিদা অনুযায়ী অন্য লজিক দিয়ে ভিউগুলোকে uniquely identify করতে পারেন। এমন কি আপনার ডেটা ক্লাসে সরাসরি postType নামের ভ্যারিয়েবলও রাখতে পারেন যা কিনা সার্ভার থেকে বা অন্য ডেটা সোর্স থেকে আসবে।

ViewHolder Classes

আমরা যেহেতু ২ রকমের ভিউ নিয়ে কাজ করছি তাই দুইটা আলাদা custom ViewHolder ক্লাস বানিয়ে ফেলি।

class TextPostViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

    val profilePhoto = itemView.profilePhoto
    val profileName = itemView.profileName
    val timeStamp = itemView.timeStamp
    val postDescription = itemView.postDescription

}
class ImagePostViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

    val profilePhoto = itemView.profilePhoto
    val profileName = itemView.profileName
    val timeStamp = itemView.timeStamp
    val postDescription = itemView.postDescription
    val imageView = itemView.imageView
}

দুইটা ViewHolder এর জন্য দুইটি xml ফাইলে আলাদা আলাদা করে UI design করে রেখেছি। ইমেজ টাইপের পোস্টে টেক্সট টাইপের পোস্টের সবগুলো component ই আছে। ইমেজের ভিউটা বাড়তি আছে।

RecyclerView Adapter Class

চলুন এবার দেখে নিই Custom Adapter ক্লাসটা।

class TimelineRecyclerViewAdapter(private val postDataList : List<PostData>): RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    private val POST_TYPE_TEXT = 1
    private val POST_TYPE_IMAGE = 2

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val view: View
        return if (viewType == POST_TYPE_TEXT) {
            view = LayoutInflater.from(parent.context).inflate(R.layout.item_text_post, parent, false)

            TextPostViewHolder(view) //object of TextPostViewHolder will return
        } else {
            view = LayoutInflater.from(parent.context).inflate(R.layout.item_image_post, parent, false)

            ImagePostViewHolder(view) //object of ImagePostViewHolder will return
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {

        val postData = postDataList[position]
        val context = holder.itemView.context

        if (holder.itemViewType == POST_TYPE_TEXT) {
            val viewHolder = holder as TextPostViewHolder

            viewHolder.profileName.text = postData.userName
            Glide.with(context).load(postData.userProfilePhotoUrl).into(viewHolder.profilePhoto)
            viewHolder.timeStamp.text = postData.timeStamp
            viewHolder.postDescription.text = postData.postDescription

            viewHolder.itemView.setOnClickListener {
                Toast.makeText(context, "Text type post", Toast.LENGTH_SHORT).show()
            }

        } else {
            val viewHolder = holder as ImagePostViewHolder

            viewHolder.profileName.text = postData.userName
            Glide.with(context).load(postData.userProfilePhotoUrl).into(viewHolder.profilePhoto)
            viewHolder.timeStamp.text = postData.timeStamp
            viewHolder.postDescription.text = postData.postDescription

            Glide.with(holder.itemView.context).load(postData.postImageUrl).into(viewHolder.imageView)

            viewHolder.itemView.setOnClickListener {
                Toast.makeText(context, "Image type post", Toast.LENGTH_SHORT).show()
            }
        }
    }

    override fun getItemCount(): Int {
        return postDataList.size
    }

    override fun getItemViewType(position: Int): Int {
        return if (postDataList[position].postImageUrl.isEmpty()) POST_TYPE_TEXT else POST_TYPE_IMAGE
    }
}

TimelineRecyclerViewAdapter ক্লাসের constructor এ PostData ক্লাসের একটা লিস্ট পাঠানো হয়েছে। এরপর ক্লাসের শুরুতে আমরা দুইটা int variable declare করেছি দুই রকমের ভিউ বুঝানোর জন্য।

আমরা জানি onCreateViewHolder() মেথড আমাদের RecyclerView এর view generate করে রিটার্ন করে। যখন লিস্টে একই রকম ভিউ দেখাতে চাই তখন এই মেথডের ভিতর আমাদের ভিউয়ের জন্য বানানো xml layout ফাইলটা inflate করে View class এর একটা অবজেক্ট বানাই। এরপর সেই ভিউ দিয়ে custom ViewHolder ক্লাসের একটা অবজেক্ট রিটার্ন করে দিই। কিন্তু আমাদের এই প্রবলেমের ক্ষেত্রে ২ রকমের ভিউ। তাই onCreateViewHolder() মেথডে আমরা view type চেক করেছি। সেই অনুযায়ী সংশ্লিষ্ট view holder class এর অবজেক্ট রিটার্ন করবে।

একই ভাবে onBindViewHolder() মেথডে ভিউ টাইপ চেক করে সে অনুযায়ী ভিউতে ডেটা populate করা হয়েছে। একই সাথে এখান থেকেই click event handle করা হয়েছে।

সবশেষে নতুন একটা মেথড দেখা যাচ্ছে getItemViewType(). এর কাজ কী? এর কাজ হচ্ছে প্রতিটা ভিউয়ের টাইপ কী সেটা রিটার্ন করা। এই মেথডের থেকে রিটার্ন করা ভ্যালুই মূলত আমরা উপরের দুইটা মেথডে পেয়েছি। getItemViewType() মেথডে আমরা চেক করছি পোস্টের ইমেজ url empty কিনা। যদি empty হয় তাহলে POST_TYPE_TEXT রিটার্ন করা হয়েছে অন্যথায় রিটার্ন হচ্ছে POST_TYPE_IMAGE. এই ভ্যারিয়েবল দুটি adapter class এর শুরুতেই declare করে রাখা হয়েছে। কোন লজিক ব্যবহার করে আপনি আপনার ভিউগুলোকে uniquely identify করবেন সেই লজিক এই override করা মেথডের ভিতর লিখতে হবে।

আশা করি বিষয়টা বুঝে ফেলেছেন। ভিন্ন রকমের ভিউ আরেকটা পারপাসে ইউজ করা যেতে পারে। যদি লিস্টে header বা footer থাকে তাহলে সেটাকে আলাদা ভিউ হিসাবে লিস্টে দেখাতে পারি। পুরো প্রোজেক্টের সোর্সকোড একত্রে পাওয়া যাবে আমার গিটহাব রিপোজিটরিতে। ব্লগ সম্পর্কে আপনার যে কোনো পরামর্শ বা গঠনমূলক সমালোচনা একান্ত কাম্য।

6 thoughts on “Android অ্যাপে একটা RecyclerView তে একাধিক টাইপের View দেখানো

  1. Thanks for this good explanation. It will be better if you add problem preview and solution preview in your post.

Leave a Reply

Your email address will not be published. Required fields are marked *