I have been working on a stock keeping application as a demo to learn kotlin and android studio, I have added textchangedlistener in a recycler view item, which everytime it text is changed, gets information from Api and displays new item on textview. This works good for only one time, After that it keeps infinitely changing. please check video for better understanding : ScreenRecording .
Here is my rcv adapter :
class RecyclerViewAdapterU (val dataList:ArrayList<ModelClass>): RecyclerView.Adapter<RecyclerViewAdapterU.ViewHolder>() {
var _binding: UploadItemViewBinding? = null
val binding get() = _binding!!
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RecyclerViewAdapterU.ViewHolder {
val v =
LayoutInflater.from(parent.context).inflate(R.layout.upload_item_view, parent, false)
_binding = UploadItemViewBinding.bind(v)
return ViewHolder(binding.root)
}
override fun onBindViewHolder(holder: ViewHolder, #SuppressLint("RecyclerView") position: Int) {
bindItems(dataList[position])
holder.getStock()
holder.updateStockDetail()
}
fun bindItems(data: ModelClass) {
binding.apply {
itemquant.text=data.item_quant
uploadItemName.text = data.item_name
uploadMfg.text = data.mfg
skuStock.setText(data.item_stock.toString())
skuCode.setText(data.sku_code)
}
}
fun getUpdatedDetails(skucode:String,pos:Int){
val call: Call<List<ModelClass>>? =
ApiClient.instance?.myApi?.getfromsku(skucode)!!
call!!.enqueue(object : Callback<List<ModelClass>?> {
override fun onResponse(
call: Call<List<ModelClass>?>,
response: Response<List<ModelClass>?>
) {
val skuDetails=response.body()
if (skuDetails != null) {
dataList.removeAt(pos)
for (i in skuDetails){
println(i.item_name)
dataList.add(pos,i)
}
notifyItemChanged(pos)
}
}
override fun onFailure(call: Call<List<ModelClass>?>, t: Throwable) {
}
})
}
override fun getItemCount(): Int {
return dataList.size
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun getStock() {
binding.skuStock.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
for (i in 0 until RecyclerViewAdapter.ob.dataSelected.size){
if (editable.toString().trim()!=""){
var x= editable.toString().trim().toInt()
RecyclerViewAdapter.ob.dataSelected[adapterPosition].item_stock=x
//getting current itemstock before pushing update.
//assigning latest itemstock to the data for the update
}
}
}
})
}
fun updateStockDetail(){
binding.skuCode.addTextChangedListener(object : TextWatcher{
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
}
override fun afterTextChanged(editable: Editable) {
var x:String=""
var pos:Int=adapterPosition
for (i in 0 until RecyclerViewAdapter.ob.dataSelected.size){
if (editable.toString().trim()!=""){
x=editable.toString().trim()
//RecyclerViewAdapter.ob.dataSelected[adapterPosition].sku_code=x
println("$x in if")
}
}
//println(RecyclerViewAdapter.ob.dataSelected[adapterPosition].sku_code)
//getting edited text and calling the function to get updated details.
getUpdatedDetails(x,pos)
binding.skuStock.removeTextChangedListener(this)
}
})
}
}
}
If possible please review my code and let me know what are things i need to work on.
Note: ob.dataselected is a global variable from another recyclerview adapter.
Textchangedlistener i am talking about is in the fun updateStockDetail()
In the documentation for afterTextChanged we see the following:
This method is called to notify you that, somewhere within s, the text has been changed. It is legitimate to make further changes to s from this callback, but be careful not to get yourself into an infinite loop, because any changes you make will cause this method to be called again recursively.
(The emphasis is mine.) I don't see where you remove the text watcher before changing the text, so you may be in an infinite loop.
To remove the text watcher use removeTextChangedListener. You can replace the text watcher after making changes.
I'm attempting to create a custom EditText that will implement a delay before executing onTextChanged.
class CustomEditText(context: Context, attributeSet: AttributeSet) : AppCompatEditText(context, attributeSet) {
private var millisDelay: Long = 500
private var timer: Timer? = null
override fun onTextChanged(
text: CharSequence?,
start: Int,
lengthBefore: Int,
lengthAfter: Int
) {
doDelay {
Log.d("somekoder", "Calling onTextChanged after $millisDelay milliseconds")
super.onTextChanged(text, start, lengthBefore, lengthAfter)
}
}
fun setDelay(millisDelay: Long){
this.millisDelay = millisDelay
}
private fun doDelay(then: () -> Unit){
timer?.cancel()
timer = Timer()
// Log.d("somekoder", "Got action. Waiting $millisDelay milliseconds.")
timer?.schedule(timerTask {
then.invoke()
}, millisDelay)
}
}
Here's in my MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
search.setDelay(1000)
search.addTextChangedListener {
Log.d("somekoder", "MainActivity: onTextChanged")
}
}
}
This is what my logs look like:
D/somekoder: MainActivity: onTextChanged
D/somekoder: MainActivity: onTextChanged
D/somekoder: MainActivity: onTextChanged
D/somekoder: MainActivity: onTextChanged
D/somekoder: Calling onTextChanged after 1000 milliseconds
MainActivity onTextChanged gets invoked even though I have a delay in there.
Can someone explain what I'm doing wrong?
Thanks in advance!
with addTextChangedListener() you can add multiple listener not like setOnclickListener where you can set only One listener at a time. As a general api implementation you have two listeners to your view. In your case both implemenation are different. Delay won't affect other listener execution.
onTextChanged method is a protected one which works like an inner listener for TextView subclasses. The default implementation is empty and it gets called when text is changed and after listeners. So it is completely independent of listeners.
If you want to delay listeners (not the text changing) you can do something like this. But I don't recommend it because I think it can cause problems especially in afterTextChanged (as it can change the text).
override fun addTextChangedListener(watcher: TextWatcher) {
super.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
doDelay {
watcher.beforeTextChanged(s, start, count, after)
}
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
doDelay {
watcher.onTextChanged(s, start, before, count)
}
}
override fun afterTextChanged(s: Editable?) {
doDelay {
watcher.afterTextChanged(s)
}
}
})
}
I would like to add full text from the editText into an array. But while running this code the array
values will be like first word , second word etc. How to get the full sentence and i want to add it
into an array(Already did it using interface)
Note : The code is placed on the recyclerview adapter ,
Where texBox is the EditText and examinationListener is the interface used
Here i shared the code. Please check it.
textBox.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable) {
textFromBox = s.toString()
examinationListener.addAnswer(textFromBox)
}
override fun beforeTextChanged(s: CharSequence, start: Int,
count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence, start: Int,
before: Int, count: Int) {
}
})
override fun afterTextChanged(s: Editable) {
textFromBox = textBox.text.toString()
examinationListener.addAnswer(textFromBox)
}
I created an array up to the list size. Each items of RecyclerView have an EditText.
val array = arrayOfNulls<String>(listSize)
We need to know the position of EditText to add to the array. Change interface like this:
interface ExaminationListener {
fun addAnswer(text: String, position: Int)
}
function addAnswer:
override fun addAnswer(text: String, position: Int) {
array[position] = text // we add every changes to the array.
}
This use is more useful. We don't need other methods:
textBox.doAfterTextChanged {
examinationListener.addAnswer(it.toString(), position) // you can use adapterPosition if that is in Viewholder
}
We create a sentence from array elements. You can see your full sentence:
val sentence = array.filterNotNull().joinToString (separator = " ") { it -> it }
Log.d("Sentence" , sentence)
Very Simple just add a TextWatcher to your EditText like:
val answerWatcher = object : TextWatcher {
override fun beforeTextChanged(value: CharSequence, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(value: CharSequence, start: Int, before: Int, count: Int) {
when {
// Here value is your full text from your EditText
value.toString().equals("I am a sentence", ignoreCase = true) -> {
}
else -> {
// Just a test condition
}
}
}
override fun afterTextChanged(s: Editable) {
}
}
// Add this TextWatcher to your EditText
edittext.addTextChangedListener(answerWatcher)
I am developing an open source text masker. You can click here to see source code.
My problem is that, when I wrap my custom edit text with TextInputLayout, onTextChanged is being triggered twice only for first letter. Then it works as expected.
This "twice call" breaks my logic. Do you guys have any idea what might be the problem? Since it is being used by other developers, I don't want to fix it with hacky solution. I need to find out the problem.
I set text manually after removing text watcher, then I add text watcher again.
Here is my main logic;
This method is being called only once;
private fun initTextWatcher() {
textWatcher = object : TextWatcher {
override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
masker?.onTextChanged(s, start, count, before)
}
}
addTextChangedListener(textWatcher)
}
And this is how I set my text manually;
private fun setEditTextWithoutTriggerListener(newText: String) {
removeTextChangedListener(textWatcher)
onTextChangedListener?.invoke(newText) // This is a custom listener.
setText(newText)
setSelection(text?.length ?: 0) // This puts cursor at the end of the text.
addTextChangedListener(textWatcher)
}
To handle hint position like TextInputEditText does, I simply copied it's functions into mine.
override fun getHint(): CharSequence? {
val layoutHint = getTextInputLayout()?.hint
return layoutHint ?: super.getHint()
}
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
val ic = super.onCreateInputConnection(outAttrs)
if (ic != null && outAttrs.hintText == null) {
outAttrs.hintText = getHintFromLayout()
}
return ic
}
private fun getTextInputLayout(): TextInputLayout? {
var parent = this.parent
while (parent is View) {
if (parent is TextInputLayout) {
return parent
}
parent = parent.getParent()
}
return null
}
private fun getHintFromLayout(): CharSequence? {
val layout = getTextInputLayout()
return layout?.hint
}
And my class extends AppCompatEditText like TextInputEditText does.
If you are calling addTextChangedListener() from onCreate() or init() move the call into onResume() or any other function called later, otherwise onTextChanged() is triggered before the resume
How do you build a lambda expression for the EditText addTextChangeListener in Kotlin? Below gives an error:
passwordEditText.addTextChangedListener { charSequence ->
try {
password = charSequence.toString()
} catch (error: Throwable) {
raise(error)
}
}
addTextChangedListener() takes a TextWatcher which is an interface with 3 methods. What you wrote would only work if TextWatcher had only 1 method. I'm going to guess the error you're getting relates to your lambda not implementing the other 2 methods. You have 2 options going forward.
Ditch the lambda and just use an anonymous inner class
editText.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
})
Create an extension method so you can use a lambda expression:
fun EditText.afterTextChanged(afterTextChanged: (String) -> Unit) {
this.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
override fun afterTextChanged(editable: Editable?) {
afterTextChanged.invoke(editable.toString())
}
})
}
And then use the extension like so:
editText.afterTextChanged { doSomethingWithText(it) }
Add this core ktx dependence
implementation 'androidx.core:core-ktx:1.0.0'
You simply have to do
passwordEditText.doAfterTextChanged{ }
A bit old, but using Kotlin Android extensions you can do something like that:
editTextRequest.textChangedListener {
afterTextChanged {
// Do something here...
}
}
No extra code needed, just add:
implementation 'androidx.core:core-ktx:1.0.0'
Sorry for being late!
If you add implementation 'androidx.core:core-ktx:1.1.0' to your module's build.gradle file then you can use
etPlayer1.doOnTextChanged { text, start, count, after -> // Do stuff }
Test it :
passwordEditText.addTextChangedListener(object:TextWatcher{
override fun afterTextChanged(s: Editable?) { }
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { }
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { }
})
hope this Kotlin sample help making it clear:
class MainFragment : Fragment() {
private lateinit var viewModel: MainViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.main_fragment, container, false)
view.user.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
}
override fun afterTextChanged(s: Editable) {
userLayout.error =
if (s.length > userLayout.counterMaxLength) {
"Max character length is: ${userLayout.counterMaxLength}"
} else null
}
})
return view
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
// TODO: Use the ViewModel
}
}
With this XML layout:
<android.support.design.widget.TextInputLayout
android:id="#+id/userLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:counterMaxLength="5"
app:counterEnabled="true"
android:hint="user_name">
<android.support.design.widget.TextInputEditText
android:id="#+id/user"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.design.widget.TextInputLayout>
And this Gradle:
android {
compileSdkVersion 'android-P'
...
}
api 'com.android.support:design:28.0.0-alpha1'
implementation 'com.android.support:appcompat-v7:28.0.0-alpha1' // appcompat library
In case you're using Material Filled text field or Outlined text field, attempt to respond to input text change as mentioned by documentation, respectively:
filledTextField.editText?.doOnTextChanged { inputText, _, _, _ ->
// Respond to input text change
}
and
outlinedTextField.editText?.doOnTextChanged { inputText, _, _, _ ->
// Respond to input text change
}
if you use implementation 'androidx.core:core-ktx:1.1.0-alpha05' you can use
For android.widget.TextView
TextWatcher
TextView.doBeforeTextChanged(crossinline action: (text: CharSequence?, start: Int, count: Int, after: Int) -> Unit)
Add an action which will be invoked before the text changed.
TextWatcher
TextView.doOnTextChanged(crossinline action: (text: CharSequence?, start: Int, count: Int, after: Int) -> Unit)
Add an action which will be invoked when the text is changing.
TextWatcher
TextView.doAfterTextChanged(crossinline action: (text: Editable?) -> Unit)
https://developer.android.com/reference/kotlin/androidx/core/widget/package-summary#extension-functions
Add the core ktx dependency
implementation 'androidx.core:core-ktx:1.3.0'
And you can simply implement like this
edit_text.addTextChangedListener { it: Editable? ->
// Do your stuff here
}
Another alternative is the KAndroid library -
implementation 'com.pawegio.kandroid:kandroid:0.8.7#aar'
Then you could do something like this...
editText.textWatcher { afterTextChanged { doSomething() } }
Obviously it is excessive to use an entire library to solve your problem, but it also comes with a range of other useful extensions that eliminate boilerplate code in the Android SDK.
You can make use of kotlin's named parameters:
private val beforeTextChangedStub: (CharSequence, Int, Int, Int) -> Unit = { _, _, _, _ -> }
private val onTextChangedStub: (CharSequence, Int, Int, Int) -> Unit = { _, _, _, _ -> }
private val afterTextChangedStub: (Editable) -> Unit = {}
fun EditText.addChangedListener(
beforeTextChanged: (CharSequence, Int, Int, Int) -> Unit = beforeTextChangedStub,
onTextChanged: (CharSequence, Int, Int, Int) -> Unit = onTextChangedStub,
afterTextChanged: (Editable) -> Unit = afterTextChangedStub
) = addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
beforeTextChanged(charSequence, i, i1, i2)
}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
onTextChanged(charSequence, i, i1, i2)
}
override fun afterTextChanged(editable: Editable) {
afterTextChanged(editable)
}
})
This is the lambda function with edit text field with TextWatcher
searchField.addTextChangedListener(
afterTextChanged = {
},
onTextChanged = {s, start, before, count->
TODO("DO your code")
},
beforeTextChanged = {s, start, before, count->
TODO("DO your code")
}
)
This looks neat:
passwordEditText.setOnEditorActionListener {
textView, keyCode, keyEvent ->
val DONE = 6
if (keyCode == DONE) {
// your code here
}
false
}