Kotlin Efficient way of Toggling an item in a list - android

I have a toggleItem algorithm which removes/adds item from/to a recyclerview items list. It was optimised by a friend of mine to reduce the code smell that comes with !! .
I'm looking for alternative / minimal ways I can write this simple algorithm, with the awesome kotlin collections operations that we have. What's your optimum/minimal alternative to these two?
.
(ItemType is an enum class, as a tag of initialised item)
Original:
fun toggleItem(itemType: ItemType, show: Boolean) {
val item = _allItems.value?.find { it.type == itemType }
item?.let {
if (!show) _carouselItems.value = _carouselItems.value!!.minus(it)
} ?: if (show) _carouselItems.value = _carouselItems.value!!.plus(item!!)
}
Further optimised :
fun toggleItem(itemType: ItemType, show: Boolean) {
if (show) {
val item = _allItems.value?.find { it.type == itemType }
item?.let {
_carouselItems.value = _carouselItems.value?.plus(it)?.distinct()?.sortedBy { it.type }
}
} else
_carouselItems.value = _carouselItems.value?.filter { it.type != itemType }
}

I’m inferring that _allItems and _carouselItems are both of type MutableLiveData<List<ItemType>?> or something very similar. You can replace your !! with an Elvis operator and default value to avoid the long chain of null-safe calls. It also is clearer to use an empty list to represent no data rather than using null so I would make the LiveData’s type non-nullable.
You can use the Elvis operator to do an early return / smart-cast in this case to avoid the use of let. A little more readable this way, in my opinion.
fun toggleItem(itemType: ItemType, show: Boolean) {
val currentItems = _carouselItems.value ?: emptyList()
if (show) {
val item = _allItems.value?.find { it.type == itemType }
?: return
_carouselItems.value = (currentItems + item).distinct().sortedBy { it.type }
} else
_carouselItems.value = currentItems.filter { it.type != itemType }
}
}
If you know the item should always be in the all items list, this could be more concise:
fun toggleItem(itemType: ItemType, show: Boolean) {
val item = _allItems.value?.find { it.type == itemType }
?: return
val currentItems = _carouselItems.value ?: emptyList()
_carouselItems.value = when {
show -> (currentItems + item).distinct().sortedBy { it.type }
else -> currentItems - item
}
}
You might also consider transitioning to StateFlow. It is a similar concept to LiveData, except that it enforces a beginning value, so its value parameter is not nullable if the type isn't nullable. But you should be familiar with coroutine basics first.

Both are ok when data set is small, but the computation will increase quickly as the data size grows.
// store visibility value to a map using ItemType as its keys
val _toggles: Map<ItemType, Boolean> = mapOf(ItemType.One to false, ItemType.Two to true);
fun toggleItem(itemType: ItemType, show: Boolean) {
_toggles[itemType] = show
val list = _allItem.values?.filter(item -> _toggles[item.type]) ?: listOf()
_carouselItems.value = list
}

Related

How to use LazyVerticalGrid with paginated api?

Here's the network request I have:
fun getItems(pageNumber: Int): Single<List<Item>>
Here's my lazy grid:
#Composable
fun ItemGridView(
productTiles: List<Item>,
) {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier.fillMaxSize(),
) {
items(productTiles) { item -> item.toPrettyComposableView() }
}
}
Currently, my ItemGridView will stop rendering after the first page, but I would like it to continue requesting and rendering the next page after the user reaches the last item of the page. If the api response gives me an odd number of items for the first page, for the next page, it should continue filling the grid on the right side of the rendered item instead of creating a new row.
Please help
If you want pagination in your app, perhaps you might want to take a look at the AndroidX Pagination library. It handles all sorts of cases with a nice API, it also has Jetpack Compose support by importing this library implementation("androidx.paging:paging-compose:1.0.0-alpha16").
After following the official guide and trying it out in Compose you might notice that it does have support for LazyColumn and LazyRow but it does not yet have for LazyVerticalGrid.
This extension function might come in useful to you:
fun <T : Any> LazyGridScope.items(
items: LazyPagingItems<T>,
key: ((item: T) -> Any)? = null,
span: ((item: T) -> GridItemSpan)? = null,
contentType: ((item: T) -> Any)? = null,
itemContent: #Composable LazyGridItemScope.(value: T?) -> Unit
) {
items(
count = items.itemCount,
key = if (key == null) null else { index ->
val item = items.peek(index)
if (item == null) {
PagingPlaceholderKey(index)
} else {
key(item)
}
},
span = if (span == null) null else { index ->
val item = items.peek(index)
if (item == null) {
GridItemSpan(1)
} else {
span(item)
}
},
contentType = if (contentType == null) {
{ null }
} else { index ->
val item = items.peek(index)
if (item == null) {
null
} else {
contentType(item)
}
}
) { index ->
itemContent(items[index])
}
}
And you would use it like so:
// Get hold of a Flow of PagingData from your ViewModel or something similar
val pagingListFlow: Flow<PagingData<T>> = ...
val pagingList = photosPagingList.collectAsLazyPagingItems()
LazyVerticalGrid(columns = GridCells.Fixed(columnCount)) {
// Use the extension function here
items(items = pagingList) { item ->
// Draw your composable
}
}

Android Paging 3 library loading infinitely without scroll with Jetpack Compose

I'm attempting to make a paged list of books using Jetpack Compose and Android's Paging 3 library. I am able to make the paged list and get the data fine, but the load() function of my paging data source is being called infinitely, without me scrolling the screen.
My paging data source looks like this:
class GoogleBooksBookSource #Inject constructor(
private val googleBooksRepository: GoogleBooksRepository,
private val query: String
): PagingSource<Int, Book>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Book> {
val position = params.key ?: 0
return try {
val response = googleBooksRepository.searchForBookStatic(query, position)
if (response is Result.Success) {
LoadResult.Page(
data = response.data.items,
prevKey = if (position == 0) null else position - 1,
nextKey = if (response.data.totalItems == 0) null else position + 1
)
} else {
LoadResult.Error(Exception("Error loading paged data"))
}
} catch (e: Exception) {
Log.e("PagingError", e.message.toString())
return LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, Book>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
}
and this is the UI:
Column() {
// other stuff
LazyColumn(
modifier = Modifier.padding(horizontal = 24.dp),
content = {
for (i in 0 until searchResults.itemCount) {
searchResults[i]?.let { book ->
item {
BookCard(
book = book,
navigateToBookDetail = { navigateToBookDetail(book.id) }
)
}
}
}
}
)
}
As far as I can tell, the data loads correctly and in the correct order, but when I log the API request URLs, it's making infinite calls with an increasing startIndex each time. That would be fine if I was scrolling, since Google Books searches often return thousands of results, but it does this even if I don't scroll the screen.
The issue here was the way I was creating elements in the LazyColumn - it natively supports LazyPagingItem but I wasn't using that. Here is the working version:
LazyColumn(
modifier = Modifier.padding(horizontal = 24.dp),
state = listState,
content = {
items(pagedSearchResults) { book ->
book?.let {
BookCard(
book = book,
navigateToBookDetail = { navigateToBookDetail(book.id) }
)
}
}
}
)
In your original example, you have to use peek to check for non-null and access the list as you do only inside item block, which is lazy. Otherwise the paging capabilities will be lost and it will load the entire dataset in one go.

How to detect when an EditText is empty using RxTextView (RxBinding)

I'm doing validation on an EditText. I want the CharSequence to be invalid if it's empty or it doesn't begin with "https://". I'm also using RxBinding, specifically RxTextView. The problem is that when there is one character left, and I then delete it leaving no characters left in the the CharSequence the map operator doesn't fire off an emission. In other words I want my map operator to return false when the EditText is empty. I'm beginning to think this may not be possible the way I'm doing it. What would be an alternative?
Here is my Observable / Disposable:
val systemIdDisposable = RxTextView.textChanges(binding.etSystemId)
.skipInitialValue()
.map { charSeq ->
if (charSeq.isEmpty()) {
false
} else {
viewModel.isSystemIdValid(charSeq.toString())
}
}
.subscribe { isValid ->
if (!isValid) {
binding.systemIdTextInputLayout.isErrorEnabled = true
binding.systemIdTextInputLayout.error = viewModel.authErrorFields.value?.systemId
} else {
binding.systemIdTextInputLayout.isErrorEnabled = false
binding.systemIdTextInputLayout.error = viewModel.authErrorFields.value?.systemId
}
}
And here is a function in my ViewModel that I pass the CharSequence to for validation:
fun isSystemIdValid(systemId: String?): Boolean {
return if (systemId != null && systemId.isNotEmpty()) {
_authErrors.value?.systemId = null
true
} else {
_authErrors.value?.systemId =
getApplication<Application>().resources.getString(R.string.field_empty_error)
false
}
}
After sleeping on it, I figured it out.
I changed RxTextView.textChanges to RxTextView.textChangeEvents. This allowed me to query the CharSequence's text value (using text() method provided by textChangeEvents) even if it's empty. Due to some other changes (not really relevant to what I was asking in this question) I was also able to reduce some of the conditional code too. I'm just putting that out there in case someone comes across this and is curious about these changes. The takeaway is that you can get that empty emission using RxTextView.textChangeEvents.
Here is my new Observer:
val systemIdDisposable = RxTextView.textChangeEvents(binding.etSystemId)
.skipInitialValue()
.map { charSeq -> viewModel.isSystemIdValid(charSeq.text().toString()) }
.subscribe {
binding.systemIdTextInputLayout.error = viewModel.authErrors.value?.systemId
}
And here is my validation code from the ViewModel:
fun isSystemIdValid(systemId: String?): Boolean {
val auth = _authErrors.value
return if (systemId != null && systemId.isNotEmpty()) {
auth?.systemId = null
_authErrors.value = auth
true
} else {
auth?.systemId =
getApplication<Application>().resources.getString(R.string.field_empty_error)
_authErrors.value = auth
false
}
}
Lastly, if anyone is curious about how I'm using my LiveData / MutableLiveData objects; I create a private MutableLiveData object and only expose an immutable LiveData object that returns the values of the first object. I do this for better encapsulation / data hiding. Here is an example:
private val _authErrors: MutableLiveData<AuthErrorFields> by lazy {
MutableLiveData<AuthErrorFields>()
}
val authErrors: LiveData<AuthErrorFields>
get() { return _authErrors }
Hope this helps someone! 🤗

Sorting Strings that contains number in kotlin

I wanna sort some strings that contain numbers but after a sort, it becomes like this ["s1", "s10", "s11", ... ,"s2", "s21", "s22"]. after i search i fount this question with same problem. but in my example, I have mutableList<myModel>, and I must put all string in myModel.title for example into a mutable list and place into under code:
val sortData = reversedData.sortedBy {
//pattern.matcher(it.title).matches()
Collections.sort(it.title, object : Comparator<String> {
override fun compare(o1: String, o2: String): Int {
return extractInt(o1) - extractInt(o2)
}
fun extractInt(s: String): Int {
val num = s.replace("\\D".toRegex(), "")
// return 0 if no digits found
return if (num.isEmpty()) 0 else Integer.parseInt(num)
}
})
}
I have an error in .sortedBy and Collections.sort(it.title), may please help me to fix this.
you can use sortWith instead of sortBy
for example:
class Test(val title:String) {
override fun toString(): String {
return "$title"
}
}
val list = listOf<Test>(Test("s1"), Test("s101"),
Test("s131"), Test("s321"), Test("s23"), Test("s21"), Test("s22"))
val sortData = list.sortedWith( object : Comparator<Test> {
override fun compare(o1: Test, o2: Test): Int {
return extractInt(o1) - extractInt(o2)
}
fun extractInt(s: Test): Int {
val num = s.title.replace("\\D".toRegex(), "")
// return 0 if no digits found
return if (num.isEmpty()) 0 else Integer.parseInt(num)
}
})
will give output:
[s1, s21, s22, s23, s101, s131, s321]
A possible solution based on the data you posted:
sortedBy { "s(\\d+)".toRegex().matchEntire(it)?.groups?.get(1)?.value?.toInt() }
Of course I would move the regex out of the lambda, but it is a more concise answer this way.
A possible solution can be this:
reversedData.toObservable()
.sorted { o1, o2 ->
val pattern = Pattern.compile("\\d+")
val matcher = pattern.matcher(o1.title)
val matcher2 = pattern.matcher(o2.title)
if (matcher.find()) {
matcher2.find()
val o1Num = matcher.group(0).toInt()
val o2Num = matcher2.group(0).toInt()
return#sorted o1Num - o2Num
} else {
return#sorted o1.title?.compareTo(o2.title ?: "") ?: 0
}
}
.toList()
.subscribeBy(
onError = {
it
},
onSuccess = {
reversedData = it
}
)
As you state that you need a MutableList, but don't have one yet, you should use sortedBy or sortedWith (in case you want to work with a comparator) instead and you get just a (new) list out of your current one, e.g.:
val yourMutableSortedList = reversedData.sortedBy {
pattern.find(it)?.value?.toInt() ?: 0
}.toMutableList() // now calling toMutableList only because you said you require one... so why don't just sorting it into a new list and returning a mutable list afterwards?
You may want to take advantage of compareBy (or Javas Comparator.comparing) for sortedWith.
If you just want to sort an existing mutable list use sortWith (or Collections.sort):
reversedData.sortWith(compareBy {
pattern.find(it)?.value?.toInt() ?: 0
})
// or using Java imports:
Collections.sort(reversedData, Compatarator.comparingInt {
pattern.find(it)?.value?.toInt() ?: 0 // what would be the default for non-matching ones?
})
Of course you can also play around with other comparator helpers (e.g. mixing nulls last, or similar), e.g.:
reversedData.sortWith(nullsLast(compareBy {
pattern.find(it)?.value
}))
For the samples above I used the following Regex:
val pattern = """\d+""".toRegex()
I wrote a custom comparator for my JSON sorting. It can be adapted from bare String/Number/Null
fun getComparator(sortBy: String, desc: Boolean = false): Comparator<SearchResource.SearchResult> {
return Comparator { o1, o2 ->
val v1 = getCompValue(o1, sortBy)
val v2 = getCompValue(o2, sortBy)
(if (v1 is Float && v2 is Float) {
v1 - v2
} else if (v1 is String && v2 is String) {
v1.compareTo(v2).toFloat()
} else {
getCompDefault(v1) - getCompDefault(v2)
}).sign.toInt() * (if (desc) -1 else 1)
}
}
private fun getCompValue(o: SearchResource.SearchResult, sortBy: String): Any? {
val sorter = gson.fromJson<JsonObject>(gson.toJson(o))[sortBy]
try {
return sorter.asFloat
} catch (e: ClassCastException) {
try {
return sorter.asString
} catch (e: ClassCastException) {
return null
}
}
}
private fun getCompDefault(v: Any?): Float {
return if (v is Float) v else if (v is String) Float.POSITIVE_INFINITY else Float.NEGATIVE_INFINITY
}

Remove data from list while iterating kotlin

I am new to kotlin programming. What I want is that I want to remove a particular data from a list while iterating through it, but when I am doing that my app is crashing.
for ((pos, i) in listTotal!!.withIndex()) {
if (pos != 0 && pos != listTotal!!.size - 1) {
if (paymentsAndTagsModel.tagName == i.header) {
//listTotal!!.removeAt(pos)
listTotal!!.remove(i)
}
}
}
OR
for ((pos,i) in listTotal!!.listIterator().withIndex()){
if (i.header == paymentsAndTagsModel.tagName){
listTotal!!.listIterator(pos).remove()
}
}
The exception which I am getting
java.lang.IllegalStateException
use removeAll
pushList?.removeAll { TimeUnit.MILLISECONDS.toMinutes(
System.currentTimeMillis() - it.date) > THRESHOLD }
val numbers = mutableListOf(1,2,3,4,5,6)
val numberIterator = numbers.iterator()
while (numberIterator.hasNext()) {
val integer = numberIterator.next()
if (integer < 3) {
numberIterator.remove()
}
}
It's forbidden to modify a collection through its interface while iterating over it. The only way to mutate the collection contents is to use Iterator.remove.
However using Iterators can be unwieldy and in vast majority of cases it's better to treat the collections as immutable which Kotlin encourages. You can use a filter to create a new collections like so:
listTotal = listTotal.filterIndexed { ix, element ->
ix != 0 && ix != listTotal.lastIndex && element.header == paymentsAndTagsModel.tagName
}
The answer by miensol seems perfect.
However, I don't understand the context for using the withIndex function or filteredIndex. You can use the filter function just by itself.
You don't need access to the index the list is at, if you're using
lists.
Also, I'd strongly recommend working with a data class if you already aren't. Your code would look something like this
Data Class
data class Event(
var eventCode : String,
var header : String
)
Filtering Logic
fun main(args:Array<String>){
val eventList : MutableList<Event> = mutableListOf(
Event(eventCode = "123",header = "One"),
Event(eventCode = "456",header = "Two"),
Event(eventCode = "789",header = "Three")
)
val filteredList = eventList.filter { !it.header.equals("Two") }
}
The following code works for me:
val iterator = listTotal.iterator()
for(i in iterator){
if(i.haer== paymentsAndTagsModel.tagName){
iterator.remove()
}
}
You can also read this article.
People didn't break iteration in previous posts dont know why. It can be simple but also with extensions and also for Map:
fun <T> MutableCollection<T>.removeFirst(filter: (T) -> Boolean) =
iterator().removeIf(filter)
fun <K, V> MutableMap<K, V>.removeFirst(filter: (K, V) -> Boolean) =
iterator().removeIf { filter(it.key, it.value) }
fun <T> MutableIterator<T>.removeFirst(filter: (T) -> Boolean): Boolean {
for (item in this) if (filter.invoke(item)) {
remove()
return true
}
return false
}
Use a while loop, here is the kotlin extension function:
fun <E> MutableList<E>.removeIfMatch(isMatchConsumer: (existingItem: E) -> Boolean) {
var index = 0
var lastIndex = this.size -1
while(index <= lastIndex && lastIndex >= 0){
when {
isMatchConsumer.invoke(this[index]) -> {
this.removeAt(index)
lastIndex-- // max is decreased by 1
}
else -> index++ // only increment if we do not remove
}
}
}
Typically you can use:
yourMutableCollection.removeIf { someLogic == true }
However, I'm working with an Android app that must support APIs older than 24.
In this case removeIf can't be used.
Here's a solution that is nearly identical to that implemented in Kotlin Collections that doesn't rely on Predicate.test - which is why API 24+ is required in the first place
//This function is in Kotlin Collections but only for Android API 24+
fun <E> MutableCollection<E>.removeIff(filter: (E) -> Boolean): Boolean {
var removed = false
val iterator: MutableIterator<E> = this.iterator()
while (iterator.hasNext()) {
val value = iterator.next()
if (filter.invoke(value)) {
iterator.remove()
removed = true
}
}
return removed
}
Another solution that will suit small collections. For example set of listeners in some controller.
inline fun <T> MutableCollection<T>.forEachSafe(action: (T) -> Unit) {
val listCopy = ArrayList<T>(this)
for (element: T in listCopy) {
if (this.contains(element)) {
action(element)
}
}
}
It makes sure that elements of collection can be removed safely even from outside code.

Categories

Resources