Kotlin: Idiomatic way to check for multiple boolean functions - android

I am trying to validate a form by calling the same functions multiple times. My goal is that the isFormValid function should "wait" till all functions are called and then return the boolean.
My current solution works but it looks really od. Ain't there a better way?
FormValidator Class
class FormValidator(private val context: Context) {
// some strings
private fun String.isValidEmail() = android.util.Patterns.EMAIL_ADDRESS.matcher(this).matches()
fun validateNormalET(editText: MutableLiveData<String>, editTextEM: MutableLiveData<String>): Boolean {
if (editText.value.isNullOrEmpty()) {
editTextEM.value = emptyFieldError
return false
}
return true
}
fun validateMinLengthET(editText: MutableLiveData<String>, editTextEM: MutableLiveData<String>, minLength: Int): Boolean {
val errorMessage = when {
minLength < 5 -> postCodeTooFewChar
minLength < 7 -> btNumberTooFewChar
else -> "Error"
}
if (editText.value.isNullOrEmpty()) {
editTextEM.value = emptyFieldError
return false
} else if (editText.value.toString().length < minLength) {
editTextEM.value = errorMessage
return false
}
return true
}
fun validateEmail(editText: MutableLiveData<String>, editTextEM: MutableLiveData<String>): Boolean {
if (editText.value.isNullOrEmpty()) {
editTextEM.value = emptyFieldError
return false
} else if (!editText.value.toString().isValidEmail()) {
editTextEM.value = emailNotValidError
return false
}
return true
}
Current isFormValid Function
fun isFormValid(): Boolean =
formValidator.validateMinLengthET(btNumber, btNumberEM, 7) and
formValidator.validateNormalET(etFirstName, etFirstNameEM) and
formValidator.validateNormalET(etLastName, etLastNameEM) and
formValidator.validateEmail(etEmail, etEmailEM) and
formValidator.validateMinLengthET(etPostCode, etPostCodeEM, 5) and
formValidator.validateNormalET(etCity, etCityEM) and
formValidator.validateNormalET(etStreet, etStreetEM) and
formValidator.validateNormalET(etHouseNumber, etHouseNumberEM)
I appreciate every help, thank you. If there was already a question like this, then I am sorry that I opened another one..

You could use a list with its .all implementation:
fun isFormValid(): Boolean = listOf(
formValidator.validateMinLengthET(btNumber, btNumberEM, 7),
formValidator.validateNormalET(etFirstName, etFirstNameEM),
formValidator.validateNormalET(etLastName, etLastNameEM),
formValidator.validateEmail(etEmail, etEmailEM),
formValidator.validateMinLengthET(etPostCode, etPostCodeEM, 5),
formValidator.validateNormalET(etCity, etCityEM),
formValidator.validateNormalET(etStreet, etStreetEM),
formValidator.validateNormalET(etHouseNumber, etHouseNumberEM)
)
.all { it }
That way you're following the Open/Closed Principle.
If you want to make it slightly shorter, use with(formValidator) {}-scope like #iknow posted in the comment.
EDIT:
If you want it to use as little resources as possible, you could convert the list type to a boolean producer: () -> Boolean
fun isFormValid(): Boolean = listOf(
{ formValidator.validateMinLengthET(btNumber, btNumberEM, 7) },
{ formValidator.validateNormalET(etFirstName, etFirstNameEM) },
...
)
.all { it() }

Related

Avoid duplicate call in LaunchEffect with multiple key in jetpack compose

I want to avoid multiple function call when LaunchEffect key triggers.
LaunchedEffect(key1 = isEnableState, key2 = viewModel.uiState) {
viewModel.scanState(bluetoothAdapter)
}
when first composition isEnableState and viewModel.uiState both will trigger twice and call viewModel.scanState(bluetoothAdapter).
isEnableState is a Boolean type and viewModel.uiState is sealed class of UI types.
var uiState by mutableStateOf<UIState>(UIState.Initial)
private set
var isEnableState by mutableStateOf(false)
private set
So how can we handle idiomatic way to avoid duplicate calls?
Thanks
UPDATE
ContentStateful
#Composable
fun ContentStateful(
context: Context = LocalContext.current,
viewModel: ContentViewModel = koinViewModel(),
) {
LaunchedEffect(key1 = viewModel.isEnableState, key2 = viewModel.uiState) {
viewModel.scanState(bluetoothAdapter)
}
LaunchedEffect(viewModel.previous) {
viewModel.changeDeviceSate()
}
ContentStateLess{
viewModel.isEnableState = false
}
}
ContentStateLess
#Composable
fun ContentStateLess(changeAction: () -> Unit) {
Button(onClick = { changeAction() }) {
Text(text = "Click On me")
}
}
ContentViewModel
class ContentViewModel : BaseViewModel() {
var uiState by mutableStateOf<UIState>(UIState.Initial)
var isEnableState by mutableStateOf(false)
fun scanState(bluetoothAdapter: BluetoothAdapter) {
if (isEnableState && isInitialOrScanningUiState()) {
// start scanning
} else {
// stop scanning
}
}
private fun isInitialOrScanningUiState(): Boolean {
return (uiState == UIState.Initial || uiState == UIState.ScanningDevice)
}
fun changeDeviceSate() {
if (previous == BOND_NONE && newState == BONDING) {
uiState = UIState.LoadingState
} else if (previous == BONDING && newState == BONDED) {
uiState = UIState.ConnectedState(it)
} else {
uiState = UIState.ConnectionFailedState
}
}
}
scanState function is start and stop scanning of devices.
I guess the answer below would work or might require some modification to work but logic for preventing double clicks can be used only if you wish to prevent actions happen initially within time frame of small interval. To prevent double clicks you you set current time and check again if the time is above threshold to invoke click callback. In your situation also adding states with delay might solve the issue.
IDLE, BUSY, READY
var launchState by remember {mutableStateOf(IDLE)}
LaunchedEffect(key1 = isEnableState, key2 = viewModel.uiState) {
if(launchState != BUSY){
viewModel.scanState(bluetoothAdapter)
if(launchState == IDLE){ launchState = BUSY)
}
}
LaunchedEffect(launchState) {
if(launchState == BUSY){
delay(50)
launchState = READY
}
}

How to detect when an EditText is empty using RxTextView (RxBinding)

I'm doing validation on an EditText. I want the CharSequence to be invalid if it's empty or it doesn't begin with "https://". I'm also using RxBinding, specifically RxTextView. The problem is that when there is one character left, and I then delete it leaving no characters left in the the CharSequence the map operator doesn't fire off an emission. In other words I want my map operator to return false when the EditText is empty. I'm beginning to think this may not be possible the way I'm doing it. What would be an alternative?
Here is my Observable / Disposable:
val systemIdDisposable = RxTextView.textChanges(binding.etSystemId)
.skipInitialValue()
.map { charSeq ->
if (charSeq.isEmpty()) {
false
} else {
viewModel.isSystemIdValid(charSeq.toString())
}
}
.subscribe { isValid ->
if (!isValid) {
binding.systemIdTextInputLayout.isErrorEnabled = true
binding.systemIdTextInputLayout.error = viewModel.authErrorFields.value?.systemId
} else {
binding.systemIdTextInputLayout.isErrorEnabled = false
binding.systemIdTextInputLayout.error = viewModel.authErrorFields.value?.systemId
}
}
And here is a function in my ViewModel that I pass the CharSequence to for validation:
fun isSystemIdValid(systemId: String?): Boolean {
return if (systemId != null && systemId.isNotEmpty()) {
_authErrors.value?.systemId = null
true
} else {
_authErrors.value?.systemId =
getApplication<Application>().resources.getString(R.string.field_empty_error)
false
}
}
After sleeping on it, I figured it out.
I changed RxTextView.textChanges to RxTextView.textChangeEvents. This allowed me to query the CharSequence's text value (using text() method provided by textChangeEvents) even if it's empty. Due to some other changes (not really relevant to what I was asking in this question) I was also able to reduce some of the conditional code too. I'm just putting that out there in case someone comes across this and is curious about these changes. The takeaway is that you can get that empty emission using RxTextView.textChangeEvents.
Here is my new Observer:
val systemIdDisposable = RxTextView.textChangeEvents(binding.etSystemId)
.skipInitialValue()
.map { charSeq -> viewModel.isSystemIdValid(charSeq.text().toString()) }
.subscribe {
binding.systemIdTextInputLayout.error = viewModel.authErrors.value?.systemId
}
And here is my validation code from the ViewModel:
fun isSystemIdValid(systemId: String?): Boolean {
val auth = _authErrors.value
return if (systemId != null && systemId.isNotEmpty()) {
auth?.systemId = null
_authErrors.value = auth
true
} else {
auth?.systemId =
getApplication<Application>().resources.getString(R.string.field_empty_error)
_authErrors.value = auth
false
}
}
Lastly, if anyone is curious about how I'm using my LiveData / MutableLiveData objects; I create a private MutableLiveData object and only expose an immutable LiveData object that returns the values of the first object. I do this for better encapsulation / data hiding. Here is an example:
private val _authErrors: MutableLiveData<AuthErrorFields> by lazy {
MutableLiveData<AuthErrorFields>()
}
val authErrors: LiveData<AuthErrorFields>
get() { return _authErrors }
Hope this helps someone! 🤗

android setOnClickListener multiple buttons at once

I have three buttons with ids b00, b01, b02 that I want to all do the same thing when they are long clicked. Is there a better way to do this than
b00.setOnLongClickListener {
//code
true
}
b01.setOnLongClickListener {
//same code
true
}
b02.setOnLongClickListener {
//same code
true
}
You can do this:
/...
b00.setOnLongClickListener(this)
b01.setOnLongClickListener(this)
b02.setOnLongClickListener(this)
}
//...
override fun onLongClick(v: View?): Boolean {
var id = v?.id
if ((id == b00.id) or (id == b01.id) or (id == b02.id)) {
//your code
return true
}
return false
}
Following your example, I assume you're using Kotlin.
In programming, try to keep it simple, easy to understand and don't repeat yourself.
Since for OnLongClickListener you need to return Boolean that you consumed event, I suggest adding inline function
inline fun consumeEvent(function: () -> Unit): Boolean {
function()
return true
}
Then, move common code from listeners to new function, like fun myFunction() and call it.
fun myFunction() {
// some code
}
b00.setOnLongClickListener { consumeEvent { myFunction() } }
b01.setOnLongClickListener { consumeEvent { myFunction() } }
b02.setOnLongClickListener { consumeEvent { myFunction() } }

Android Data Binding Problem: Missing return statement in generated code while using with a custom view?

I am using data binding in my current application, and so far so good. However, when I tried to use it with a custom data binding adapter I wrote for my custom view, I got an error from auto generated file as the title says, missing return statement. This error does not occur when I used this data binding on only one view, but more than one gives the error. Below are my custom view and adapters, and usage in xml file. I already checked the answer of kinda duplicated question but it doesn't worked in my case, and does not have enough explanation.
class NeonTextView(context: Context, attrs: AttributeSet) : TextView(context, attrs) {
private val drawableClear: Drawable?
get() = ContextCompat.getDrawable(context, R.drawable.ic_clear)
lateinit var actionMethod: () -> Unit
lateinit var clearMethod: () -> Unit
var hasActionMethod = false
var hasClearMethod = false
init {
setupAttributes(attrs)
}
private fun setupAttributes(attrs: AttributeSet) {
val typedArray =
context.theme.obtainStyledAttributes(attrs, R.styleable.NeonTextView, 0, 0)
hasActionMethod = typedArray.getBoolean(
R.styleable.NeonTextView_hasActionMethod,
false
)
hasClearMethod = typedArray.getBoolean(
R.styleable.NeonTextView_hasClearMethod,
false
)
typedArray.recycle()
}
override fun onTextChanged(
text: CharSequence?,
start: Int,
lengthBefore: Int,
lengthAfter: Int
) {
text?.let { text ->
drawableClear?.let {
it.setBounds(0, 0, it.intrinsicWidth, it.intrinsicHeight)
}
setCompoundDrawablesWithIntrinsicBounds(
null,
null,
if (text.isNotEmpty()) drawableClear!! else null,
null
)
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
event?.let {
return when (it.action) {
ACTION_DOWN -> return true
ACTION_UP -> {
if (compoundDrawables[2] == null && hasActionMethod) {
actionMethod()
} else {
if (it.x > (width - paddingRight - compoundDrawables[2]!!.intrinsicWidth)) {
if (hasClearMethod) clearMethod()
text = ""
} else {
if (hasActionMethod) actionMethod()
}
}
performClick()
true
}
else -> false
}
}.run {
return false
}
}
override fun performClick(): Boolean {
super.performClick()
return true
}
}
And here is my binding adapters for binding methods that are used inside this custom text view:
#BindingAdapter("actionMethod")
fun NeonTextView.setActionMethod(actionMethod: () -> Unit) {
this.actionMethod = actionMethod
this.hasActionMethod = true
}
#BindingAdapter("clearMethod")
fun NeonTextView.setClearMethod(clearMethod: () -> Unit) {
this.clearMethod = clearMethod
this.hasClearMethod = true
}
And here is how I applied inside xml file:
<com.android.platform.NeonTextView
android:id="#+id/textViewSectionField"
style="#style/HeaderTextView.SubHeader"
app:hasActionMethod="true"
app:actionMethod="#{() -> viewModel.getDepartmentList()}"/>
Any ideas why I am getting error from generated file when I used this binding in more than one view inside xml?
Thanks in advance.
The problem is with Java<->Kotlin compatibility. In Kotlin if you declare function
interface Func {
fun test(): Unit {
}
}
And use it from java
class FuncImpl implements Func {
#Override
public Unit test() {
return Unit.INSTANCE;
}
}
Note, please, that in that case in java code you need return statement. The same is for lambdas.
So when you set up lambda from xml using databinding, it's treated as java's lambda, so generated code wasn't correctly processed in that case.

Sorting Strings that contains number in kotlin

I wanna sort some strings that contain numbers but after a sort, it becomes like this ["s1", "s10", "s11", ... ,"s2", "s21", "s22"]. after i search i fount this question with same problem. but in my example, I have mutableList<myModel>, and I must put all string in myModel.title for example into a mutable list and place into under code:
val sortData = reversedData.sortedBy {
//pattern.matcher(it.title).matches()
Collections.sort(it.title, object : Comparator<String> {
override fun compare(o1: String, o2: String): Int {
return extractInt(o1) - extractInt(o2)
}
fun extractInt(s: String): Int {
val num = s.replace("\\D".toRegex(), "")
// return 0 if no digits found
return if (num.isEmpty()) 0 else Integer.parseInt(num)
}
})
}
I have an error in .sortedBy and Collections.sort(it.title), may please help me to fix this.
you can use sortWith instead of sortBy
for example:
class Test(val title:String) {
override fun toString(): String {
return "$title"
}
}
val list = listOf<Test>(Test("s1"), Test("s101"),
Test("s131"), Test("s321"), Test("s23"), Test("s21"), Test("s22"))
val sortData = list.sortedWith( object : Comparator<Test> {
override fun compare(o1: Test, o2: Test): Int {
return extractInt(o1) - extractInt(o2)
}
fun extractInt(s: Test): Int {
val num = s.title.replace("\\D".toRegex(), "")
// return 0 if no digits found
return if (num.isEmpty()) 0 else Integer.parseInt(num)
}
})
will give output:
[s1, s21, s22, s23, s101, s131, s321]
A possible solution based on the data you posted:
sortedBy { "s(\\d+)".toRegex().matchEntire(it)?.groups?.get(1)?.value?.toInt() }
Of course I would move the regex out of the lambda, but it is a more concise answer this way.
A possible solution can be this:
reversedData.toObservable()
.sorted { o1, o2 ->
val pattern = Pattern.compile("\\d+")
val matcher = pattern.matcher(o1.title)
val matcher2 = pattern.matcher(o2.title)
if (matcher.find()) {
matcher2.find()
val o1Num = matcher.group(0).toInt()
val o2Num = matcher2.group(0).toInt()
return#sorted o1Num - o2Num
} else {
return#sorted o1.title?.compareTo(o2.title ?: "") ?: 0
}
}
.toList()
.subscribeBy(
onError = {
it
},
onSuccess = {
reversedData = it
}
)
As you state that you need a MutableList, but don't have one yet, you should use sortedBy or sortedWith (in case you want to work with a comparator) instead and you get just a (new) list out of your current one, e.g.:
val yourMutableSortedList = reversedData.sortedBy {
pattern.find(it)?.value?.toInt() ?: 0
}.toMutableList() // now calling toMutableList only because you said you require one... so why don't just sorting it into a new list and returning a mutable list afterwards?
You may want to take advantage of compareBy (or Javas Comparator.comparing) for sortedWith.
If you just want to sort an existing mutable list use sortWith (or Collections.sort):
reversedData.sortWith(compareBy {
pattern.find(it)?.value?.toInt() ?: 0
})
// or using Java imports:
Collections.sort(reversedData, Compatarator.comparingInt {
pattern.find(it)?.value?.toInt() ?: 0 // what would be the default for non-matching ones?
})
Of course you can also play around with other comparator helpers (e.g. mixing nulls last, or similar), e.g.:
reversedData.sortWith(nullsLast(compareBy {
pattern.find(it)?.value
}))
For the samples above I used the following Regex:
val pattern = """\d+""".toRegex()
I wrote a custom comparator for my JSON sorting. It can be adapted from bare String/Number/Null
fun getComparator(sortBy: String, desc: Boolean = false): Comparator<SearchResource.SearchResult> {
return Comparator { o1, o2 ->
val v1 = getCompValue(o1, sortBy)
val v2 = getCompValue(o2, sortBy)
(if (v1 is Float && v2 is Float) {
v1 - v2
} else if (v1 is String && v2 is String) {
v1.compareTo(v2).toFloat()
} else {
getCompDefault(v1) - getCompDefault(v2)
}).sign.toInt() * (if (desc) -1 else 1)
}
}
private fun getCompValue(o: SearchResource.SearchResult, sortBy: String): Any? {
val sorter = gson.fromJson<JsonObject>(gson.toJson(o))[sortBy]
try {
return sorter.asFloat
} catch (e: ClassCastException) {
try {
return sorter.asString
} catch (e: ClassCastException) {
return null
}
}
}
private fun getCompDefault(v: Any?): Float {
return if (v is Float) v else if (v is String) Float.POSITIVE_INFINITY else Float.NEGATIVE_INFINITY
}

Categories

Resources