Search code examples
androidandroid-recyclerviewandroid-roommultiple-viewsandroid-paging-library

RecyclerView with multiple view types and paging library with boundary callback


I have a project in which I want to show multiple Items ( Image, Text, Video etc.) to the user. I am aware of how multiple view types work for the RecyclerView. In my project, I have included most of the recommended Android Architecture Components like Room, Paging, LiveData, etc.

To summarize the project design, I followed this codelab which helped me a lot. The Room library + Paging library with a BoundaryCallback provides a good a way to implement an offline cache. Considering the database as source of truth and letting the Paging library request more data whenever the Recyclerview needs it via a DataSource.Factory, seemed to me a very good approach.

But the downside of that project is that they show how the whole architecture components stuff (Room and Paging with BoundaryCallback) works for only one item type. But I have multiple view types and I could not handle that.

In the following I show you code snippets from my project to illustrate where I was stucked.

Let's start with the models. Suppose, we have two item types: Image and Text.

sealed class Item {

    @JsonClass(generateAdapter = true)
    @Entity(tableName = "image_table")
    data class Image(
        @PrimaryKey
        @Json(name = "id")
        val imageId: Long,
        val image: String
    ): Item()


    @JsonClass(generateAdapter = true)
    @Entity(tableName = "text_table")
    data class Text(
        @PrimaryKey
        @Json(name = "id")
        val textId: Long,
        val text:String
    ):Item()
}

As you can see, my model classes are extending the Item sealed class. So, I can treat the Image and Text class as Items. The adapter class looks then like this:

private val ITEM_VIEW_TYPE_IMAGE = 0
private val ITEM_VIEW_TYPE_TEXT = 1

class ItemAdapter():
        PagedListAdapter<Item, RecyclerView.ViewHolder>(ItemDiffCallback()) {

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {
            is TextItemViewHolder -> {
                val textItem = getItem(position) as Item.Text
                holder.bind(textItem)
            }
            is ImageItemViewHolder -> {
                val imageItem = getItem(position) as Item.Image
                holder.bind(imageItem)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            ITEM_VIEW_TYPE_IMAGE -> ImageItemViewHolder.from(parent)
            ITEM_VIEW_TYPE_TEXT -> TextItemViewHolder.from(parent)
            else -> throw ClassCastException("Unknown viewType ${viewType}")
        }
    }

    override fun getItemViewType(position: Int): Int {
        return when (getItem(position)) {
            is Item.Image -> ITEM_VIEW_TYPE_IMAGE
            is Item.Text -> ITEM_VIEW_TYPE_TEXT
        }
    }

    class TextItemViewHolder private constructor(val binding: ListItemTextBinding): RecyclerView.ViewHolder(binding.root) {

        fun bind(item: Text) {
            binding.text = item
            binding.executePendingBindings()
        }
        companion object {
            fun from(parent: ViewGroup): TextItemViewHolder {
                val layoutInflater = LayoutInflater.from(parent.context)
                val binding = ListItemTextBinding.inflate(layoutInflater, parent, false)
                return TextItemViewHolder(binding)
            }
        }
    }


    class ImageItemViewHolder private constructor(val binding: ListItemImageBinding) : RecyclerView.ViewHolder(binding.root){

        fun bind(item: Image) {
            binding.image = item
            binding.executePendingBindings()
        }

        companion object {
            fun from(parent: ViewGroup): ImageItemViewHolder {
                val layoutInflater = LayoutInflater.from(parent.context)
                val binding = ListItemImageBinding.inflate(layoutInflater, parent, false)
                return ImageItemViewHolder(binding)
            }
        }
    }
}

class ItemDiffCallback : DiffUtil.ItemCallback<Item>() {

    // HERE, I have the problem that I can not access the id attribute
    override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
        return oldItem == newItem
    }
}

}

Here, the problem I am faced with is the ItemDiffCallback class. In the areItemsTheSame method I can not access the id attribute using the superclass Item. What can I do here?

Now, I am going to the repository class. As you might know, the repository class is responsible for retrieving the data first from the database since in my project the database is the source of truth. If no data is there or if more data is needed, the Paging library uses a BoundaryCallback class to request more data from the service and store them into the database. Here is my repository class for the Items:

class ItemRepository(private val database: MyLimDatabase, private val service: ApiService) {

    fun getItems(): LiveData<PagedList<Item>> {
        val dataSourceFactory = ???????????? FROM WHICH TABLE I HAVE TO CHOOSE ???????? database.textDao.getTexts() or database.imageDao.getImages()
        val config = PagedList.Config.Builder()
            .setPageSize(30)            // defines the number of items loaded at once from the DataSource
            .setInitialLoadSizeHint(50) // defines how many items to load when first load occurs
            .setPrefetchDistance(10)    // defines how far from the edge of loaded content an access must be to trigger further loading
            .build()

        val itemBoundaryCallback = ItemBoundaryCallback(service, database)

        return LivePagedListBuilder(dataSourceFactory, config)
            .setBoundaryCallback(itemBoundaryCallback)
            .build()
    }
}

In this case, I have the problem that I do not know how to initialize the dataSourceFactory variable because I have two DAO classes. These are:

@Dao
interface ImageDao{
    @Query("SELECT * from image_table")
    fun getImages(): DataSource.Factory<Int, Image>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertAll(images: List<Image>)
}

@Dao
interface TextDao{
    @Query("SELECT * from text_table")
    fun getTexts(): DataSource.Factory<Int, Text>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertAll(texts: List<Text>)
}

So, how to handle this ?

Can someone help ?


Solution

  • Why do you need 2 tables? The columns are the same in both. Use a common data class for both types that holds another field to differentiate what that rows text value represents, text, image uri, image path. This could easily be done using an IntDef or enum. This then allows you to return one set of data that can be handled accordingly based on that new column.

    For example :

    @IntDef(IMAGE, TEXT)
    annotation class ItemType {
        companion object {
            const val IMAGE = 1
            const val TEXT = 2
        }
    }
    
    @Entity(tableName = "items_table")
    data class Item(
        @PrimaryKey
        val id: Long,
        val itemData: String,
        @ItemType
        val type : Int)
    
    @Dao
    interface ItemsDao {
        @Query("SELECT * from items_table")
        fun getItems(): DataSource.Factory<Int, Item>
    
        @Insert(onConflict = OnConflictStrategy.REPLACE)
        fun insertAll(itemss: List<Item>)
    }
    
    class ItemAdapter():
        PagedListAdapter<Item, RecyclerView.ViewHolder>(ItemDiffCallback()) {
    
        override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
            with(getItem(position)) {
                when (holder) {
                     // this could be simplified if using a common super view holder with a bind method that these subtypes inherit from
                     is TextItemViewHolder -> holder.bind(this)
                     is ImageItemViewHolder -> holder.bind(this)
                }
            }
        }
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
            return when (viewType) {
                ITEM_VIEW_TYPE_IMAGE -> ImageItemViewHolder.from(parent)
                ITEM_VIEW_TYPE_TEXT -> TextItemViewHolder.from(parent)
                else -> throw ClassCastException("Unknown viewType ${viewType}")
            }
        }
    
        override fun getItemViewType(position: Int): Int {
            return when (getItem(position)?.type) {
                IMAGE -> ITEM_VIEW_TYPE_IMAGE
                TEXT -> ITEM_VIEW_TYPE_TEXT
                else -> -1
            }
        }
    }
    

    With a single table the problems you are currently facing become non existent. If you are pulling information from a remote data source / api you can easily convert the api data types into the correct data type for your database before inserting, which I'd recommend anyway. Without knowing the specifics of the boundary check / service this would seem a better approach based on current code and information provided.