Can I send extension function through function parameter - android

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.

Related

How to pass a string-array from the function to the activity in android kotlin?

I am trying to pass a string-array from function to activity. I want to use that array into the spinner. But when I am trying to do this, I am passing an array but the function wants to pass a array of type Int. But in activity array of Array<String> type is required.
here is the code of the activity:-
fun selectingDistrictLocation() {
val (districtArray: Int, districtVisibility: Int, stateWarningVisibility: Int, recyclerViewVisibility: Int) = selectedState(SelectedStateName)
district_location.visibility = districtVisibility
state_warning.visibility = stateWarningVisibility
val locationDistrictAdapter = ArrayAdapter(
this,
R.layout.support_simple_spinner_dropdown_item,
districtArray
)
districts_spinner_location.adapter = locationDistrictAdapter
districts_spinner_location.onItemSelectedListener =
object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
adapterViewDistrict: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
DistrictSelectedName =
adapterViewDistrict?.getItemAtPosition(position).toString()
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
}
here is function code:-
object RegionSelectionFunctions {
fun selectedStateForSmallRegions(state: String): SelectingDistrictLocationArrayForSmallRegions {
when (state) {
"-- Select State --" -> return SelectingDistrictLocationArrayForSmallRegions(R.array.empty,View.GONE, View.VISIBLE, View.GONE)
}
}
here is the code of the data class:-
data class SelectingDistrictLocationArray(
val DistrictList: Array<String>,
val districtVisibility: Int,
val stateWarningVisibility: Int,
val recyclerViewVisibility: Int) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SelectingDistrictLocationArray
if (!DistrictList.contentEquals(other.DistrictList)) return false
return true
}
override fun hashCode(): Int {
return DistrictList.contentHashCode()
}
}
I have tried all the possible ways I know to solve this. Can someone please help me?
It is not possible to convert java.lang.Integer cannot be cast to java.lang.String[].
Refer to this question.
For doing the same thing you can pass the string as a value for each case. Then compare that case in the activity. Assign the values of the arrays in the activity.

How to pass a parameter to a extension function in Kotlin

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

Hide text input layout's error after editing the field's text

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

error: <identifier> expecte when setting up binding adapters for two way data binding

I am trying to setup two way binding for TextInputEditText with Float variable.
Here is the entire content of my DataBindingAdapters class.
object DataBindingAdapters {
#BindingAdapter("android:text")
#JvmStatic
fun setText(view: TextInputEditText, value: Float) {
if(value != view.text.toString().toFloat()) {
view.setText(value.toString())
}
}
#InverseBindingAdapter(attribute = "android:text")
fun getText(view: TextInputEditText): Float {
return view.text.toString().toFloat()
}
}
But I am getting:
error: <identifier> expected
float callbackArg_0 = mBindingComponent.null.getText(inputFieldSize);
What am i missing?
You miss one more binding adapter, as mentioned in the InverseBindingAdapter documentation:
#BindingAdapter(value = [ "android:textAttrChanged"], requireAll = false)
fun setTextWatcher( view: TextInputEditText, textAttrChanged: InverseBindingListener?) {
val newValue = object: TextWatcher {
override fun afterTextChanged(s: Editable?) = Unit
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged( s:CharSequence, start:Int, before:Int, count:Int) {
textAttrChanged?.onChange()
}
}
val oldValue = ListenerUtil.trackListener(view, newValue, R.id.textWatcher)
if (oldValue != null) {
view.removeTextChangedListener(oldValue)
}
view.addTextChangedListener(newValue)
}
When you create #InverseBindingAdapter(attribute = "android:text"), an event android:textAttrChanged is created, and you have to add a binding adapter for it.
Calling textAttrChanged?.onChange() will trigger the InverseBindingAdapter that you created.
EDIT:
Also, you are using Kotlin so you don't need to put your binding adapters in an object. A separate file will suffice. Just remove wrapping object DataBindingAdapters {..} and #JvmStatic and you should be fine.
I have the same issue,
the fix is:
class MyView {
companion object {
#BindingAdapter("android:text")
#JvmStatic
fun setText(view: TextInputEditText, value: Float) {
if(value != view.text.toString().toFloat()) {
view.setText(value.toString())
}
}
#InverseBindingAdapter(attribute = "android:text")
fun getText(view: TextInputEditText): Float {
return view.text.toString().toFloat()
}
}
}
Besides this, you may/have to add textAttrChanged event also. If it needs then it will tell you xxx: textAttrChanged not found...... error. Then add below method just side the companion object body
#BindingAdapter(value = ["android:textAttrChanged"], requireAll = false)
#JvmStatic fun setValue(view: MyView, textAttrChanged: InverseBindingListener) {
val newValue = 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) {
textAttrChanged.onChange()
}
}
view.binding.editText.addTextChangedListener(newValue)
}
Besides this, you might get warnings in above code.
Then you can use:
object XXXAdapter {
//Put the binding methods here.
}
to silence the warnings.
I had the same issue and was able to solve it by making the inverse binding adapter static like in the official documentation:
#InverseBindingAdapter(attribute = "android:text")
#JvmStatic fun getText(view: TextInputEditText): Float {
return view.text.toString().toFloat()
}
Note the #JvmStatic right before fun getText(.... This is the only change from the code in your question.
BTW: Mohru's answer also mentions this in the edit-section, but at least for me it was not obvious enough to be found in the first place ;)

How can i validate multiple editText fields and enable button?

I am trying to figure out how to validate 6 EditText input fields and enable button
button_step_one_next_FSF.isEnabled = true
when everything fits my condition. I want to validate everything using this util class without creating TextWatcher object.
Here is my editText util class
inline fun EditText.onTextChange(crossinline f: (s: CharSequence?) -> Unit) {
val listener = object : TextWatcher {
override fun onTextChanged(s: CharSequence, start: Int,
before: Int, count: Int) {
f(s)
}
override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
}
addTextChangedListener(listener)
}
Here is short validation method example
private fun validateInput() {
edit_text_name.onTextChange { s ->
val name: String = s?.toString() ?: ""
if (!name.isNameNotValid()) {
text_input_name.isEndIconVisible = true
text_input_name.isErrorEnabled = false
} else {
text_input_name.error = getString(R.string.error_not_valid_name)
text_input_name.isEndIconVisible = false
}
}
edit_text_surname.onTextChange { s ->
val surname: String = s?.toString() ?: ""
if (!surname.isNameNotValid()) {
text_input_surname.isEndIconVisible = true
text_input_surname.isErrorEnabled = false
} else {
text_input_surname.error = getString(R.string.error_not_valid_surname)
text_input_surname.isEndIconVisible = false
}
}
I just added this method checkButtonEnableState() at the end of each validation in TextWatcher lambda expression and it solved my problem!
private fun checkButtonEnableState() {
button_step_one_next_FSF.isEnabled =
(!edit_text_name.text.toString().isNameNotValid()
&& !edit_text_surname.text.toString().isNameNotValid()
&& edit_text_password_FSF.text.toString().isValidPassword()
&& edit_text_password_confirm_FSF.text.toString().isValidPassword()) &&
(edit_text_password_confirm_FSF.text.toString() == edit_text_password_FSF.text.toString())
}

Categories

Resources