I am having an issue with RecyclerView - in onBindViewHolder(), when I make a condition for each item in the list (going through the whole list, position by position), and want to change the item text value, it doesn't work. I assume that's because RecyclerView recycles items.
`
override fun onBindViewHolder(holder: TimeViewHolder, position: Int) {
val time = holder.itemView.findViewById<TextView>(R.id.timeTextView)
val item = list[position]
//items in list: "11aa", "11bb", "11cc"
if (item.value.substring(0, 2).toInt() == 11 && item.value.substring(2,4) == "bb") {
time.text = "true"
} else {
time.text="false"
}
// list should be shown as: "false", "true", "false"
if (item.value.substring(0,2).toInt() == 11 && item.value.substring(2,4) == "aa") {
time.text="true"
} else {
time.text="false"
}
//list should be shown as: "true", "false", "false"
`
Example: item is "11bb" -> show true, all other items should be false (including "11aa")
item is "11aa" -> show true, all other items are false (including "11bb")
I would greatly appreciate any suggestion!
I tried the else condition, so it is not only "if" without other possible outcome, still doesn't work
For every item in the list, you're doing two checks. First you check if item is "11bb" and display true or false. But then you throw that result out the window by checking if it's "11aa" instead, and display true or false based on that.
The end result is each item displays true if it's "11aa", otherwise it shows false.
Here's what you said you wanted in your question:
Example: item is "11bb" -> show true, all other items should be false (including "11aa")
item is "11aa" -> show true, all other items are false (including "11bb")
So it sounds like you want a single item to display true, and displaying that sets the others to false, right?
Those two rules you've provided contradict each other - what if you have both "11aa" and "11bb" in your data, like your example does?
//items in list: "11aa", "11bb", "11cc"
Which one should show true? The first one in the list, "11aa"? Or does a later one override the earlier one, so "11bb"? What's the rule for resolving this?
Here's a way you can do it, assuming you want the first one in the list.
First, you need a function to set your adapter's data. This is so you can check through it, and work out in advance which item needs to be set to true:
// inside your adapter
// your internal data - private so anything setting the data -has to- go through
// the setter function below.
private var items: List<String>
// index of the item that should be displayed as true, or null if there isn't one
// (I'm storing an index instead of the item in case there are duplicates in the list)
private val trueItemPosition: Int? = null
fun setData(newData: List<String>) {
// set the new data
items = newData
// store the index of the first item that matches your rules, or null if none do
val matchedIndex = items.indexOfFirst(::matchesRule)
trueItemPosition = if (matchedIndex == -1) null else matchedIndex
// always refresh after setting data!
notifyDataSetChanged()
}
// you don't need to do any of that Int conversion or substring stuff, just match the string!
private fun matchesRule(item: String) = when {
item.startsWith("11aa") -> true
item.startsWith("11bb") -> true
else -> false
}
Then in onBindViewHolder you can decide what to do depending on the current item's position and the value of trueItemPosition:
// in onBindViewHolder
when(trueItemPosition) {
null -> {
// display your item normally - there's no 11aa or 11bb in the list
}
position -> {
// this item is the matching one
time.text = "true"
}
else -> {
// there's a matching item, but this isn't it
time.text = "false"
}
}
And that's pretty much it! You can use indexOfLast if you want the last matching item to take precedence, and you could make separate functions for the different rule matchers if you want, say, a 11aa anywhere in the list to take precedence over any 11bbs. Just run them one after another on the whole data set in order of preference, until you get a matching index.
Like I said, this relies on you setting all your data through that setData function, so it can work things out before you display anything. That includes the initial data you provide - so if you're passing data in as a constructor parameter, that can't be a property and you need to call setData with that parameter in an init block:
class MyAdapter(
...
data: List<String> // not a val/var, we're just using this temporarily
... {
init {
// set the internal data through the setter
setData(data)
}
}
and that should do it!
edit If you need arbitrary matching indices (other than first or last) you'll need to code that logic - it's probably best to put it in a function:
fun <T> Iterable<T>.matchingIndexNumber(ordinal: Int, predicate: (T) -> Boolean) =
mapIndexedNotNull { i, item -> if (predicate(item)) i else null }
.getOrNull(ordinal - 1) ?: -1
Now you can call items.matchingIndexNumber(2, ::matchesRule) to get the 2nd match's index, etc.
By the way, I kinda glossed over this, but the reason I used indices was because they're guaranteed to be unique - if you know all your items will be unique objects (i.e. none of them return true for equals when compared to the other items) then you can drop the indices entirely, and just store the matching object:
// potentially store the important -item- itself
private val trueItem: String? = null
fun setData(newData: List<String>) {
...
// just set the first item that matches, or null if nothing matches
trueItem = items.first(::matchesRule)
...
}
override fun onBindViewHolder(holder: TimeViewHolder, position: Int) {
...
when(trueItem) {
null -> // no important item in list - display this normally
item -> // this item is the important one
else -> // there's an important item, this isn't it
}
...
}
Which is a whole lot simpler! The indices approach works no matter what, but if your items are all unique, this is the way I'd go. (If they're not, e.g. you have two "11aa" strings, they'll both match trueItem in that when block so they'll both show true or whatever)
Anyway, if you're working with the items themselves instead of having to juggle indices and return -1 for missing stuff, you can just do this:
// get the 3rd matching item
trueItem = items.filter(::matchesRule).getOrNull(3)
// or
trueItem = items.filter(::matchesRule).drop(2).firstOrNull()
// or if you want the 3rd item, but if there isn't one you want the 2nd, and so on
trueItem = items.filter(::matchesRule).take(3).lastOrNull()
There are way more options in the standard library for working with the collection of items directly, it's much more flexible, so I'd recommend it if you can!
(You could also do the take(count).lastOrNull() trick in that function I wrote if you do want/need to stick with the indices though)
Related
I have a screen where I display some items using pagination. Here is what I have tried:
fun ItemsScreen(
viewModel: ItemsViewModel = hiltViewModel()
) {
Box(
modifier = Modifier.fillMaxSize()
) {
val items = viewModel.getItems().collectAsLazyPagingItems()
Log.d(TAG, "itemCount is ${items.itemCount}")
}
}
Here is how I call it from the ViewModel class:
fun getItems() = repo.getItems()
And here is the repo:
override fun getItems() = Pager(
config = config
) {
AppPagingSource(
query = db.collection("items").limit(12)
)
}.flow
When I open the page, I get:
itemCount is 0
itemCount is 12
So, first time I get zero. When the data becomes available, I get 12. How can I stop collectAsLazyPagingItems() from firing when the itemCount is zero? I only want to fire when the data is received. How to solve this?
Edit:
Why do I need to stop collectAsLazyPagingItems() from firing?
Because the same code as above is used in a pagination. So, each time I type a character, a new request is performed, and until I get the page results, I get zero, and after 2 seconds I get 12.
What I want to achieve, is when I get no results because of a wrong search, I want to display a message, "No items found". But only then, not each time I load new data. With the code above, until I'm getting new results, that message is displayed, because items.itemCount == 0 for 2 seconds. After that, the results are correctly displayed in the grid. Here is the logic:
if (items.itemCount > 0) {
LazyVerticalGrid(...)
} else {
if (searchText.isNotEmpty()) {
Text("No items found")
}
}
Edit2:
fun getItems(searchText: String) = if (searchText.isEmpty()) {
repo.getItems()
} else {
repo.getSearchItems(searchText)
}.cachedIn(viewModelScope)
How can I stop collectAsLazyPagingItems() from firing when the itemCount is zero?
You can't. Following is a section from the source code:
When you call items.itemCount it gives you the size of itemSnapshotList which is initialized with an emptyList in the beginning. Think of the paging items as a state instead of an event which is fired. A state has always some value associated with it.
Although I believe that an empty list emission shouldn't cause any problem in general but if you really don't want to process that value, the only option is to ignore it by an if statement like this:
val items = viewModel.getItems().collectAsLazyPagingItems()
if(items.itemCount > 0) {
// UI goes here
}
// Some other UI
Or you can also return from the composable, if there's nothing to show in case of empty list.
val items = viewModel.getItems().collectAsLazyPagingItems()
if(items.itemCount == 0)
return
// UI goes here
Edit: You can use items.loadState.refresh to check if the data is being refreshed. The updated code will look like this:
when {
items.loadState.refresh is LoadState.Loading -> {
CircularProgressIndicator()
}
items.itemCount > 0 -> {
LazyVerticalGrid(...)
}
searchText.isNotEmpty() -> {
Text("No items found")
}
}
I have a problem with the state of individual elements in LazyColumn and LazyRow. If the first element is open and I want to delete it, then the second element becomes the first and also becomes open. I want it to work differently.
Screen
enter image description here
Fragment LazyColumn
items(zamList.size) { index ->
ExpandableCard()
}
Expandable Card
#Composable
fun ExpandableCard() {
//Expandable state
var expandedState by remember {
mutableStateOf(false)
}
Card(
onClick = {
expandedState = !expandedState
}
)
It's a bad idea to combine LazyLists with remember:
Try adding 20 items, opening 1, and scrolling, until the item is not visible anymore: the item will have closed.
The way i suggest to do it is:
Hold that state in a viewModel, e.g. a Map<YourItem, isOpen>
onOpen/onClose update your viewmodel-state.
Other than that, it's a good idea to provide ids if possible.
Also, you might try Modifier.animateContentSize() (which is only defined in a LazyScope(!), so your animations look better :)
You need to use unique keys with items. Using key will makes sure only the items that changed being recomposed and keeps order of items that are not changed based on their ids.
val myItems = listOf<MyItem>()
LazyColumn() {
items(
items = myItems,
key = {item: MyItem ->
// Some unique id here
item.hashCode()
}
) {
}
}
Hey i have nested list and i wanted find first occurrence index value.
data class ABC(
val key: Int,
val value: MutableList<XYZ?>
)
data class XYZ)
val isRead: Boolean? = null,
val id: String? = null
)
I added code which find XYZ object, but i need to find index. So how can i achieved in efficient way. How can i improve my code?
list?.flatMap { list ->
list.value
}?.firstOrNull { it?.isRead == false }
If you would like to stick to functional style then you can do it like this:
val result = list.asSequence()
.flatMapIndexed { outer, abc ->
abc.value.asSequence()
.mapIndexed { inner, xyz -> Triple(outer, inner, xyz) }
}
.find { it.third?.isRead == false }
if (result != null) {
val (outer, inner) = result
println("Outer: $outer, inner: $inner")
}
For each ABC item we remember its index as outer and we map/transform a list of its XYZ items into a list of tuples: (outer, inner, xyz). Then flatMap merges all such lists (we have one list per ABC item) into a single, flat list of (outer, inner, xyz).
In other words, the whole flatMapIndexed() block changes this (pseudo-code):
[ABC([xyz1, xyz2]), ABC([xyz3, xyz4, xyz5])]
Into this:
[
(0, 0, xyz1),
(0, 1, xyz2),
(1, 0, xyz3),
(1, 1, xyz4),
(1, 2, xyz5),
]
Then we use find() to search for a specific xyz item and we acquire outer and inner attached to it.
asSequence() in both places changes the way how it works internally. Sequences are lazy, meaning that they perform calculations only on demand and they try to work on a single item before going to another one. Without asSequence() we would first create a full list of all xyz items as in the example above. Then, if xyz2 would be the one we searched, that would mean we wasted time on processing xyz3, xyz4 and xyz5, because we are not interested in them.
With asSequence() we never really create this flat list, but rather perform all operations per-item. find() asks for next item to check, mapIndexed maps only a single item, flatMapIndexed also maps only this single item and if find() succeed, the rest of items are not processed.
In most cases using sequences here could greatly improve the performance. In some cases, like for example when lists are small, sequences may degrade the performance by adding an overhead. However, the difference is very small, so it is better to leave it as it is.
As we can see, functional style may be pretty complicated in cases like this. It may be a better idea to use imperative style and good old loops:
list.indicesOfFirstXyzOrNull { it?.isRead == false }
inline fun Iterable<ABC>.indicesOfFirstXyzOrNull(predicate: (XYZ?) -> Boolean): Pair<Int, Int>? {
forEachIndexed { outer, abc ->
abc.value.forEachIndexed { inner, xyz ->
if (predicate(xyz)) {
return outer to inner
}
}
}
return null
}
In Kotlin, you can use the indexOf() function that returns the index of the first occurrence of the given element, or -1 if the array does not contain the element.
Example:
fun findIndex(arr: Array<Int>, item: Int): Int {
return arr.indexOf(item)
}
Help me please.
The app is just for receiving list of plants from https://trefle.io and showing it in RecyclerView.
I am using Paging library 3.0 here.
Task: I want to add a header where total amount of plants will be displayed.
The problem: I just cannot find a way to pass the value of total items to header.
Data model:
data class PlantsResponseObject(
#SerializedName("data")
val data: List<PlantModel>?,
#SerializedName("meta")
val meta: Meta?
) {
data class Meta(
#SerializedName("total")
val total: Int? // 415648
)
}
data class PlantModel(
#SerializedName("author")
val author: String?,
#SerializedName("genus_id")
val genusId: Int?,
#SerializedName("id")
val id: Int?)
DataSource class:
class PlantsDataSource(
private val plantsApi: PlantsAPI,
private var filters: String? = null,
private var isVegetable: Boolean? = false
) : RxPagingSource<Int, PlantView>() {
override fun loadSingle(params: LoadParams<Int>): Single<LoadResult<Int, PlantView>> {
val nextPageNumber = params.key ?: 1
return plantsApi.getPlants( //API call for plants
nextPageNumber, //different filters, does not matter
filters,
isVegetable)
.subscribeOn(Schedulers.io())
.map<LoadResult<Int, PlantView>> {
val total = it.meta?.total ?: 0 // Here I have an access to the total count
//of items, but where to pass it?
LoadResult.Page(
data = it.data!! //Here I can pass only plant items data
.map { PlantView.PlantItemView(it) },
prevKey = null,
nextKey = nextPageNumber.plus(1)
)
}
.onErrorReturn{
LoadResult.Error(it)
}
}
override fun invalidate() {
super.invalidate()
}
}
LoadResult.Page accepts nothing but list of plant themselves. And all classes above DataSource(Repo, ViewModel, Activity) has no access to response object.
Question: How to pass total count of items to the list header?
I will appreciate any help.
You can change the PagingData type to Pair<PlantView,Int> (or any other structure) to add whatever information you need.
Then you will be able to send total with pages doing something similar to:
LoadResult.Page(
data = it.data.map { Pair(PlantView.PlantItemView(it), total) },
prevKey = null,
nextKey = nextPageNumber.plus(1)
)
And in your ModelView do whatever, for example map it again to PlantItemView, but using the second field to update your header.
It's true that it's not very elegant because you are sending it in all items, but it's better than other suggested solutions.
Faced the same dilemma when trying to use Paging for the first time and it does not provide a way to obtain count despite it doing a count for the purpose of the paging ( i.e. the Paging library first checks with a COUNT(*) to see if there are more or less items than the stipulated PagingConfig value(s) before conducting the rest of the query, it could perfectly return the total number of results it found ).
The only way at the moment to achieve this is to run two queries in parallel: one for your items ( as you already have ) and another just to count how many results it finds using the same query params as the previous one, but for COUNT(*) only.
There is no need to return the later as a PagingDataSource<LivedData<Integer>> since it would add a lot of boilerplate unnecessarily. Simply return it as a normal LivedData<Integer> so that it will always be updating itself whenever the list results change, otherwise it can run into the issue of the list size changing and that value not updating after the first time it loads if you return a plain Integer.
After you have both of them set then add them to your RecyclerView adapter using a ConcatAdapter with the order of the previously mentioned adapters in the same order you'd want them to be displayed in the list.
ex: If you want the count to show at the beginning/top of the list then set up the ConcatAdapter with the count adapter first and the list items adapter after.
One way is to use MutableLiveData and then observe it. For example
val countPlants = MutableLiveData<Int>(0)
override fun loadSingle(..... {
countPlants.postValue(it.meta?.total ?: 0)
}
Then somewhere where your recyclerview is.
pagingDataSource.countPlants.observe(viewLifecycleOwner) { count ->
//update your view with the count value
}
The withHeader functions in Paging just return a ConcatAdapter given a LoadStateHeader, which has some code to listen and update based on adapter's LoadState.
You should be able to do something very similar by implementing your own ItemCountAdapter, except instead of listening to LoadState changes, it listens to adapter.itemCount. You'll need to build a flow / listener to decide when to send updates, but you can simply map loadState changes to itemCount.
See here for LoadStateAdapter code, which you can basically copy, and change loadState to itemCount: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:paging/runtime/src/main/java/androidx/paging/LoadStateAdapter.kt?q=loadstateadapter
e.g.,
abstract class ItemCountAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
var itemCount: Int = 0
set(itemCount { ... }
open fun displayItemCountAsItem(itemCount: Int): Boolean {
return true
}
...
Then to actually create the ConcatAdapter, you want something similar to: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:paging/runtime/src/main/java/androidx/paging/PagingDataAdapter.kt;l=236?q=withLoadStateHeader&sq=
fun PagingDataAdapter.withItemCountHeader(itemCountAdapter): ConcatAdapter {
addLoadStateListener {
itemCountAdapter.itemCount = itemCount
}
return ConcatAdapter(itemCountAdapter, this)
}
Another solution, although also not very elegant, would be to add the total amount to your data model PlantView.
PlantView(…val totalAmount: Int…)
Then in your viewmodel you could add a header with the information of one item. Here is a little modified code taken from the official paging documenation
pager.flow.map { pagingData: PagingData<PlantView> ->
// Map outer stream, so you can perform transformations on
// each paging generation.
pagingData
.map { plantView ->
// Convert items in stream to UiModel.PlantView.
UiModel.PlantView(plantView)
}
.insertSeparators<UiModel.PlantView, UiModel> { before, after ->
when {
//total amount is used from the next PlantView
before == null -> UiModel.SeparatorModel("HEADER", after?.totalAmount)
// Return null to avoid adding a separator between two items.
else -> null
}
}
}
A drawback is the fact that the total amount is in every PlantView and it's always the same and therefore redundant.
For now, I found this comment usefull: https://issuetracker.google.com/issues/175338415#comment5
There people discuss the ways to provide metadata state to Pager
A simple way I found to fix it is by using a lambda in the PagingSource constructor. Try the following:
class PlantsDataSource(
// ...
private val getTotalItems: (Int) -> Unit
) : RxPagingSource<Int, PlantView>() {
override fun loadSingle(params: LoadParams<Int>): Single<LoadResult<Int, PlantView>> {
...
.map<LoadResult<Int, PlantView>> {
val total = it.meta?.total ?: 0
getTotalItems(total)
...
}
...
}
}
I want to update Recyclerview in realtime when a document is added or removed from firestore. I am using this logic in Kotlin:
for (doc in docs!!.documentChanges) {
val classElement: FireClassModel=doc.document.toObject(FireClassModel::class.java)
if (doc.type == DocumentChange.Type.ADDED) {
adapterList.add(classElement)
} else if(doc.type == DocumentChange.Type.REMOVED){
adapterList.remove(classElement)
}
adapter.notifyDataSetChanged()
}
Its working fine when document is added but it does not work when data is removed. It has no error but it doesn't update in real-time. It only updates when I restart the Application.
Updated
FireClassModel:
class FireClassModel {
var classID: String = ""
var className: String = ""
}
I tried this classesList.contains(classElement) and it returns false. It means I am unable to compare objects in my ArrayList.
Finally I have solved the issue. The issue was, I am not getting the right object.
I just replaced adapterList.remove(classElement) with following:
for (cls in adapterList) {
if (cls.classID == doc.document.id) {
adapterList.remove(cls)
}
}
Thanks to #Markus
Your code looks fine, but it seems that the element that you are trying to remove from the list cannot be found there.
adapterList.remove(classElement)
When removing an element from a list using remove(o: Object): Boolean the first matching element from the list will be removed. A matching element is an element where the equals method returns true.
listElement == elementToRemove // Kotlin
listElement.equals(elementToRemove); // Java
By default (that means if you do not override equals) objects will only be equal, if they share the same location in memory. In your example the element in the list sits at a different location than the element that you create from Firestore in your document change listener.
The solution depends on your FireClassModel. Looking at multiple FireClassModel objects, how would you decide which two of them are equal? Maybe they'll have the same id? Then override the equals method (and per contract also hashCode) and compare the fields that make two objects identical. For an id, the solution could look like that (generated by Android Studio):
class FireClassModel(val id: Int) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as FireClassModel
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
return id
}
}
After that comparing two FireClassModel objects with the same ID will return true. Without overriding the equals method that would not be the case (unless you comparing an object to itself).
More about equals can be found on Stackoverflow and in the Java documentation.