After a lot of searching, I know its possible with regular adapter, but I have no idea how to do it using Paging Library. I don`t need code just a clue.
Example
You can achieve the same result using insertSeparators
in Paging 3 library.
Make sure your items are sorted
by date.
Inside or viewmodel
retrieve a Pager
something like that
private val communicationResult: Flow<PagingData<CommunicationHistoryItem>> = Pager(
PagingConfig(
pageSize = 50,
enablePlaceholders = false,
maxSize = 400,
initialLoadSize = 50
)
) {
CommunicationPagingSource(repository)
}.flow.cachedIn(viewModelScope)
After all insert separators
like a header
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
is required for grouping
by dd.MM.yyyy
ignoring time
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
}
To add separators, you essentially have 2 options:
For the paging library only option 2 is viable since it only partially loads the data and inserting the separators becomes much more complicated. You will simply need to figure out a way to check if item x
is a different day than item x-1
and show/hide the date section in the view depending on the result.
I was in the same spot as you and I came up with this solution.
One important note though, in order to implement this I had to change my date converter to the database, from long to string to store a timestamp
these are my converters
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)
}
}
Some starting info though, I have a list of report headers that I wish to show , and page through and be able to filter
They are represented by this object:
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)
I also wanted to add separators between the items in the list to show them by date
I achieved this by doing the following:
I created a new query in room like this
@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>
This basically creates a separator line for all the items I have filtered through
it also creates a dummy date for sorting (with the time of 25:60:60 so that it will always appear in front of the other reports)
I then combine this with my list using union and sort them by the dummy date
The reason I had to change from long to string is because it is much easier to create dummy dates with string in sql and seperate the date part from the whole date time
The above creates a list like this:
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
In my PagedListAdapter I changed it to be an implementation of PagedListAdapter<ReportHeader, RecyclerView.ViewHolder>
(not a specific viewholder)
Added to the companion object:
companion object {
private val EMPTY_ID = UUID(0L,0L)
private const val LABEL = 0
private const val HEADER = 1
}
and overrode get view type like so:
override fun getItemViewType(position: Int): Int = if (getItem(position)?.id ?: EMPTY_ID == EMPTY_ID) LABEL else HEADER
I then created two seperate view holders :
class ReportHeaderViewHolder(val binding: ListItemReportBinding) : RecyclerView.ViewHolder(binding.root)
class ReportLabelViewHolder(val binding: ListItemReportLabelBinding) : RecyclerView.ViewHolder(binding.root)
and implemented the other overriden methods like so:
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()
}
}
}
}
I hope this helps and inspires people to find even better solutions
Kiskae's answer is excellent and for your case option 2 probably works well.
In my case I wanted to have one additional item that wasn't in the database, like this:
It needed to be clickable as well. There's the usual way of overriding getItemCount
to return +1 and offsetting positions for the other methods.
But I stumbled on another way that I haven't seen documented yet that might be useful for some cases. You might be able to incorporate additional elements into your query using union
:
@Query("select '' as name, 0 as id " +
"union " +
"select name, id from user " +
"order by 1 asc")
DataSource.Factory<Integer, User> getAllDataSource();
That means the data source actually returns another item in the beginning, and there's no need to adjust positions. In your adapter, you can check for that item and handle it differently.
In your case the query would have to be different but I think it would be possible.
As mentioned here, Paging Library works with PagedListAdapter. And PagedListAdapter extends from RecyclerView.Adapter, so you can do this simply like in Recyclerview (example). Just in two words - you need to use different view types for your date headers and content items.
When binding the data pass in the previous item as well
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)
}
Every view then sets a header, which is only made visible if the previous item doesn't have the same header.
val previousHeader = previousItem?.name?.capitalize().first()
val header = item?.name?.capitalize()?.first()
view.cachedContactHeader.text = header
view.cachedContactHeader.isVisible = previousHeader != header