androidx 与 Spinner 和自定义对象的数据绑定(bind)

标签 android android-spinner android-databinding android-viewmodel

如何使用 androidx 数据绑定(bind)库将自定义对象列表填充到 Spinner (app:entries)?以及如何为 Spinner (app:onItemSelected) 创建一个合适的选择回调?

我的布局:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data>

    <variable
        name="viewModel"
        type=".ui.editentry.EditEntryViewModel" />
</data>

<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.editentry.EditEntryActivity">

        <Spinner
            android:id="@+id/spClubs"
            android:layout_width="368dp"
            android:layout_height="25dp"
            app:entries="@{viewModel.projects}"
            app:onItemSelected="@{viewModel.selectedProject}"
             />

</FrameLayout>

</layout>

EditEntryViewModel.kt

class EditEntryViewModel(repository: Repository) : ViewModel() {

    /** BIND SPINNER DATA TO THESE PROJECTS **/
    val projects : List<Project> = repository.getProjects()

    /** BIND SELECTED PROJECT TO THIS VARIABLE **/
    val selectedProject: Project;
}

项目.kt

data class Project(
    var id: Int? = null,
    var name: String = "",
    var createdAt: String = "",
    var updatedAt: String = ""
)

Spinner 应该显示每个项目的名称,当我选择一个项目时,它应该保存在 viewModel.selectedProject 中。 LiveData 的使用是可选的。

我想我必须为 app:entries 编写一个@BindingAdapter,为 app:onItemSelected 编写一个@InverseBindingAdapter。但是如果不为 Spinneradapter 编写通常的样板代码,我无法弄清楚如何实现它们...

最佳答案

好的,我想出了一个合适的解决方案。这是带有一些解释的代码:

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绑定(bind)到 val projects: List<Project>在我的 ViewModel 中

app:selectedProject绑定(bind)到 val entry: Entry这是一个具有 Project 的类作为属性(property)。

所以这是我的 ViewModel 的一部分:

class EditEntryViewModel() : ViewModel() {
    var entry: MutableLiveData<Entry> = MutableLiveData()
    var projects : List<Project> = repository.getProjects()
}

现在缺少的是 BindingAdapter 和 InverseBindingAdapter 来实现以下事情:

  1. Spinner 应该列出来自存储库的所有项目
  2. Spinner 应该预先选择当前选择的项目 entry
  3. 选择新项目时,应将其设置为 entry自动

绑定(bind)适配器

    /**
     * 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 values. 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
    }
}

现在我们有了一个保存整个项目信息的 Spinner,InverseBindingAdapter 就很简单了。它用于告诉 DataBinding 库它应该从 UI 设置什么值到实际的类属性 viewModel.entry.project :

反向绑定(bind)适配器

    @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
}

关于androidx 与 Spinner 和自定义对象的数据绑定(bind),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53355125/

相关文章:

android - 在错误之间不断跳动,找不到 Spinner/Adapter 的良好修复

android - 让函数返回一个 View (即 RecyclerView、TextView)而不是 id 来查找该 View (int)是否是一种不好的做法?

c# - 如何将数据绑定(bind)到 xamarin Android 中的 ListView ?

使用 Gradle Build Flavors 进行 Android Auto 设置

java - Android 我可以在 mediastore 查询中使用 JOIN

android - 如何在微调器中自定义对话框大小

Android 自定义微调器未填充

android - 如何通过数据绑定(bind)将其他布局 View 的内容传递到 onclick 中的演示者?

android - 具有多个参数的数据绑定(bind)绑定(bind)适配器不起作用

android - RecyclerView onClick 总是返回 "No view found for id"