Okay, I came up with a proper solution. Here's the code with some explanation:
layout.xml
<Spinner
android:id="@+id/spProjects"
android:layout_width="368dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/spActivities"
app:projects="@{viewModel.projects}"
app:selectedProject="@={viewModel.entry.project}" />
app:projects
is bound to val projects: List<Project>
in my ViewModel
app:selectedProject
is bound to val entry: Entry
which is a class having a Project
as property.
So this is part of my ViewModel:
class EditEntryViewModel() : ViewModel() {
var entry: MutableLiveData<Entry> = MutableLiveData()
var projects : List<Project> = repository.getProjects()
}
What's missing now are the BindingAdapter and the InverseBindingAdapter to achieve the following things:
- The Spinner should list all the Projects coming from the repsitory
- The Spinner should pre-select the currently selected Project of
entry
- When a new Project is selected, it should be set to
entry
automatically
BindingAdapter
/**
* 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?, selectedProject: Project, listener: InverseBindingListener) {
if (projects == null) return
spinner.adapter = NameAdapter(spinner.context, android.R.layout.simple_spinner_dropdown_item, projects)
setCurrentSelection(spinner, selectedProject)
setSpinnerListener(spinner, listener)
}
You can place the BindingAdapter in an empty file. It has not to be part of any class.
The important thing are its parameters. They are deducted by the BindingAdapters value
s. In this case the values are projects
, selectedProject
and selectedProjectAttrChanged
. The first two parameters correspond to the two layout-xml attributes that we defined ourselves. The last/third parameter is part of the DataBinding process: For each layout-xml attribute with two-way databining (i.e. @={) a value get generated with the name <attribute-name>AttrChanged
Another important part for this special case is the NameAdapter
which is my own SpinnerAdapter that is able to hold my Projects as items and only display their name
property in the UI. That way we always have access to the whole Project instances instead of only a String (which is usually the case for the default SpinnerAdapter).
Here's the code for my custom Spinner Adapter:
NameAdapter
class NameAdapter(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
}
}
Now that we have a Spinner that holds our whole Project information, the InverseBindingAdapter is easy. It is used to tell the DataBinding library what value it should set from the UI to the actual class property viewModel.entry.project
:
InverseBindingAdapter
@InverseBindingAdapter(attribute = "selectedProject")
fun getSelectedProject(spinner: Spinner): Project {
return spinner.selectedItem as Project
}
That's it. All working smoothly together. One thing to mention is that this approach is not recommended if your List would contain a lot of data, since all this data is stored in the adapter. In my case it's only a bit of String fields, so it should be fine.
For completion, I wanna add the two methods from the 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: HasNameField): Boolean {
for (index in 0 until spinner.adapter.count) {
if (spinner.getItemAtPosition(index) == selectedItem.name) {
spinner.setSelection(index)
return true
}
}
return false
}
android:entries
would be an 1D array ...whileProject
has 4 fields and no.toString()
method. forid
+name
it would take two synchron 1D arrays to populate. – Amphiprostyle