0

i have a spinner defined in the xml like this

<Spinner
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:id="@+id/expense_category"
        app:sourceData="@{()->createExpenseViewModel.getAllSourceItems(1)}"
        app:layout_constraintStart_toStartOf="@+id/textView"
        android:layout_marginTop="20dp"
        app:layout_constraintTop_toBottomOf="@+id/textView" app:layout_constraintWidth_percent="0.7"
/>

createExpenseViewModel.getAllSourceItems(1) this method returns LiveData <List<Source>>, so i have written a binding adapter for that case

@BindingAdapter("app:sourceData")
fun setSourceData(spinner: Spinner, sourceList: List<Source>) {

    val categoryItems = ArrayList<String>()
    categoryItems.addAll(sourceList.map { it.sourceName })
    val spinnerAdapter =
        ArrayAdapter<String>(spinner.context, R.layout.simple_spinner_dropdown_item, categoryItems)
    spinner.adapter = spinnerAdapter


}

when building the app, i am getting the following error, ****/ data binding error ****msg:Cannot find the proper callback class for app:sourceData. Tried java.util.List but it has 25 abstract methods, should have 1 abstract methods. file:/home/naveen/Desktop/project-expense/app/src/main/res/layout/activity_create_expense.xml loc:94:34 - 94:80 ****\ data binding error ****

what does this error actually mean,how to resolve this error?

Edit:

what i intend to do is get the list returned by live data and convert to type ArrayList , i need my binding adapter to be triggered once the livedata returns the list, but if i use this app:sourceData="@{createExpenseViewModel.getAllSourceItems(1)}" and set the binding adapter, the adapter get only null list

Naveen
  • 769
  • 1
  • 9
  • 28
  • one more thing to your question. If you cannot understand the error messages of databinding stuff, I recommend digging into the generated databinding code. You can navigate it easily with AndroidStudio. It's actually not hard to understand what is going on under the hood. Plus it gives you insights of how data binding works. – muetzenflo Apr 21 '19 at 11:13

2 Answers2

5

You are binding a method to app:sourceData, but you're expecting a variable for it in your binding adapter. That cannot work. I guess you want to populate the List into the Spinner. For that I would create a property in your viewModel and bind this property in the xml. I did just that in an app where I had a list of projects to display in the Spinner. Here's the code including the InverseBindingAdapter to automatically save the selected Project in another variable of the ViewModel.

ViewModel:

// getProjects() returns the LiveData
val projects = metaDataRepository.getProjects() 

// use _selectedProject only within ViewModel. Do not expose MediatorLiveData to UI.
// in UI observe selectedProject
private val _selectedProject = MediatorLiveData<Project>()
val selectedProject: LiveData<Project>
    get() = _selectedProject 

Layout XML:

<Spinner
    android:id="@+id/spProjects"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:projects="@{viewModel.projects}"
    app:selectedProject="@={viewModel.selectedProject}" />

BindingAdapter (to populate data from the viewModel into the UI):

/**
 * fill the Spinner with all available projects.
 * Set the Spinner selection to selectedProject.
 * If the selection changes, call the InverseBindingAdapter
 */
@BindingAdapter(value = ["projects", "selectedProject", "selectedProjectAttrChanged"], requireAll = false)
fun setProjects(spinner: Spinner, projects: List<Project>?, selectedProject: Project, listener: InverseBindingListener) {
    if (projects == null) return
    spinner.adapter = ProjectAdapter(spinner.context, android.R.layout.simple_spinner_dropdown_item, projects)
    setCurrentSelection(spinner, selectedProject)
    setSpinnerListener(spinner, listener)
}

Helper Methods for BindingAdapter:

private fun setSpinnerListener(spinner: Spinner, listener: InverseBindingListener) {
    spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
        override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) = listener.onChange()
        override fun onNothingSelected(adapterView: AdapterView<*>) = listener.onChange()
    }
}

private fun setCurrentSelection(spinner: Spinner, selectedItem: Project?): Boolean {
    if (selectedItem == null) {
        return false
    }

    for (index in 0 until spinner.adapter.count) {
        val currentItem = spinner.getItemAtPosition(index) as Project
        if (currentItem.name == selectedItem.name) {
            spinner.setSelection(index)
            return true
        }
    }

    return false
}

Simple Adapter for your Spinner. Change this to your needs:

/**
 * Adapter for displaying the name-field of an Project in a Spinner
 */
class ProjectAdapter(context: Context, textViewResourceId: Int, private val values: List<Project>) : ArrayAdapter<Project>(context, textViewResourceId, values) {

    override fun getCount() = values.size
    override fun getItem(position: Int) = values[position]
    override fun getItemId(position: Int) = position.toLong()

    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        val label = super.getView(position, convertView, parent) as TextView
        label.text = values[position].name
        return label
    }

    override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
        val label = super.getDropDownView(position, convertView, parent) as TextView
        label.text = values[position].name
        return label
    }
}

InverseBindingAdapter (to store the selected Spinner item in the viewModel)

/**
 * get the selected projectName and use it to return a
 * Project which is then used to set appEntry.value.project
 */
@InverseBindingAdapter(attribute = "selectedProject")
fun getSelectedProject(spinner: Spinner): Project {
    return spinner.selectedItem as Project
}
muetzenflo
  • 5,653
  • 4
  • 41
  • 82
  • "You are binding a method to app:sourceData, but you're expecting a variable for it in your binding adapter.", that method returns a result of type LiveData> – Naveen Apr 21 '19 at 11:21
  • what i intend to do is get the list returned by live data and convert to type ArrayList , i need my binding adapter to be triggered once the livedata returns the list, but if i use this app:sourceData="@{createExpenseViewModel.getAllSourceItems(1)}" and set the binding adapter, the adapter get only null list – Naveen Apr 21 '19 at 11:40
  • what does this expression actually means? () -> – Naveen Apr 21 '19 at 11:41
  • this is difficult to explain in a comment. This type of data binding is usually used for callbacks. For example `android:onClick="@{() -> viewModel.doSomething()}"`. So on every click the method `doSomething()` in your viewModel is called. This means that you are referencing the method itself...not the data that is has as return type. You could say this is METHOD-binding. But you want to bind DATA. That's why you have to reference the LIST OF DATA instead of the method that is fetching the data. – muetzenflo Apr 21 '19 at 14:50
  • even when i call @{viewModel.doSomething()} and set up the binding adapter the binding adapter is called, but only problem in that case is the method returns null – Naveen Apr 21 '19 at 23:40
  • even if you access a property, you are indirectly calling get() method of that property, what difference does it make? – Naveen Apr 21 '19 at 23:44
1

I have followed the core idea of what @muetzenflo suggested, i have created a property on view model like this

class MainViewModel @Inject constructor(

    val expenseSourceItems:LiveData<List<Source>> = getAllSourceItems(1)

        fun getAllSourceItems(sourceType:Int?): LiveData<List<Source>> {
        val res = sourceRepository.getAllSourceItems(sourceType)
        return res
    }

    // the methods below are omitted for brevity


}

then i have bound to the spinner using property access syntax

<Spinner
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:id="@+id/expense_category"
        app:sourceData="@{createExpenseViewModel.expenseSourceItems}"
        app:layout_constraintStart_toStartOf="@+id/textView"
        android:layout_marginTop="20dp"
        app:layout_constraintTop_toBottomOf="@+id/textView" app:layout_constraintWidth_percent="0.7"
/>

and then used the same binding adapter

@BindingAdapter("app:sourceData")
fun setSourceData(spinner: Spinner, sourceList: List<Source>) {

    val categoryItems = ArrayList<String>()
    categoryItems.addAll(sourceList.map { it.sourceName })
    val spinnerAdapter =
        ArrayAdapter<String>(spinner.context, R.layout.simple_spinner_dropdown_item, categoryItems)
    spinner.adapter = spinnerAdapter


}

for live data calling a method inside data binding only works for callbacks like onclick, and property access is needed to be used for normal data binding like populating a spinner.

Naveen
  • 769
  • 1
  • 9
  • 28
  • Maybe you can help me in this post: https://stackoverflow.com/questions/57285153/how-to-bind-a-view-model-variable-with-custom-class-type – Matan Marciano Jul 31 '19 at 08:09