Android: Paging3: Duplicates items - android

Problem:
I get 40 items at the beginning of the list, then it starts to count from 11, and after this, everything is good. So, 1...40,11,12,13,...,300.
And when I scroll a lot to the bottom and then scroll up to see first items, the items have been changed to 1,2,...,10,1,2,...,10,1,2,...,10,11,12,...,300.
But, when I pass false to enablePlaceholders in the PagingConfig, when I scroll to the bottom, I see the issue as I said above(1,2,..,40,11,...,300) and suddenly the 40 items vanish and I only see 1,2,...,10 + 11,12,...,300(the correct way); And it doesn't change or get worse again.
ProductsPagingSource:
#Singleton
class ProductsPagingSource #Inject constructor(
private val productsApi: ProductsApi
//private val query: String
) : RxPagingSource<Int, RecyclerItem>() {
override fun loadSingle(params: LoadParams<Int>): Single<LoadResult<Int, RecyclerItem>> {
val position = params.key ?: STARTING_PAGE_INDEX
//val apiQuery = query
return productsApi.getBeersList(position, params.loadSize)
.subscribeOn(Schedulers.io())
.map { listBeerResponse ->
listBeerResponse.map { beerResponse ->
beerResponse.toDomain()
}
}
.map { toLoadResult(it, position) }
.onErrorReturn { LoadResult.Error(it) }
}
private fun toLoadResult(
#NonNull response: List<RecyclerItem>,
position: Int
): LoadResult<Int, RecyclerItem> {
return LoadResult.Page(
data = response,
prevKey = if (position == STARTING_PAGE_INDEX) null else position - 1,
nextKey = if (response.isEmpty()) null else position + 1,
itemsBefore = LoadResult.Page.COUNT_UNDEFINED,
itemsAfter = LoadResult.Page.COUNT_UNDEFINED
)
}
}
ProductsListRepositoryImpl:
#Singleton
class ProductsListRepositoryImpl #Inject constructor(
private val pagingSource: ProductsPagingSource
) : ProductsListRepository {
override fun getBeers(ids: String): Flowable<PagingData<RecyclerItem>> = Pager(
config = PagingConfig(
pageSize = 10,
enablePlaceholders = true,
maxSize = 30,
prefetchDistance = 5,
initialLoadSize = 40
),
pagingSourceFactory = { pagingSource }
).flowable
}
ProductsListViewModel:
class ProductsListViewModel #ViewModelInject constructor(
private val getBeersUseCase: GetBeersUseCase
) : BaseViewModel() {
private val _ldProductsList: MutableLiveData<PagingData<RecyclerItem>> = MutableLiveData()
val ldProductsList: LiveData<PagingData<RecyclerItem>> = _ldProductsList
init {
loading(true)
getProducts("")
}
private fun getProducts(ids: String) {
loading(false)
getBeersUseCase(GetBeersParams(ids = ids))
.cachedIn(viewModelScope)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
_ldProductsList.value = it
}.addTo(compositeDisposable)
}
}
ProductsListFragment:
#AndroidEntryPoint
class ProductsListFragment : Fragment(R.layout.fragment_product_list) {
private val productsListViewModel: ProductsListViewModel by viewModels()
private val productsListAdapter: ProductsListAdapter by lazy {
ProductsListAdapter(::navigateToProductDetail)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecycler()
setupViewModel()
}
private fun setupRecycler() {
itemErrorContainer.gone()
productListRecyclerView.adapter = productsListAdapter
}
private fun setupViewModel() {
productsListViewModel.run {
observe(ldProductsList, ::addProductsList)
observe(ldLoading, ::loadingUI)
observe(ldFailure, ::handleFailure)
}
}
private fun addProductsList(productsList: PagingData<RecyclerItem>) {
loadingUI(false)
productListRecyclerView.visible()
productsListAdapter.submitData(lifecycle, productsList)
}
...
BASE_DIFF_CALLBACK:
val BASE_DIFF_CALLBACK = object : DiffUtil.ItemCallback<RecyclerItem>() {
override fun areItemsTheSame(oldItem: RecyclerItem, newItem: RecyclerItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: RecyclerItem, newItem: RecyclerItem): Boolean {
return oldItem == newItem
}
}
BasePagedListAdapter:
abstract class BasePagedListAdapter(
vararg types: Cell<RecyclerItem>,
private val onItemClick: (RecyclerItem, ImageView) -> Unit
) : PagingDataAdapter<RecyclerItem, RecyclerView.ViewHolder>(BASE_DIFF_CALLBACK) {
private val cellTypes: CellTypes<RecyclerItem> = CellTypes(*types)
override fun getItemViewType(position: Int): Int {
getItem(position).let {
return cellTypes.of(it).type()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return cellTypes.of(viewType).holder(parent)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
getItem(position).let {
cellTypes.of(it).bind(holder, it, onItemClick)
}
}
}

I had to set the same number for pageSize and initialLoadSize. Also, I had to set the enablePlaceholders to false.
config = PagingConfig(
pageSize = 10,
enablePlaceholders = false,
maxSize = 30,
prefetchDistance = 5,
initialLoadSize = 10
),
But still, I want to know if it's the normal way? If yes, I couldn't find anywhere point to this! If not, why's that? Why initialLoadSize can not have value more than the pageSize?!
As we can see, the default value for the initialLoadSize is:
internal const val DEFAULT_INITIAL_PAGE_MULTIPLIER = 3
val initialLoadSize: Int = pageSize * DEFAULT_INITIAL_PAGE_MULTIPLIER,

Your issue is most likely because at the repository layer your results page numbers are indexed with numbers derived from the total divided by your page size.
This page number scheme assumes that you will page through the items with pages of the same size. However, Paging wants to get an initial page that's three times bigger by default, and then each page should be the page size.
So, the indexes might be like 0 through totalElements/PageSize, with the number for page that includes a given position equaling itemsIndex/PageSize.
This part is especially relevant:
return LoadResult.Page(
data = response,
prevKey = if (position == STARTING_PAGE_INDEX) null else position - 1,
nextKey = if (response.isEmpty()) null else position + 1,
itemsBefore = LoadResult.Page.COUNT_UNDEFINED,
itemsAfter = LoadResult.Page.COUNT_UNDEFINED
)
Let's imagine a pagesize of 10, an initial load of 30, and 100 elements total. We start with page 0, and we request 30 items. Position is 0, and params.loadSize is 30.
productsApi.getBeersList(0, 30)
Then we return a page object with those 30 elements and a nextPage key of 1.
If we imagine all our objects as list, the first page would be the asterisks in this span: [***-------]
Let's get the next page:
productsApi.getBeersList(1, 10)
That returns elements that are this from our span: [-*--------]
And then you get: [--*-------]
So the 0th page is fine, but the 1th and 2nd page overlap with it. Then, pages 3 and onward contain new elements.
Because you're getting the keys for the next and previous pages by adding or subtracting to get the adjacent keys to the current page of the current length, the index can't scale with the page size. This isn't always easy without knowing what is inside the PagingConfig.
However you can make it work with dynamic page sizes if you can guarantee your initial load size is your regular page size times some integer and that you'll always get the requested number of items except for the last page, you could store an offset as your next/previous page keys, and turn that offset into the next page at load like:
/* Value for api pageNo */
val pageNo = (params.key/params.loadSize + STARTING_PAGE_INDEX) ?: STARTING_PAGE_INDEX
productsApi.getBeersList(pageNo, params.loadSize) // and so on
/* for offset keys in PageData */
nextKey = (pageNo + 1) * loadSize
prevKey = (pageNo - 1) * loadSize // Integer division rounds down so larger windows start where they should

Related

Horizontal Pager with Paging3 in Jetpack Compose

There are two screens in my app. The first screen shows the list of images on the device by using the Paging3 library in a vertical grid. Now, when the user clicks on an image, I am passing the click position to the second screen where I am using HorizontalPager from Accompanist to show the images in full screen. Both the screens share the same ViewModel to fetch images using Paging3.
The code to show the images in HorizontalPager is shown below.
val images: LazyPagingItems<MediaStoreImage> =
viewModel.getImages(initialLoadSize = args.currentImagePosition + 1, pageSize = 50)
.collectAsLazyPagingItems()
val pagerState = rememberPagerState(initialPage = currentImagePosition)
Box(modifier = modifier) {
HorizontalPager(
count = images.itemCount,
state = pagerState,
itemSpacing = 16.dp
) { page ->
ZoomableImage(
modifier = modifier,
imageUri = images[page]?.contentUri
)
}
}
Here, currentImagePosition is the index of the image clicked on the first screen. I am setting the initialLoadSize to currentImagePosition + 1 which makes sure that the clicked image to be shown is already fetched by the paging library.
When the second screen is opened, the clicked image is shown in full screen as expected. However, when the user swipes for the next image, instead of loading the next image, the image with the index 50 and so on gets loaded as the user swipes further.
I am not sure what am I missing here. Any help will be appreciated.
Edit: Added ViewModel, Repository & Paging code
ViewModel
fun getImages(initialLoadSize: Int = 50): Flow<PagingData<MediaStoreImage>> {
return Pager(
config = PagingConfig(
pageSize = 50,
initialLoadSize = initialLoadSize,
enablePlaceholders = true
)
) {
repository.getImagesPagingSource()
}.flow.cachedIn(viewModelScope)
}
Repository
fun getImagesPagingSource(): PagingSource<Int, MediaStoreImage> {
return ImagesDataSource { limit, offset ->
getSinglePageImages(
limit,
offset
)
}
}
private fun getSinglePageImages(limit: Int, offset: Int): List<MediaStoreImage> {
val images = ArrayList<MediaStoreImage>()
val cursor = getCursor(limit, offset)
cursor?.use {
val idColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
val dateModifiedColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_MODIFIED)
val displayNameColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
val sizeColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE)
while (it.moveToNext()) {
val id = it.getLong(idColumn)
val dateModified =
Date(TimeUnit.SECONDS.toMillis(it.getLong(dateModifiedColumn)))
val dateModifiedString = getFormattedDate(dateModified)
val displayName = it.getString(displayNameColumn)
val size = it.getLong(sizeColumn)
val sizeInMbKb = getFileSize(size)
val contentUri = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
id
)
images.add(
MediaStoreImage(
id,
displayName,
dateModifiedString,
contentUri,
sizeInMbKb
)
)
}
}
cursor?.close()
return images
}
private fun getCursor(limit: Int, offset: Int): Cursor? {
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATE_MODIFIED,
MediaStore.Images.Media.SIZE
)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val bundle = bundleOf(
ContentResolver.QUERY_ARG_SQL_SELECTION to "${MediaStore.Images.Media.RELATIVE_PATH} like ? ",
ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to arrayOf("%${context.getString(R.string.app_name)}%"),
ContentResolver.QUERY_ARG_OFFSET to offset,
ContentResolver.QUERY_ARG_LIMIT to limit,
ContentResolver.QUERY_ARG_SORT_COLUMNS to arrayOf(MediaStore.Images.Media.DATE_MODIFIED),
ContentResolver.QUERY_ARG_SORT_DIRECTION to ContentResolver.QUERY_SORT_DIRECTION_DESCENDING
)
context.contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
bundle,
null
)
} else {
context.contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
"${MediaStore.Images.Media.DATA} like ? ",
arrayOf("%${context.getString(R.string.app_name)}%"),
"${MediaStore.Images.Media.DATE_MODIFIED} DESC LIMIT $limit OFFSET $offset",
null
)
}
}
Paging Data Source
class ImagesDataSource(private val onFetch: (limit: Int, offset: Int) -> List<MediaStoreImage>) :
PagingSource<Int, MediaStoreImage>() {
override fun getRefreshKey(state: PagingState<Int, MediaStoreImage>): Int? {
return state.anchorPosition?.let {
state.closestPageToPosition(it)?.prevKey?.plus(1)
?: state.closestPageToPosition(it)?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MediaStoreImage> {
val pageNumber = params.key ?: 0
val pageSize = params.loadSize
val images = onFetch.invoke(pageSize, pageNumber * pageSize)
val prevKey = if (pageNumber > 0) pageNumber.minus(1) else null
val nextKey = if (images.isNotEmpty()) pageNumber.plus(1) else null
return LoadResult.Page(
data = images,
prevKey = prevKey,
nextKey = nextKey
)
}
}
Seems like a bug in the Paging3 library. Consider sharing the codebase with the issue tracker team to help with the resolution of the issue.
The issue is solved. Whenever the second screen was opened, the getImages() function of the shared ViewModel was called which created a new instance of Pager. This new instance of the Pager was different from the one used on the first screen. Referring to this answer on StackOverflow, I created the Pager in the init block of the ViewModel as shown below.
val images: Flow<PagingData<MediaStoreImage>>
init {
images = Pager(
config = PagingConfig(
pageSize = 50,
initialLoadSize = 50,
enablePlaceholders = true
)
) {
repository.getImagesPagingSource()
}.flow.cachedIn(viewModelScope)
}
On the second screen, I collected the paging items as shown below.
val lazyImages: LazyPagingItems<MediaStoreImage> = viewModel.images.collectAsLazyPagingItems()
Now, I didn't have to pass the initialLoadSize as the Pager created in the first screen was being reused which had already loaded the items.

Can't understand code of getRefreshKey in Android paging3 codelab

// GitHub page API is 1 based: https://developer.github.com/v3/#pagination
private const val GITHUB_STARTING_PAGE_INDEX = 1
class GithubPagingSource(
private val service: GithubService,
private val query: String
) : PagingSource<Int, Repo>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
val position = params.key ?: GITHUB_STARTING_PAGE_INDEX
val apiQuery = query + IN_QUALIFIER
return try {
val response = service.searchRepos(apiQuery, position, params.loadSize)
val repos = response.items
val nextKey = if (repos.isEmpty()) {
null
} else {
// initial load size = 3 * NETWORK_PAGE_SIZE
// ensure we're not requesting duplicating items, at the 2nd request
position + (params.loadSize / NETWORK_PAGE_SIZE)
}
LoadResult.Page(
data = repos,
prevKey = if (position == GITHUB_STARTING_PAGE_INDEX) null else position - 1,
nextKey = nextKey
)
} catch (exception: IOException) {
return LoadResult.Error(exception)
} catch (exception: HttpException) {
return LoadResult.Error(exception)
}
}
// The refresh key is used for subsequent refresh calls to PagingSource.load after the initial load
override fun getRefreshKey(state: PagingState<Int, Repo>): Int? {
// We need to get the previous key (or next key if previous is null) of the page
// that was closest to the most recently accessed index.
// Anchor position is the most recently accessed index
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
}
This is the code of getRefreshKey function of the paging3 codelab.
I thought that it's just okay to return state.anchorPosition. But why is this returning closestPagetToPosition's previous key plus 1??
This is the link of the paging 3 code lab.
Well, state.anchorPosition is index of the first item you see in this list, getRefreshKey should return the key to load on refresh.
In this example, key is index of page and each page typically contains many items, lets say 20. Now loading two pages would mean using keys 1 and 2 and having 40 items loaded.
In this situation, when you are looking at item on index 30, state.anchorPosition is 30 as well, but you definitely don't want your refresh key to be 30, you need it to be 2 because item on index 30 comes from page 2. This is what closestPageToPosition does.

Read ble values and update livedata/recyclerview list

I have an Android application, that reads Bluetooth values from a device. I present these values in a recyclerView. I would like to find a way to update this list and i feel abit lost since i tried diffrent solutions that i found on google.
I really dont know if it's my recyclerView thats the issue or my livedata Observer that is not getting triggerd. When i try to read the values again the list disappears, but i can see in my logs that there is a new list of values comming from the viewmodel
The app runs as i intended the first iteration.
I can provide more code if needed
Sharing some code for extra clarity
Here i read the values where i retrive a byteArray.
ReadLineNodeValues().iRObjectTemperature -> {
bleListViewModel.addBleToList(characteristic.value)
}
Which i send to my ViewModel here
class BleValueViewModel: ViewModel() {
fun addBleToList(bleValue: ByteArray) {
blueToothLEvalue.add(bleValue)
mutableLiveDataBluetooth.postValue(blueToothLEvalue)
}
fun getList(): MutableLiveData<ArrayList<ByteArray>> {
return mutableLiveDataBluetooth
}
Here i trying to retrive the list where i add it to my recyclverView
private fun showItems() {
val bleValueViewModel = ViewModelProvider(requireActivity()).get(BleValueViewModel::class.java)
bleValueViewModel.getList().observe(viewLifecycleOwner) {
if (it.size == 26) {
showlist(it)
}
}
}
RecyclerView
class LineNodeValueDataAdapter :
RecyclerView.Adapter<LineNodeBigViewHolder>() {
private val differCallback = object : DiffUtil.ItemCallback<LinenNodeValueData>() {
override fun areItemsTheSame(
oldItem: LinenNodeValueData,
newItem: LinenNodeValueData
): Boolean {
return oldItem.valueOne == newItem.valueOne && oldItem.valueTwo == newItem.valueTwo && oldItem.valueThree == newItem.valueThree && oldItem.valueFour == newItem.valueFour
}
override fun areContentsTheSame(
oldItem: LinenNodeValueData,
newItem: LinenNodeValueData
): Boolean {
return oldItem == newItem
}
}
val differ = AsyncListDiffer(this, differCallback)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LineNodeBigViewHolder {
val binding = CardviewListlayoutValueBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
return LineNodeBigViewHolder(binding)
}
override fun onBindViewHolder(holder: LineNodeBigViewHolder, position: Int) {
val place = differ.currentList[position]
holder.bind(place)
}
override fun getItemCount(): Int = differ.currentList.size
}
I convert the values since they all come as ByteArrays
fun showlist(listValues: MutableList<ByteArray>) {
//Systemnode
val systemNodeMCUTemp = String(listValues[0])
val systemNodeVoltages = String(listValues[1])
val systemNodeWeatherSensor = String(listValues[2])
val systemNodeOpenThreadCfg = String(listValues[3])
//Linenode
val currentLineNode = String(listValues[4])
val acceleroMeterX = ByteBuffer.wrap(listValues[5]).order(ByteOrder.LITTLE_ENDIAN).float
val acceleroMeterY = ByteBuffer.wrap(listValues[6]).order(ByteOrder.LITTLE_ENDIAN).float
val acceleroMeterZ = ByteBuffer.wrap(listValues[7]).order(ByteOrder.LITTLE_ENDIAN).float
val iRObjectTemperature =
ByteBuffer.wrap(listValues[8]).order(ByteOrder.LITTLE_ENDIAN).float
val contactSensorTemperature =
ByteBuffer.wrap(listValues[9]).order(ByteOrder.LITTLE_ENDIAN).float
val magneticField = ByteBuffer.wrap(listValues[10]).order(ByteOrder.LITTLE_ENDIAN).float
val internalVoltage1V8 =
ByteBuffer.wrap(listValues[11]).order(ByteOrder.LITTLE_ENDIAN).float
val internalVoltagevBusRail =
ByteBuffer.wrap(listValues[12]).order(ByteOrder.LITTLE_ENDIAN).float
val internalVoltageVDD =
ByteBuffer.wrap(listValues[13]).order(ByteOrder.LITTLE_ENDIAN).float
val internalVoltageVDDH =
ByteBuffer.wrap(listValues[14]).order(ByteOrder.LITTLE_ENDIAN).float
val powerHarvestingVoltageSol1 =
ByteBuffer.wrap(listValues[15]).order(ByteOrder.LITTLE_ENDIAN).float
val powerHarvestingVoltageSol2 =
ByteBuffer.wrap(listValues[16]).order(ByteOrder.LITTLE_ENDIAN).float
val powerHarvestingVoltageEmppt =
ByteBuffer.wrap(listValues[17]).order(ByteOrder.LITTLE_ENDIAN).float
val powerHarvestingVoltageHmppt =
ByteBuffer.wrap(listValues[18]).order(ByteOrder.LITTLE_ENDIAN).float
val eHFieldHField =
ByteBuffer.wrap(listValues[19]).order(ByteOrder.LITTLE_ENDIAN).float
val eHFieldEField =
ByteBuffer.wrap(listValues[20]).order(ByteOrder.LITTLE_ENDIAN).float
val ambientTemperaturesAcc =
ByteBuffer.wrap(listValues[21]).order(ByteOrder.LITTLE_ENDIAN).float
val ambientTemperaturesIR =
ByteBuffer.wrap(listValues[22]).order(ByteOrder.LITTLE_ENDIAN).float
val ambientTemperaturesMag =
ByteBuffer.wrap(listValues[23]).order(ByteOrder.LITTLE_ENDIAN).float
val ambientTemperaturesMCU =
ByteBuffer.wrap(listValues[24]).order(ByteOrder.LITTLE_ENDIAN).float
val openThreadCFG =
ByteBuffer.wrap(listValues[25]).order(ByteOrder.LITTLE_ENDIAN).float
val systemNodeList = ArrayList<SystemValue>()
systemNodeList.add(SystemValue("Systemode MguTemp", systemNodeMCUTemp))
systemNodeList.add(SystemValue("Systemnode Voltages", systemNodeVoltages))
systemNodeList.add(SystemValue("Systemnode WeatherSensor", systemNodeWeatherSensor))
systemNodeList.add(SystemValue("Systemnode OpenThreadCfg", systemNodeOpenThreadCfg))
val lineNodevaluelist = ArrayList<LinenNodeValueData>()
val lineNodeListBle = ArrayList<SystemValue>()
lineNodeListBle.add(SystemValue("LineNode Service", currentLineNode))
lineNodeListBle.add(SystemValue("Current LineNode", acceleroMeterX.toString()))
//lineNodeListBle.add(SystemValue("Acclero meter", lineNodeValueSeven.toString()))
Timber.i("Acclerometer2 :: ${acceleroMeterY}}")
lineNodeListBle.add(
SystemValue(
"Accelero Meter",
"X: $acceleroMeterX " + "Y: $acceleroMeterY"
+ " Z: $acceleroMeterZ"
)
)
lineNodeListBle.add(
SystemValue(
"iRObject Temperature",
iRObjectTemperature.toString()
)
)
lineNodeListBle.add(
SystemValue(
"ContactSensor Temperature",
contactSensorTemperature.toString()
)
)
lineNodeListBle.add(SystemValue("Magnetic Field", magneticField.toString()))
lineNodevaluelist.add(
LinenNodeValueData(
"Internal Voltage",
"1V8: $internalVoltage1V8",
"Vbus rail: $internalVoltagevBusRail",
"VDD: $internalVoltageVDD",
"VDDH: $internalVoltageVDDH"
)
)
lineNodevaluelist.add(
LinenNodeValueData(
"Power Harvesting Voltage",
"Sol1: $powerHarvestingVoltageSol1",
"Sol2: $powerHarvestingVoltageSol2",
"E-Mppt: $powerHarvestingVoltageEmppt", "H-Mppt: $powerHarvestingVoltageHmppt"
)
)
lineNodeListBle.add(
SystemValue(
"eHField",
"H-field: $eHFieldHField E-field: $eHFieldEField"
)
)
lineNodevaluelist.add(
LinenNodeValueData(
"Ambient Temperatures",
"Acc: $ambientTemperaturesAcc",
"IR: $ambientTemperaturesIR",
"Mag: $ambientTemperaturesMag",
"Mcu: $ambientTemperaturesMCU"
)
)
lineNodeListBle.add(SystemValue("openThreadCFG", openThreadCFG.toString()))
setupRecyclerViewSystemNode(binding.systemNodeRecyclerView, systemNodeList)
Timber.i("blueviewRecycler :: ${systemNodeList.size}")
setupRecylerViewLineNodeOne(binding.lineNodeRecyclerView, lineNodeListBle, lineNodevaluelist)
Timber.i("blueviewRecycler1 :: ${lineNodeListBle.size}+${lineNodevaluelist.size}")
}
Have a nice day everyone
Kind regards
Droid
Here you have a condition.
if (it.size == 26) {
showlist(it)
}
If you add new values to the list, list.size changed and condition is false. So, just remove a condition:
bleValueViewModel.getList().observe(viewLifecycleOwner) {
showlist(it)
}
also, i hope you call showItems only once.
Regarding your diff utils implementation, from the doc:
areContentsTheSame(int oldItemPosition, int newItemPosition)
Called by the DiffUtil when it wants to check whether two items have
the same data.
areItemsTheSame(int oldItemPosition, int newItemPosition)
Called by the DiffUtil to decide whether two object represent the same
Item.
Also it looks like you should reimplement your DIfUtils Callback:
private val differCallback = object : DiffUtil.ItemCallback<LinenNodeValueData>() {
override fun areItemsTheSame(
oldItem: LinenNodeValueData,
newItem: LinenNodeValueData
): Boolean {
return //return true if i's the same item. I suppose in your case you should check that it's the same ble characteristic.
}
override fun areContentsTheSame(
oldItem: LinenNodeValueData,
newItem: LinenNodeValueData
): Boolean {
return // return true if it has the same content. If you return true it means that content(values) for the object has not change. In other case if values are different, you should return false, it means that values for characteristic changed and recyclerView should update respective view.
}
}

In Paging3, after deleting the item, how to return the recyclerview to its original position?

I imitated the codelab and implemented the getRefreshKey() method, but since the params.loadSize is 3*PAGE_SIZE after refresh(that is, to delete an item or edit an item), most of the probability of my recyclerview will not return to the original position. What should I do?
This is my pagingSource:
class PasswordPagingSource2(
val type: String,
val service: PasswordService,
val PAGE_SIZE:Int) :
PagingSource<Int, Any>() {
private val PAGE_INDEX = 0
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Any> {
val page = params.key ?: PAGE_INDEX
return try {
val list =
service.getPasswords(
"{\"type\":\"$type\"}",
params.loadSize, page * PAGE_SIZE).items
val preKey = if (page > 0) page - 1 else null
val nextKey = if (list.isNotEmpty()) page + (params.loadSize/PAGE_SIZE)else null
return LoadResult.Page(data =list, prevKey = preKey, nextKey = nextKey)
} catch (exception: IOException) {
return LoadResult.Error(Throwable("IO Error"))
} catch (exception: HttpException) {
return LoadResult.Error(Throwable("Network Error"))
}
}
override fun getRefreshKey(state: PagingState<Int, Any>): Int? {
val page = state.anchorPosition?.let { anchorPosition->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?:state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
return page
}
}
It is your responsibility to ensure that initialLoadSize will load enough items to cover the viewport so that on invalidation, you will load enough items such that the previous list, and the initial refresh after invalidation overlap so that DiffUtil can help you resume scrolling position.
If you want to keep your pageSize the same, you can modify initialLoadSize in PagingConfig instead. As a rule of thumb, you want it to load at least 2x the number of visible items at once, both before and after the last PagingState.anchorPosition.

How to update RecyclerView positions after deleting?

I Have Recycler view with its adapter and holder. Each element has "delete" button, which has to update recyclerview. But after deleting first element, when I'm trying to delete others, from datasource are removing items with old indices of items, which I want to delete
Hope, I can explain it with an example:
Old data: (1 2 3 4 5 6 7) -> deleting "1" -> (2 3 4 5 6 7) -> deleting "2" -> (2 4 5 6 7) -> deleting "2" -> (2 5 6 7)
Here is ViewHolder source code:
inner class ViewHolder(binding: RecyclerItemBinding) : RecyclerView.ViewHolder(binding.root) {
val idView: TextView = binding.itemNumber
val contentView: Button = binding.deleteButton
init {
contentView.setOnClickListener {
removeAt(adapterPosition)
}
}
private fun removeAt(position: Int) {
values.removeAt(position)
notifyItemRemoved(position)
notifyItemRangeChanged(position, itemCount)
Log.d(TAG, "remove $position left $itemCount")
}
override fun toString(): String {
return super.toString() + " '" + contentView.text + "'"
}
}
Content code:
object PlaceholderContent {
/**
* An array of sample (placeholder) items.
*/
val ITEMS: MutableList<PlaceholderItem> = ArrayList()
/**
* A map of sample (placeholder) items, by ID.
*/
private val ITEM_MAP: MutableMap<Int, PlaceholderItem> = HashMap()
private val deletedNumbers : Queue<Int> = LinkedList()
private const val INITIAL_COUNT = 15
private var biggest: Int
init {
// Add some sample items.
for (i in 1..INITIAL_COUNT) {
addItem(createPlaceholderItem(i))
}
biggest = ITEMS.last().id
}
fun size() : Int = ITEMS.size
fun addNext() {
addItem(createPlaceholderItem(deletedNumbers.poll() ?: ++biggest))
Log.d(TAG, "add $biggest")
}
fun removeAt(position: Int) {
val toRemove = ITEMS.removeAt(position)
if (toRemove.id == biggest) {
biggest--
}
deletedNumbers.add(toRemove.content.toInt())
}
private fun addItem(item: PlaceholderItem) {
ITEMS.add(item)
ITEM_MAP[item.id] = item
}
private fun createPlaceholderItem(position: Int): PlaceholderItem {
return PlaceholderItem(size(), "$position")
}
/**
* A placeholder item representing a piece of content.
*/
data class PlaceholderItem(val id: Int, val content: String): Comparable<PlaceholderItem> {
override fun toString(): String = content
override fun compareTo(other: PlaceholderItem): Int = this.id.compareTo(other.id)
}
private const val TAG = "PLACEHOLDER_CONTENT"
}
To remove a specific positioned value, I just called the getAdapterPosition() to specify the item position to be removed. Simply try this in your onBindViewHolder with the arrayList that is using like Old data: (1 2 3 4 5 6 7).
override fun onBindViewHolder(holder: RecyclerViewAdapter.ViewHolder, position: Int) {
holder.buttonDelete2.setOnClickListener {
arrayList.removeAt(holder.adapterPosition)
notifyItemRemoved(holder.adapterPosition)
}
}
Add increment in adapterPosition by 1
adapterPosition + 1
Alternatively for performance-wise, you can use DiffUtil class which will help calculates the item position change.
you can check some tutorials here:
https://blog.mindorks.com/the-powerful-tool-diff-util-in-recyclerview-android-tutorial

Categories

Resources