RecyclerView с несколькими типами представлений и библиотекой подкачки с граничным обратным вызовом

У меня есть проект, в котором я хочу показать пользователю несколько элементов (изображение, текст, видео и т. Д.). Я знаю, как работают несколько типов представлений для RecyclerView. В свой проект я включил большинство рекомендованных Android Architecture Components, таких как Room, Paging, LiveData и т. Д.

Обобщая дизайн проекта, я следовал этой codelab, которая мне помогла много. Библиотека Room + библиотека Paging с BoundaryCallback предоставляет хороший способ реализовать автономный кеш. Считать базу данных источником истины и позволять библиотеке Paging запрашивать дополнительные данные всякий раз, когда Recyclerview это необходимо через DataSource.Factory, мне показалось очень хорошим подходом.

Но оборотной стороной этого проекта является то, что они показывают, как весь компоненты архитектуры (Room и Paging с BoundaryCallback) работают только для одного типа элементов. Но у меня несколько типов представлений, и я не мог с этим справиться.

Далее я покажу вам фрагменты кода из своего проекта, чтобы проиллюстрировать, в чем я застрял.

Начнем с моделей. Предположим, у нас есть два типа элементов: изображение и текст.

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()
}

Как видите, мои классы моделей расширяют Item класс sealed. Итак, я могу рассматривать классы Image и Text как Items. Тогда класс адаптера выглядит так:

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
    }
}

}

Здесь я столкнулся с проблемой класса ItemDiffCallback. В методе areItemsTheSame я не могу получить доступ к атрибуту id с помощью суперкласса Item. Что я могу здесь сделать?

Теперь я перейду к классу репозитория. Как вы, возможно, знаете, класс репозитория отвечает за извлечение данных в первую очередь из базы данных, поскольку в моем проекте база данных является источником истины. Если данных нет или требуется больше данных, библиотека Paging использует класс BoundaryCallback для запроса дополнительных данных из службы и сохранения их в базе данных. Вот мой класс репозитория для 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()
    }
}

В этом случае у меня проблема в том, что я не знаю, как инициализировать переменную dataSourceFactory, потому что у меня есть два DAO класса. Эти:

@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>)
}

Итак, как с этим справиться?

Может кто поможет?




Ответы (1)


Зачем вам 2 стола? Столбцы в обоих одинаковые. Используйте общий класс данных для обоих типов, который содержит другое поле, чтобы различать, что представляет собой текстовое значение этой строки: текст, uri изображения, путь к изображению. Это легко сделать с помощью IntDef или enum. Затем это позволяет вам вернуть один набор данных, который можно обрабатывать соответствующим образом на основе этого нового столбца.

Например :

@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
        }
    }
}

С одной таблицей проблемы, с которыми вы сейчас сталкиваетесь, перестают существовать. Если вы извлекаете информацию из удаленного источника данных / api, вы можете легко преобразовать типы данных api в правильный тип данных для своей базы данных перед вставкой, что я бы порекомендовал в любом случае. Без знания специфики пограничной проверки / обслуживания это могло бы показаться лучшим подходом, основанным на текущем коде и предоставленной информации.

person Mark Keen    schedule 01.01.2020
comment
Действительно, для моего варианта использования достаточно одной таблицы. Спасибо за подсказку / решение. - person aminakoy; 02.01.2020