Data Binding onFocusChanged Listener (and other non-onclick listeners)

Whilst continuing development for my app, Shopshop Shopping List, I encountered a problem.

I have list items on my shopping list, their names are EditTexts that you can change directly from the list. When focus is lost (i.e. when the user clicks out of the EditText), they must be saved, and the list must be refreshed.

I could’ve done this by adding an onFocusChangedListener to the view in my adapter’s bind method, but I wanted to keep as little interaction logic in the adapter as possible. In short, I wanted to accomplish this using data binding.

override fun onNameChanged(editText: EditText, shopItem: ShopItem) {
    updateItem(shopItem, shopItem.withName(editText.text.toString()))
}

To update the item in the ViewModel, I need to pass in the EditText (so I can get the newly changed text), and the shopItem (which I can use to find and replace the item in the list).

Now if you’ve tried it, you know you can’t add onFocusChanged in xml. In fact, the only listener you can add is onClick.

 

The Solution

This solution will be specifically for adding an onFocusLost binding adapter.

interface OnFocusLostListener {
  fun onFocusLost(view: EditText)
}

First, create an interface with a function that takes in an EditText (or whichever view you plan on doing this with.View works as well if you need it with multiple types).

@BindingAdapter("app:onFocusLost")
fun EditText.onFocusLost(callback: OnFocusLostListener) {
    setOnFocusChangeListener { _, hasFocus ->
        if (!hasFocus) { callback.onFocusLost(this) }
    }
}

Next, create a binding adapter that adds a FocusChangedListener and calls onFocusLost if the focus changes and hasFocus is false.

<layout>

    <data>
        <variable
            name="handler"
            type="com.ericdecanini.shopshopshoppinglist.mvvm.fragment.list.ShopItemEventHandler" />
        <variable
            name="viewstate"
            type="com.ericdecanini.shopshopshoppinglist.entities.ShopItem" />
    </data>

    <EditText
        android:id="@+id/name"
        android:text="@{viewstate.name}"
        app:onFocusLost="@{(view) -> handler.onNameChanged(view, viewstate)}" />

</layout>

The binding adapter lets us use app:onFocusLost with the EditText view itself passed as a parameter. This will allow us to call the function in our handler and pass in the view, as well as the ShopItem (named viewstate here) as its also a variable in our layout.

class ListViewModel @Inject constructor() : ViewModel(), ShopItemEventHandler {

    override fun onNameChanged(editText: EditText, shopItem: ShopItem) {
      updateItem(shopItem, shopItem.withName(editText.text.toString()))
    }
}

Of course, our ViewModel implements our handler which is just an interface with all the interaction functions we need our layout to call. It overrides the onNameChanged function defined in the handler interface and provides its implementation.

And that should be our finished solution for doing an onFocusLost binding adapter. This can of course be UI listener event that isn’t readily available in XML. Another one I’ve done this same pattern with is onChecked for checkboxes.

Disclaimer: I’m still fairly new to data binding as a whole. I’ve only been doing it for a few months, and not very extensively. I may not know if this pattern is common knowledge, or if there even is a much better solution. But I did scramble my brain for a couple days trying to figure out a way to call a ViewModel function like this from the layout binding and pass the view. It’s easy enough with onClick, but adding other events was a bit harder.

Anyway, I hope this finds its way to help at least one other dev who was struggling like I was. Nonetheless, happy coding ༼ つ ◕_◕ ༽つ

 

 

Subscribe to the Newsletter