My Activity has SearchView, which should have filtered query. I am filtering it via onQueryTextListener. Idea is that disable typing not allowed chars, like dot, comma, slash and etc - only [A-Z] is allowed
Here is code (I am using Kotlin, but it is readable for Java):
var shouldQueryChangeBeInvoked = true
override fun onCreate(savedInstanceState: Bundle?) {
...
searchField.setOnQueryTextListener(this)
}
override fun setSearchQuery(query: String) {
shouldQueryChangeBeInvoked = false
searchField.setQuery(query, false)
shouldQueryChangeBeInvoked = true
}
override fun onQueryTextChange(newText: String?): Boolean {
if (!shouldQueryChangeBeInvoked) {
return false
}
val validQuery = validateQuery(newText)
validQuery?.let { setSearchQuery(it) }
return false
}
When I type, for example, "ABC." it converts to "ABC". So, it works fine.
But when I type dot(".") second time onQueryTextChange doesn't invokes at all - I have set breakpoint on first line of the method.
It was tested on two different phones, so it is not keyboard settings or smth like that.
Why listener doesn't invoke?
EDIT
Validation of query I making with Regex like that:
fun validateQuery(query: String?): String? {
val regex = Regex("^([A-Z]+)")
// Return first match or null
return query?.let { regex.find(it.toUpperCase()) }?.value
}
Don't care about Regex creation - I provide it via DI, so, it creates only once per activity.
Maybe it can be problem?
Thanks a lot to #pskink!
I have implemented custom SearchView with filter input query and ability to disable triggering onQueryTextChange on setQuery.
It is Kotlin, let's move on to that beautiful language :)
I hope it will be useful for someone.
Here is Code on Gist
I have found problem: it is bad to change query text inside of onQueryTextChange - if we look inside of SearchView class, there is such code:
void onTextChanged(CharSequence newText) {
...
if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) {
mOnQueryChangeListener.onQueryTextChange(newText.toString());
}
mOldQueryText = newText.toString();
}
So, old query text will be updated two times - first time with correct query "ABC", and second time with wrong one "ABC..", as I understand well.
I will ask other question separately about changing query after onQueryTextChange
you can use coroutine
override fun onQueryTextChange(newText: String?): Boolean {
if (!newText.isNullOrEmpty()) {
val searchText = "[^A-Za-z0-9-]+".toRegex().replace(newText, "")
if (searchText != newText) {
CoroutineScope(Dispatchers.Main).launch {
binding.createPostTopicChooserSearch.setQuery(searchText, false)
}
return true
}
Related
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.
This question already has an answer here:
I don't understand the argument passing of the lambda expression in the listener
(1 answer)
Closed 1 year ago.
I found on github these lines of code and spend more than hour trying to understand.
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_fragment_tasks, menu)
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.onQueryTextChanged {
viewModel.searchQuery.value = it
}
}
and this is the part im having hard time trying to understand:
inline fun SearchView.onQueryTextChanged(crossinline listener: (String) -> Unit) {
this.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
listener(newText.orEmpty())
return true
}
})
}
I literally don't understand almost a single word.
inline crossline aside, can you explain me how listener works there.
P.S Im noob in kotlin, my first language is Java
I am learning Kotlin myself and thought I can help with the code snippet above.
SearchView.OnQueryTextListener is an interface with onQueryTextChange and onQueryTextSubmit methods - reference - https://developer.android.com/reference/kotlin/android/widget/SearchView.OnQueryTextListener
SearchView.onQueryTextChanged function looks like an extension function on SearchView, which accepts a lambda named listener.
The listener lambda itself is a function which takes a string as param and returns Unit ie returns nothing. That means the function will do some side-effect eg IO, update some state etc.
Since onQueryTextChanged is an extension function, you get the reference to the object on which this function will be called as this in the function body
object : SearchView.OnQueryTextListener part is basically an object implementing the SearchView.OnQueryTextListener interface with both the methods from the interface overridden.
The object in above step is passed as param to setOnQueryTextListener method.
At the caller site, i.e.
searchView.onQueryTextChanged {
viewModel.searchQuery.value = it
}
Please note the it value is implicit param to a lambda, you can rewrite it to make it more readable as well,
searchView.onQueryTextChanged { newSearchQuery ->
viewModel.searchQuery.value = newSearchQuery
}
the listener lambda here is a function which takes a string and sets the value to viewModel.searchQuery.value.
The inline keyword basically replaces function call with function body directly at compile time, kind of what inline variable refactoring that can be done, although the compiler does it at compile time - kind of an optimization.
Suppose that i'm writing my own View class, that represents editable field and contains EditText tv_input inside:
class EditTextIconItemView : LinearLayout {
fun setInputText(text: String?) {
with (tv_input) {
setText(text)
setCharsCount(text?.length ?: 0)
setSelection(text?.length ?: 0)
}
}
fun setCharsCount(count: Int) {
tv_chars_count?.text = "$count/$MAX_LENGTH"
}
}
I'd like to delegate textChanges() to that internal EditText tv_input, so i'm writing the following code in my custom EditTextIconItemView:
fun EditTextIconItemView.textChanges() =
tv_input.textChanges().doOnNext { setCharsCount(it.length) }
That works well, but now i want my client code to actually skip initial value, so in client code i have:
val et = EditTextIconItemView()
et.textChanges().skipInitialValue().subscribe { ... }
This requires me to explicitly specify the return type in EditTextIconItemView for textChanges():
fun EditTextIconItemView.textChanges(): InitialValueObservable<CharSequence> =
tv_input.textChanges().doOnNext { setCharsCount(it.length) }
But this won't compile since doOnNext returns Observable which cannot be cast to InitialValueObservable.
But i actually don't want the client code to handle that side effect and set up chars count on that View, this is the responsibility of EditTextIconItemView itself. But i' like to still be able to tell, whether to skip initial value or not on client's side.
How could i make it work?
Thank You!
What if overload some Kotlin operator and use it like this:
// Inits somewhere before usage.
val someStrFromServer: String?
lateinit var myFieldText: TextView
override fun onStart() {
super.onStart()
myFieldText.text = someStrFromServer / R.string.app_none
}
Operator overloading:
operator fun String?.div(resId: Int): String {
return if (this.isNullOrBlank()) {
context.getString(resId)
} else {
this
}
}
Output if someStrFromServer null:
D/DEBUG: None
Output if someStrFromServer not null:
D/DEBUG: someStrFromServer
Does anyone know, if in Kotlin exists a more efficient and short way to handle this? Perhaps, even more, global, like extension function.
You can do that, but it's not very intuitive because the div is normally used in mathematical calculations only. I'd recommend to use something like
someStrFromServer.takeUnless { it.isNullOrBlank()} ?: context.getString(resId)
Or simplified via extension
fun String?.fallback(resId: Int) = takeUnless { it.isNullOrBlank()} ?: context.getString(resId)
used like this:
myFieldText.text = someStrFromServer.fallback(R.string.app_none)
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.