После долгих поисков я знаю, что это возможно с обычным адаптером, но я понятия не имею, как это сделать с помощью библиотеки подкачки. Мне не нужен код, просто подсказка.
Пример
Чтобы добавить разделители, у вас есть 2 варианта:
Для библиотеки подкачки возможен только вариант 2, поскольку он загружает данные лишь частично, и вставка разделителей становится намного сложнее. Вам просто нужно будет найти способ проверить, является ли элемент x
другим днем, чем элемент x-1
, и показать / скрыть раздел даты в представлении в зависимости от результата.
Отличный ответ, и если мы примем во внимание разделение проблем, вариант 2 - единственный выход. DataSource не должен знать, что View хочет отображать заголовки, он должен только получать данные.
По варианту 2 есть несколько вопросов: - Что делать, если элемент, имеющий «заголовок», удален из БД? - Что делать, если новый элемент добавлен в середину списка? - Что делать, если в элементе, содержащем «заголовок», изменилось поле даты?
При связывании данных передается также и предыдущий элемент.
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
val previousItem = if (position == 0) null else getItem(position - 1)
holder.bind(item, previousItem)
}
Затем каждое представление устанавливает заголовок, который становится видимым только в том случае, если предыдущий элемент не имеет такого же заголовка.
val previousHeader = previousItem?.name?.capitalize().first()
val header = item?.name?.capitalize()?.first()
view.cachedContactHeader.text = header
view.cachedContactHeader.isVisible = previousHeader != header
Ответ Кискаэ отлично подходит, и для вашего случая вариант 2, вероятно, работает хорошо.
В моем случае я хотел иметь еще один элемент, которого не было в базе данных, например:
Он также должен был быть интерактивным. Есть обычный способ переопределения getItemCount
для возврата +1 и смещения позиций для других методов.
Но я наткнулся на другой способ, который еще не был задокументирован, который может быть полезен в некоторых случаях. Вы можете включить в свой запрос дополнительные элементы, используя union
:
@Query("select '' as name, 0 as id " +
"union " +
"select name, id from user " +
"order by 1 asc")
DataSource.Factory<Integer, User> getAllDataSource();
Это означает, что источник данных фактически возвращает другой элемент в начале, и нет необходимости корректировать позиции. В своем адаптере вы можете проверить этот элемент и обработать его по-другому.
В вашем случае запрос должен быть другим, но я думаю, что это возможно.
Решение должно быть таким же, как сказал @Cruces.
Я был в том же месте, что и вы, и я придумал это решение.
Однако одно важное замечание: чтобы реализовать это, мне пришлось изменить преобразователь даты в базу данных, с длинной строки на строку, чтобы сохранить метку времени.
это мои конвертеры
class DateConverter {
companion object {
@JvmStatic
val formatter = SimpleDateFormat("yyyyMMddHHmmss", Locale.ENGLISH)
@TypeConverter
@JvmStatic
fun toDate(text: String): Date = formatter.parse(text)
@TypeConverter
@JvmStatic
fun toText(date: Date): String = formatter.format(date)
}
}
Тем не менее, некоторая начальная информация: у меня есть список заголовков отчетов, которые я хочу показать, пролистывать страницы и иметь возможность фильтровать
Они представлены этим объектом:
data class ReportHeaderEntity(
@ColumnInfo(name = "id") override val id: UUID
, @ColumnInfo(name = "name") override val name: String
, @ColumnInfo(name = "description") override val description: String
, @ColumnInfo(name = "created") override val date: Date)
Я также хотел добавить разделители между элементами в списке, чтобы отображать их по дате
Я добился этого, выполнив следующие действия:
Я создал новый запрос в такой комнате
@Query(
"SELECT id, name, description,created " +
"FROM (SELECT id, name, description, created, created AS sort " +
" FROM reports " +
" WHERE :filter = '' " +
" OR name LIKE '%' || :filter || '%' " +
" OR description LIKE '%' || :filter || '%' " +
" UNION " +
" SELECT '00000000-0000-0000-0000-000000000000' as id, Substr(created, 0, 9) as name, '' as description, Substr(created, 0, 9) || '000000' AS created, Substr(created, 0, 9) || '256060' AS sort " +
" FROM reports " +
" WHERE :filter = '' " +
" OR name LIKE '%' || :filter || '%' " +
" OR description LIKE '%' || :filter || '%' " +
" GROUP BY Substr(created, 0, 9)) " +
"ORDER BY sort DESC ")
fun loadReportHeaders(filter: String = ""): DataSource.Factory<Int, ReportHeaderEntity>
Это в основном создает разделительную линию для всех элементов, которые я отфильтровал.
он также создает фиктивную дату для сортировки (со временем 25:60:60, чтобы она всегда отображалась перед другими отчетами)
Затем я объединяю это со своим списком, используя объединение, и сортирую их по фиктивной дате
Причина, по которой мне пришлось перейти с long на строку, заключается в том, что намного проще создавать фиктивные даты со строкой в sql и отделять часть даты от всего времени даты
Вышеупомянутый список создает такой список:
00000000-0000-0000-0000-000000000000 20190522 20190522000000
e3b8fbe5-b8ce-4353-b85d-8a1160f51bac name 16769 description 93396 20190522141926
6779fbea-f840-4859-a9a1-b34b7e6520be name 86082 description 21138 20190522141925
00000000-0000-0000-0000-000000000000 20190521 20190521000000
6efa201f-d618-4819-bae1-5a0e907ddcfb name 9702 description 84139 20190521103247
В моем PagedListAdapter я изменил его на реализацию PagedListAdapter<ReportHeader, RecyclerView.ViewHolder>
(а не на конкретного зрителя)
Добавлено в сопутствующий объект:
companion object {
private val EMPTY_ID = UUID(0L,0L)
private const val LABEL = 0
private const val HEADER = 1
}
и переопределить тип просмотра следующим образом:
override fun getItemViewType(position: Int): Int = if (getItem(position)?.id ?: EMPTY_ID == EMPTY_ID) LABEL else HEADER
Затем я создал два отдельных держателя представления:
class ReportHeaderViewHolder(val binding: ListItemReportBinding) : RecyclerView.ViewHolder(binding.root)
class ReportLabelViewHolder(val binding: ListItemReportLabelBinding) : RecyclerView.ViewHolder(binding.root)
и реализовал другие методы переопределения, например:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
HEADER -> ReportHeaderViewHolder(DataBindingUtil.inflate(inflater, R.layout.list_item_report, parent, false))
else -> ReportLabelViewHolder(DataBindingUtil.inflate(inflater, R.layout.list_item_report_label, parent, false))
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val reportItem = getItem(position)
when (getItemViewType(position)) {
HEADER -> {
(holder as ReportHeaderViewHolder).binding.apply {
report = reportItem
executePendingBindings()
}
}
LABEL -> {
(holder as ReportLabelViewHolder).binding.apply {
date = reportItem?.name
executePendingBindings()
}
}
}
}
Надеюсь, это поможет и вдохновит людей на поиск еще лучших решений.
Спасибо за отличную работу! Но ведь в GROUP BY должно быть какое-нибудь имя столбца, не так ли?
group by - это в основном тип разделителя, который вы хотите добавить, у меня была дата, поэтому я сгруппировал по дате, если вы хотите сделать что-то вроде телефонного каталога, вы можете группировать по substr (upper (name) 0,1)
Я не понимаю. Согласно документации, это работает следующим образом: GROUP BY [column_name]. Substr возвращает только строку, не связанную с именами столбцов. Или я что-то упустил?
о, извините, я не понял ваш вопрос, да, группа by, похоже, тоже работает с substr и другими функциями, в основном она оценивает функцию для каждой строки, а затем группирует их вместе в соответствии с этим значением, если вы просто добавили имя столбца, это будет выполнять группу в соответствии со значением этого столбца
Я думаю, что Group By должна избегать дублирования HEADER.
да, конечно, если вы не группируете по, тогда вы получаете один заголовок для каждой строки в вашей базе данных, нам нужен один заголовок для каждой группы строк с одинаковой датой
Спасибо за ответ, очень полезно! Я застрял с той же проблемой, и теперь я рассматриваю возможность использования опубликованного вами подхода или использования версии v3 (альфа) библиотеки подкачки, где они добавили API insertSeparators ().
библиотека подкачки в настоящее время лучше, я бы порекомендовал (если у вас есть время изучить ее) использовать ее, это то, что я использую с этого момента
Вы можете добиться того же результата, используя insertSeparators
в библиотеке Пейджинг 3.
Убедитесь, что ваши товары имеют sorted
по дате.
Внутри или viewmodel
достаньте Pager
что-нибудь в этом роде
private val communicationResult: Flow<PagingData<CommunicationHistoryItem>> = Pager(
PagingConfig(
pageSize = 50,
enablePlaceholders = false,
maxSize = 400,
initialLoadSize = 50
)
) {
CommunicationPagingSource(repository)
}.flow.cachedIn(viewModelScope)
Ведь insert separators
вроде заголовок
val groupedCommunicationResult = communicationResult
.map { pagingData -> pagingData.map { CommunicationHistoryModel.Body(it) } }
.map {
it.insertSeparators{ after, before ->
if (before == null) {
//the end of the list
return@insertSeparators null
}
val afterDateStr = after?.createdDate
val beforeDateStr = before.createdDate
if (afterDateStr == null || beforeDateStr == null)
return@insertSeparators null
val afterDate = DateUtil.parseAsCalendar(afterDateStr)?.cleanTime()?.time ?: 0
val beforeDate = DateUtil.parseAsCalendar(beforeDateStr)?.cleanTime()?.time ?: 0
if (afterDate > beforeDate) {
CommunicationHistoryModel.Header( DateUtil.format(Date(beforeDate))) // dd.MM.yyyy
} else {
// no separator
null
}
}
}
cleanTime
требуется для grouping
, так как dd.MM.yyyy
игнорирует время
fun Calendar.cleanTime(): Date {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
return this.time
}
он будет работать только для разделителей в середине элементов списка, если вы хотите, чтобы разделитель тоже был вверху, добавьте его if (after == null) {CommunicationHistoryModel.Header (DateUtil.format (Date (beforeDate)))}
Если мне нужно использовать вариант 1, есть идеи?