0

I need to create multiple editTexts with logic to handle the format of the input. The different editTexts will bind to different variables and save to different properties of viewModel.home. The editTexts will, however, use the same logic (although different for strings, ints, doubles, etc.) and thus I want to avoid copy-pasting code.

For each editText I will have to set up an addTextChangedListener and override the afterTextChanged method (see below).

I want to generalize as much as possible through calling another method (customAfterTextChanged()) from each of the overrides of afterTextChanged:

viewBinding.editTextAdress.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?) {
        customAfterTextChanged()
}})

Some other places in the code I will define the method:

        fun customAfterTextChanged () {
            if (viewBinding.adressEditText.text.toString() != "") {
                viewModel.home.adress = viewBinding.adressEditText.text.toString()
            }
        }

In the code above, home and viewModel are instances of custom classes, where home exists as an instance within viewModel.

My question now is: I want to be able to pass in, for example, viewBinding.cityEditText instead of viewBinding.adressEditText into the method. Or viewModel.home.city instead of viewModel.home.adress. To achieve this I think need to replace viewBinding.editTextAdress and viewModel.home.adress with method-specific variables, and somehow pass in references to for example viewModel.home.city into the variables.

However, I can't wrap my head around how to do it. Can someone edit my code to make it work in this particular instance? I think I would then be able to translate to all the other logic if I can just get one working example.

Ram Fattah
  • 349
  • 2
  • 11
zorigo
  • 9
  • 3
  • You might find some of the extension functions [here](https://stackoverflow.com/questions/40569436/kotlin-addtextchangelistener-lambda) useful for simplifying the TextWatcher duplication in Kotlin – Tyler V Dec 23 '21 at 02:29
  • Thanks. However, I think the link mostly explains the parts I already understand. From what I can see it doesn't help me understand how to pass in references to class instance properties as parameters into the methods or lambdas, only how to put values as parameters. – zorigo Dec 23 '21 at 09:15

1 Answers1

0

There are different ways to accomplish what you are looking for, but you are limited by the fact that you cannot pass in something like a string from your ViewModel "by reference" and modify it like you might in c++. (Relevant discussion). You'll have to use a slightly different pattern in Kotlin/Java.

Option 1 - Use a Callback

fun customAfterTextChanged(et: EditText, onResult: (String)->Unit) {
    val s = et.text.toString()
    if (s.isNotEmpty()) {
        onResult(s)
    }
}

then you can call it like this

customAfterTextChanged(viewBinding.adressEditText) { s ->
    viewModel.home.adress = s
}

customAfterTextChanged(viewBinding.cityEditText) { s ->
    viewModel.home.city = s
}

Option 2 - Move to ViewModel (better)

It is generally better to try to move all your logic to your ViewModel to make it easier to test. If you do that, your view layer would end up looking like this (with no logic - it simply calls through to the ViewModel when the text changes) if using the ktx extensions

viewBinding.adressEditText.doAfterTextChanged { e ->
    e?.let { viewModel.changedAddress(it.toString()) }
}

and in the ViewModel you would have

fun changedAddress(a: String) {
    if( a.isNotEmpty() ) home.adress = a
}

The advantage of this is that it is much easier to unit test your app's behavior. For example, to test that the user deleting the text has the intended effect you would only need something like this (instead of a full UI test)

model.changedAddress("123")
assertEqual("123", model.home.address)
model.changedAddress("")
assertEqual("123", model.home.address)

If your checker logic is more complicated, you can always combine that logic within the ViewModel still like with Option 1 - but it need not have any tie back to an actual View

Tyler V
  • 9,694
  • 3
  • 26
  • 52
  • 1
    Thank you! Your option 1 was helpful enough for me to understand the syntax and pattern. While it didn't fully solve my problem due to the logic being more complex than in the example, I think I have come as far as possible. – zorigo Dec 25 '21 at 23:50