So i have been trying to convert my EditText input that i get from my TextWatcher to an observable of stream but i cannot convert it.
I am trying the following
etSearch.addTextChangedListener(object: TextWatcher{
override fun afterTextChanged(p0: Editable?) {
//I want to create an observable here to send events
Observable.create(e->e.next(p0));
}
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
})
I am not able to figure out where i should create my events so that i am able to subscribe to it.
You can simply create an extension in kotlin which returns a Flowable of EditTextFlow
fun EditText.addTextWatcher(): Flowable<EditTextFlow> {
return Flowable.create<EditTextFlow>({ emitter ->
addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
emitter.onNext(EditTextFlow(p0.toString(), EditTextFlow.Type.BEFORE))
}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
emitter.onNext(EditTextFlow(p0.toString(), EditTextFlow.Type.ON))
}
override fun afterTextChanged(p0: Editable?) {
emitter.onNext(EditTextFlow(p0.toString(), EditTextFlow.Type.AFTER))
}
})
}, BackpressureStrategy.BUFFER)
}
EditTextFlow
data class EditTextFlow(
val query: String,
val type: Type
) {
enum class Type { BEFORE, AFTER, ON }
}
Then use it like this:
etSearch.addTextWatcher()
.filter { it.type == EditTextFlow.Type.AFTER }
.map { it.query }
.flatMap { /*Make any request or anything*/ }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(
onNext = {
// Update UI here
},
onError = {
// Log error
}
)
Actually, there is a library for this.
You can use it as
RxTextView.textChanges(etSearch)
Related
To give a bit of context, I have a spinner and several EditText boxes. "Blankets" is the only editable EditText box. "Level" and "Blankets" can each be changed and should update all the others accordingly.
I've been unable to stop "Level" and "Blankets" from looping infinitely though.
I'm still fairly new to coding so I've been trying to keep things as simple as possible.
//Level Spinner
levelSpinner.adapter = levelArrayAdapter
levelSpinner.onItemSelectedListener = object :
AdapterView.OnItemSelectedListener {
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) {
levelInt = Integer.parseInt(levelSpinner.selectedItem.toString())
println("LEVEL SPINNER VALUE IS " + levelInt)
calculateBlanketsbyLevel()
println("BLANKETS BY LEVEL = " + blanketsInt)
characterBlankets.setText("" + blanketsInt)
calculateTotalCPbyLevel()
println("CP BY LEVEL = " + cpInt)
characterCP.setText("" + cpInt)
calculateFreeCP()
characterFree.setText("" + freeInt)
}
override fun onNothingSelected(p0: AdapterView<*>?) {
}
}
//Blanket Box
characterBlankets.addTextChangedListener(object: TextWatcher {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
override fun afterTextChanged(p0: Editable?) {
val characterBlanketsStr = characterBlankets.editableText.toString()
println("CHARACTER BLANKETS AFTER TEXT CHANGED IS " + characterBlanketsStr)
if (characterBlanketsStr != ""){
blanketsInt = Integer.parseInt(characterBlanketsStr)
calculateLevelbyBlankets()
println("LEVEL BY BLANKETS = " + levelInt)
levelSpinner.setSelection(levelInt)
calculateTotalCPbyLevel()
println("CP BY LEVEL = " + cpInt)
characterCP.setText("" + cpInt)
calculateFreeCP()
characterFree.setText("" + freeInt)
}
else {
}
}
})
Here, I have to show currency decimal separator and thousands separators as per the given input like:
private var decimalSeparator: Char = '.'
private var thousandSeparator: String = ","
fun setDecimalSeparator(decimalSeparator: Char) {
this.decimalSeparator = decimalSeparator
}
fun setThousandSeparator(thousandSeparator: String) {
this.thousandSeparator = thousandSeparator
}
And I have added text watcher for the edit text field to add a respective decimal and thousands separator like this with the help of Add comma as thousands separator for numbers in EditText for Android Studio?
field.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(p0: Editable?) {
Log.d("CharCount:", p0.toString().length.toString())
field.removeTextChangedListener(this)
try {
var givenstring: String = p0.toString()
if (givenstring.contains(thousandSeparator)) {
givenstring = givenstring.replace(thousandSeparator.toRegex(), "")
}
val longVal: Long = givenstring.toLong()
val formatter = DecimalFormat("#$thousandSeparator###$thousandSeparator###")
val formattedString = formatter.format(longVal)
field.setText(formattedString)
field.setSelection(field.text.length)
// to place the cursor at the end of text
} catch (nfe: NumberFormatException) {
nfe.printStackTrace()
} catch (e: Exception) {
e.printStackTrace()
}
field.addTextChangedListener(this)
}
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
// no need any callback for this.
}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
// no need any callback for this.
}
})
this is not working when thousandSeparator is period(.); can anyone help me with this?. Thanks in advance.
Here, this is how it worked out.
I took help from https://docs.oracle.com/javase/tutorial/i18n/format/decimalFormat.html and
Add comma as thousands separator for numbers in EditText for Android Studio?
field.addTextChangedListener(object : TextWatcher {
// https://stackify.dev/354994-add-comma-as-thousands-separator-for-numbers-in-edittext-for-android-studio
override fun afterTextChanged(p0: Editable?) {
field.removeTextChangedListener(this)
try {
var givenstring: String = p0.toString()
if (givenstring.contains(thousandSeparator)) {
givenstring = givenstring.replace(thousandSeparator.toString(), "")
}
val doubleVal: Double = givenstring.toDouble()
// https://docs.oracle.com/javase/tutorial/i18n/format/decimalFormat.html
val unusualSymbols = DecimalFormatSymbols()
unusualSymbols.decimalSeparator = decimalSeparator
unusualSymbols.groupingSeparator = thousandSeparator
val formatter = DecimalFormat("#,##0.##", unusualSymbols)
formatter.groupingSize = 3
val formattedString = formatter.format(doubleVal)
field.setText(formattedString)
field.setSelection(field.text.length)
// to place the cursor at the end of text
} catch (nfe: NumberFormatException) {
nfe.printStackTrace()
} catch (e: Exception) {
e.printStackTrace()
}
field.addTextChangedListener(this)
}
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
// no need any callback for this.
}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
// no need any callback for this.
}
})
i have made custom text watcher that good for number or currency. You can get it here : https://github.com/zihadrizkyef/TextWatcherForMoney
I have an extension function in kotlin to check is it a valid string or not as stated below.
fun EditText.onAfterTextChanged(listener: (String) -> Unit) {
addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(editable: Editable?) {
val input = editable?.toString()
val allowedChars = context.getString(R.string.supported_digits)
val newValue = replaceInvalidCharacters(input, allowedChars)
if (newValue != input) {
setText(newValue)
setSelection(text.length)
}
listener(newValue)
}
override fun beforeTextChanged(s: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
override fun onTextChanged(s: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
})
}
private fun replaceInvalidCharacters(value: String?, allowedChars: String): String {
var finalValue = value ?: ""
if (finalValue.isNotEmpty()) {
val lastChar = finalValue.last()
if (!allowedChars.contains(lastChar, false)) {
finalValue = finalValue.dropLast(1)
}
}
return finalValue
}
I am using it like:
editText.onAfterTextChanged {
val length = it.length
if (length >= 250) {
activity?.toast(getString(R.string.max_limit_reached))
return#onAfterTextChanged
}
}
Here I want to pass allowedChars as a parameter to this extension as there are different strings are there for different EditText's in the application. Like 1 EditText may allow only number's but not +,- and some edit text may allow only alphanumeric, etc. Is there any way to pass a parameter to the extension?
What you can do is update the extension function signature by adding a parameter before the callback function. So, it'll look something like this
fun EditText.onAfterTextChanged(allowedChars: String, listener: (String) -> Unit) {
addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(editable: Editable?) {
val input = editable?.toString()
val allowedChars = context.getString(R.string.supported_digits)
val newValue = replaceInvalidCharacters(input, allowedChars)
if (newValue != input) {
setText(newValue)
setSelection(text.length)
}
listener(newValue)
}
override fun beforeTextChanged(s: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
override fun onTextChanged(s: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
})
}
And you can call it like so:
editText.onAfterTextChanged("123abc") {
val length = it.length
if (length >= 250) {
activity?.toast(getString(R.string.max_limit_reached))
return#onAfterTextChanged
}
}
I'm trying to hide error on a text input layout after the user solves the error but I am not sure how to do that, given that the function afterTextChanged doesn't seem to work with it. How can I do this?
This is the part I'm talking about:
loginViewModel.loginFormState.observe(this, Observer {
val loginState = it ?: return#Observer
// disable login button unless fields are valid
binding.btnLogin.isEnabled = loginState.isDataValid
if (loginState.emailError != null) {
binding.etEmail.error = getString(loginState.emailError)
}
if (loginState.passwordError != null) {
binding.tfPassword.error = getString(loginState.passwordError)
}
})
binding.etEmail.afterTextChanged {
loginViewModel.loginDataChanged(
binding.etEmail.text.toString(),
binding.etPassword.text.toString()
)
}
binding.etPassword.afterTextChanged {
loginViewModel.loginDataChanged(
binding.etEmail.text.toString(),
binding.etPassword.text.toString()
)
}
private fun TextInputEditText.afterTextChanged(afterTextChanged: (String) -> Unit) {
this.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(editable: Editable?) {
afterTextChanged.invoke(editable.toString())
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
})
Even if the user writes the password correctly and they can click on a button I have, the error is still there, how can I make it go away?
I guess your error lies in those lines:
if (loginState.emailError != null) {
binding.etEmail.error = getString(loginState.emailError)
}
Now imagine emailError was "some error" once and now is null. The bindings' text will not be set again, so it's still "some error". To fix this mistake replace it with:
binding.etEmail.error = if(loginState.emailError != null){
getString(loginState.emailError)
} else {
null
}
Do the same for your passwordError
you can use setError method and pass null as argument. from documentation
Sets an error message that will be displayed below our EditText. If
the error is null, the error message will be cleared.
you can call this method in afterTextChanged as
textInputEditText.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?) {
textInputLayout.error = null
}
})
I have some extension functions function below.
fun EditText.setEmailValidationListener(): TextWatcher {
val textWatcher = object : TextWatcher {
override fun beforeTextChanged(text: CharSequence?, start: Int, count: Int, after: Int) { }
override fun onTextChanged(text: CharSequence?, start: Int, before: Int, count: Int) { }
override fun afterTextChanged(text: Editable?) { validateEmail() }
private fun validateEmail(): Boolean {
if (validateEmailFormat(showError = false)) {
getParentInputLayout()?.isErrorEnabled = false
return true
}
return false
}
}
addTextChangedListener(textWatcher)
return textWatcher
}
fun EditText.setPasswordValidationListener(): TextWatcher {
val textWatcher = object : TextWatcher {
override fun beforeTextChanged(text: CharSequence?, start: Int, count: Int, after: Int) { }
override fun onTextChanged(text: CharSequence?, start: Int, before: Int, count: Int) { }
override fun afterTextChanged(text: Editable?) { validateEmpty() }
private fun validatePasswordText(): Boolean {
if (validateEmptyText(showError = false)) {
getParentInputLayout()?.isErrorEnabled = false
return true
}
return false
}
}
addTextChangedListener(textWatcher)
return textWatcher
}
fun EditText.validateEmailFormat(showError: Boolean = true): Boolean
{
// Do something checking the Email
return false
}
fun EditText.validatePasswordText(showError: Boolean = true): Boolean
{
// Do something checking the Password
return false
}
private fun EditText.getParentInputLayout(): TextInputLayout? {
if (parent is TextInputLayout) {
return parent as TextInputLayout
}
return null
}
Both setEmailValidationListener and setPasswordValidationListener are identical, except for the validation function they use respectively i.e. validateEmailFormat and validatePasswordFormat.
So I plan to refactor the two function common code into a common function as below
fun EditText.setupTextChangeListener(validatorFunc : (showError: Boolean) -> Boolean): TextWatcher {
val textWatcher = object : TextWatcher {
override fun beforeTextChanged(text: CharSequence?, start: Int, count: Int, after: Int) { }
override fun onTextChanged(text: CharSequence?, start: Int, before: Int, count: Int) { }
override fun afterTextChanged(text: Editable?) { validateEmpty() }
private fun validateEmpty(): Boolean {
if (validatorFunc(false)) {
getParentInputLayout()?.isErrorEnabled = false
return true
}
return false
}
}
addTextChangedListener(textWatcher)
return textWatcher
}
... where it basically just to send in validationFunc as parameter to it.
However, I can't find any way of sending the EditText.validateEmailFormat and EditText.validatePasswordFormat into the validationFunc function parameter.
How could I achieve that?
Some theory
Signature of extension functions is bit more complicated than in may look at first. The extension needs to have some reference to object of this class to be able to act upon it.
In fact the extension method
fun EditText.validateEmailFormat(showError: Boolean = true): Boolean
after decompiling to plain old java, looks like this:
public static final boolean validateEmailFormat(#NotNull EditText $receiver, boolean showError)
As it's (almost) impossible to change already-compiled Java class. So Kotlin (and quite possibly other languages that have concept of extension methods) uses static methods, with first parameter being receiver of extending class, to make it work.
Back to the business
Your validateEmailFormat is in fact of type EditText.(Boolean) -> Boolean and at the same time is of type (EditText, Boolean) -> Boolean. So you need to do either of two things:
First you can make EditText.setupTextChangeListener accept validatorFunc as EditText.(Boolean) -> Boolean or (EditText, Boolean) -> Boolean instead of (Boolean) -> Boolean.
Or you refrain from extending EditText in fun EditText.validateEmailFormat(Boolean) and make it plain Kotlin function, e.g. something like this fun validateEmailFormat(String, Boolean).
As you are extensively using extension functions, I assume the first option is correct solution for you.
fun EditText.validateEmailFormat() can be passed as EditText::validateEmailFormat.