Custom Components Sorting based on required Ordering - android

Trying to sort the custom components on defined order, and other subjects should be after that desired order: Like if there is any other subject except the defined List ie "GK" then it should be on last position etc.
However I am getting Null Pointer Exception due to subject is not defined in the requireList if scheduleCommandList have the subject which is not in requiredList. How can I overcome this?
Desired Order List is Below:
private val requiredList: HashMap<String, Int> = hashMapOf(
"Maths" to 0,
"Physics" to 1,
"Science" to 2,
)
Sorting function to sort the List:
private fun sortCommandList(scheduleCommandList: ArrayList<BaseComponent>): ArrayList<BaseComponent> {
val comparator = Comparator { o1: BaseComponent, o2: BaseComponent ->
return#Comparator requiredList[o1.name]!! - requiredList[o2.name]!!
}
val copy = arrayListOf<BaseComponent>().apply { addAll(scheduleCommandList) }
copy.sortWith(comparator)
return copy
}

It seems you understand the problem correctly. If an item is not present in requiredList then you still try to compare their required positions and this causes NullPointerException. Remember that you should use !! only in cases when you are sure there can't be a null. In this case null is possible and we have to handle it somehow. The easiest is to replace it with Int.MAX_VALUE which places the item at the end. Also, this code can be really much simpler:
private fun sortCommandList(scheduleCommandList: List<BaseComponent>): List<BaseComponent> {
return scheduleCommandList.sortedBy { requiredList[it.name] ?: Int.MAX_VALUE }
}
It can be even better to create this utility as extension function:
private fun List<BaseComponent>.mySort(): List<BaseComponent> {
return sortedBy { requiredList[it.name] ?: Int.MAX_VALUE }
}
Then we can simplify the name of the function, because it is implicit that it is used to sort BaseComponent objects.

Related

DiffUtil not refreshing view in Observer call android kotlin

Hey I am using diff util with ListAdapter. The updating of list works but I can only see those new values by scrolling the list, I need to view the updates even without recycling the view (when scrolling) just like notifyItemChanged(). I tried everything inside this answer ListAdapter not updating item in RecyclerView only working for me is notifyItemChanged or setting adapter again. I am adding some code. Please someone know how to fix this problem?
Data and Enum class
data class GroupKey(
val type: Type,
val abc: Abc? = null,
val closeAt: String? = null
)
data class Group(
val key: GroupKey,
val value: MutableList<Item?> = ArrayDeque()
)
enum class Type{
ONE,
TWO
}
data class Abc(
val qq: String? = null,
val bb: String? = null,
val rr: RType? = null,
val id: String? = null
)
data class RType(
val id: String? = null,
val name: String? = null
)
data class Item(
val text: String? = null,
var abc: Abc? = null,
val rr: rType? = null,
val id: String? = null
)
viewmodel.kt
var list: MutableLiveData<MutableList<Group>?> = MutableLiveData(ArrayDeque())
fun populateList(){
// logic to call api
list.postValue(data)
}
fun addItemTop(){
// logic to add item on top
list.postValue(data)
}
inside view model I am filling data by api call inside viewmodel function and return value to list. Also another function which item is inserting at top of list so that's why is used ArrayDeque
Now I am adding nested reyclerview diff util callback.
FirstAdapter.kt
class FirstAdapter :
ListAdapter<Group, RecyclerView.ViewHolder>(comp) {
companion object {
private val comp = object : DiffUtil.ItemCallback<Group>() {
override fun areItemsTheSame(oldItem: Group, newItem: Group): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Group, newItem: Group): Boolean {
return ((oldItem.value == newItem.value) && (oldItem.key == newItem.key))
}
}
}
......... more function of adapter
}
FirstViewHolder
val adapter = SecondAdapter()
binding.recyclerView.adapter = adapter
adapter.submitList(item.value)
SecondAdapter.kt
class SecondAdapter : ListAdapter<Item, OutgoingMessagesViewHolder>(comp) {
companion object {
private val comp = object : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return ((oldItem.rr == newItem.rr) &&
(oldItem.text == oldItem.text) && (oldItem.abc == newItem.abc))
}
}
}
..... more function
}
Activity.kt
viewModel.list.observe(this, { value ->
submitList(value)
})
private fun submitList(list: MutableList<Group>?) {
adapter?.submitList(list)
// adapter?.notifyDataSetChanged()
}
I am 100% sure that my list is updating and my observer is calling when my new list is added. I debug that through debug view. But problem is I can only see those new values by scrolling the list, I need to view the updates even without recycling the view (when scrolling) just like notifyItemChanged()
UPDATE
viewmodel.kt
class viewModel : BaseViewModel(){
var list: MutableLiveData<MutableList<Group>?> = MutableLiveData()
//... more variables...
fun fetchData(context: Context) {
viewModelScope.launch {
val response = retroitApiCall()
response.handleResult(
onSuccess = { response ->
list.postValue(GroupData(response?.items, context))
},
onError = { error ->
Log.e("error" ,"$error")
}
)
}
}
}
internal fun GroupData(items: List<CItem>?, context: Context): MutableList<Group> {
val result: MutableList<Group> = MutableList()
items?.iterator()?.forEach { item ->
// adding item in list by add function and then return list.
return result
}
private fun addItemOnTop(text: String) {
list.value?.let { oldlist ->
// logic to add items on top of oldlist variable
if(top != null){
oldlist.add(0,item)
}else{
val firstGroup = oldlist[0]
firstGroup.value.add(item)
}
list.postValue(oldlist)
}
}
}
I am using sealed class something like this but not this one Example. And Something similar to these when call api Retrofit Example. Both link I am giving you example. What I am using in my viewmodel.
I don't know what's going on, but I can tell you two things that caught my attention.
First Adapter:
override fun areItemsTheSame(oldItem: Group, newItem: Group): Boolean {
return oldItem == newItem
}
You're not comparing if the items are the same, you're comparing the items and their contents are the same. Don't you have an Id like you did in your second adapter?
I'd probably check oldItem.key == newItem.key.
Submitting the List
As indicated in the answer you linked, submitList has a very strange logic where it compares if the reference of the actual list is the same, and if it is, it does nothing.
In your question, you didn't show where the list comes from (it's observed through what appears to be liveData or RXJava), but the souce of where the list is constructed is not visible.
In other words:
// P S E U D O C O D E
val item1 = ...
val item2 = ...
val list1 = mutableListOf(item1, item2)
adapter.submitList(list1) // works fine
item1.xxx = ""
adapter.submitList(list1) // doesn't work well.
WHY?
Unfortunately, submitList's source code shows us that if the reference to the list is the same, the diff is not calculated. This is really not on the adapter, but rather on AsyncListDiffer, used by ListAdapter internally. It is this differ's responsibility to trigger the calculation(s). But if the list references are the same, it doesn't, and it silently ignores it.
My suspicion is that you're not creating a new list. This rather undocumented and silent behavior hurts more than it helps, because more often than not, developers aren't expecting to duplicate a list supplied to an object whose purpose and promise is to offer the ability to "magically" (and more importantly, automatically) calculate its differences between the previous.
I understand why they did it, but I would have at the very least emitted a log WARNING, indicating you're supplying the same list. Or, if you want to avoid polluting the already polluted logCat, then at least be much more explicit about it in its official documentation.
The only hint is this simple phrase:
you can use submitList(List) when new lists are available.
The key here being the word new lists. So not the same list with new items, but simply a new List reference (regardless of whether the items are the same or not).
What should you try?
I'd start by modifying your submitList method:
private fun submitList(list: MutableList<Group>?) {
adapter?.submitList(list.toMutableList())
}
For Java users out there:
adapter.submitList(new ArrayList(oldList));
The change is to create a copy of the list you receive: list.ToMutableList(). This way the AsyncListDiffer's check for list equality will return false and the code will continue.
UPDATE / DEBUG
Unfortunately, I don't know what is going on with your code; I assure you that ListAdapter works, as I use it myself on a daily basis; If you think you've found a case where there are problems with it, I suggest you create a small prototype and publish it on github or similar so we can reproduce it.
I would start by using debug/breakpoints in key areas:
ViewModel; write down the reference fromthe list you "return".
DiffUtil methods, is diffUtil being called?
Your submitList() method, is the list reference the same as the one you had in your ViewModel?
etc.
You need to dig a bit deeper until you find out who is not doing what.
On Deep vs Shallow copy and Java and whatever...
Please keep in mind, ListAdapter (through AsyncDiff) checks if the reference to the list is the same. In other words, if you have a list val x = mutableListOf(...) and you give this to the adapter, it will work the 1st time.
If you then modify the list...
val x = mutableListOf(...)
adapter.submitList(x)
x.clear()
adapter.submitList(x)
This will NOT WORK correctly, because to the eyes of the Adapter both lists are the same (they actually are the same list).
The fact that the list is mutable is irrelevant. (I still frown upon the mutable list; why does submitList accept a mutable list if you cannot mutate it and submit it again, escapes my knowledge but I would not have approved that Pull Request like so) It would have avoided most problems if they only took a non-mutable list, therefore implying you must supply a new list every time if you mutate it. Anyway...
as I was saying, duplicating a list is simple, in either Kotlin or Java there are multiple variations:
val newListWithSameContents = list1.toList()
List newListWithSameContents = ArrayList(list1);
now if list1 has an item...
list1.add("hello")
When you copy list1 into newList... The reference to "Hello" (the string) is the same. If String were mutable (it's not, but assume it is), and you modified that string somehow... you would be modifying both strings at the same time or rather, the same string, referenced in both lists.
data class Thing(var id: Int)
val thing = Thing(1)
val list1: MutableList<Thing> = mutableListOf(thing)
val list2: MutableList<Thing> = list1.toMutableList()
println(list1)
println(list2)
// This prints
[Thing(id=1)]
[Thing(id=1)]
Now modify the thing...
thing.id = 2
println(list1)
println(list2)
As expected, both lists, pointing to the same object:
[Thing(id=2)]
[Thing(id=2)]
This was a shallow copy because the items were not copied. They still point to the same thing in memory.
ListAdapter/DiffUtil do not care if the objects are the same in that regard (depending how you implemented your diffutil that is); but they certainly care if the lists are the same. As in the above example.
I hope this clarifies what is needed for ListAdapter to dispatch updates. If it fails to do so, then check if you're effectively doing the right thing.

Find first occurrence index value in efficent way in kotlin

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)
}

Paging library 3.0 : How to pass total count of items to the list header?

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)
...
}
...
}
}

Create dynamic sorting Comparator builder from API return

I have a problem that I really cannot solve.. Maybe You may help me. I need to sort an object list from API return that contains filters. The problem is those filters are dynamic, The object Order (the problematic filter) :
class Order(val field : String, val direction: String)
The field is an object attribute (column), the direction can be ASC or DESC.
The JSON can return more than one filter, so this can be :
order : {
field : "id",
direction : "ASC"
},
{
field : "creationDate"
direction : "DESC"
}
The problem is, I don't know how to create a dynamic function that can create a perfect sort in my list. I know I've got to do this :
return list.sortedWith(compareBy(List::id).thenByDescending(List::creationDate))
But Dynamically.. wow
KT
You can create a map from a property name to the comparator that compares orders by that property:
val comparators = mapOf<String, Comparator<Order>>(
"field" to compareBy { it.field },
"direction" to compareBy { it.direction }
)
Then you can pick comparators from that map by the given property names, change their sorting order with Comparator.reversed() extension function, and finally combine all these comparators into the single resulting comparator with Comparator.then(Comparator) function:
val givenOrder = listOf("field" to "ASC", "direction" to "DESC")
val resultingOrder = givenOrder
.map { (fieldName, direction) ->
comparators[fieldName]!!.let { if (direction == "DESC") it.reversed() else it }
}
.reduce { order, nextComparator -> order.then(nextComparator) }
val sortedList = list.sortedWith(resultingOrder)
I am guessing that the second ordering oly applies to those where the first is the same value
sortedWith + compareBy
compareBy takes a vararg of selectors which is just a array, so it can be constructed
val selectors: Array<(T) -> Comparable<*>?> = orders.map { TODO() }.toArray()
list.sortedWith(compareBy(*selectors))
i am thinking some extra function go go though all possible fields you could sort and uses either it.field or -(it.field) to create the selectors
also see this answer: Sort collection by multiple fields in Kotlin

Kotlin: How can I reduce child arrays into a single array?

I have a pice of code in Swift that reduces a list of TVSchedule objects into an array of TVMatch pobjects. Each TVSchedule, has a property called events, that is a list of TVMatches.
The code in swift is the following:
var matches: [TVMatch] {
let slots = timeSlots.reduce(into: [TVMatch]()) { (result, schedule) in
result.append(contentsOf: schedule.events)
}
return slots
}
I'm trying to do the same reduce in Kotlin and the code I have is the following:
val matches: ArrayList<TVMatch>
get() {
val slots = timeSlots.fold(arrayListOf<TVMatch>()) { result, schedule ->
result.addAll(schedule.events)
}
return slots
}
However, the Kotlin code gives me a type error, and does not compile. What is the problem here?
addAll returns a boolean, but the return value of the fold-operation should be of same type as the given initial object (in this case ArrayList).
You can solve that one easily by just adding result after your addAll-statement, e.g.:
result.addAll(schedule.events)
result // this is now the actual return value of the fold-operation
Alternatively just use apply or similar instead:
result.apply {
addAll(schedule.events)
} // result is the return value then
Note that you can actually simplify altogether using flatMap to just (side-note: if you use this approach the matches are evaluated only once of course, but flatMap is the star here anyway ;-))):
val matches = timeSlots.flatMap { it.events } // this is a new list! (note, if you do several mappings in a row, you may want to use timeSlots.asSequence().flatMap { }.map { }.toList() / or .toMutableList() instead
Alternatively if you really require the matches to be of type ArrayList, use flatMapTo instead:
val matches = timeSlots.flatMapTo(ArrayList()) { it.events }
You can of course keep the get() if you must, or just move the getting of the matches to its own function, e.g.:
fun getMatches() = timeSlots.flatMapTo(ArrayList()) { it.events }
Am I crazy, or can't you just replace the code with
val matches: List<TVMatch>
get() = timeSlots.flatMap { schedule -> schedule.events }
?

Categories

Resources