androidx 数据绑定与 Spinner 和自定义对象

mue*_*flo 6 android android-spinner android-databinding android-viewmodel

您如何使用 androidx 数据绑定库用自定义对象列表(app:entries)填充 Spinner ?以及如何为 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>
Run Code Online (Sandbox Code Playgroud)

编辑EntryViewModel.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;
}
Run Code Online (Sandbox Code Playgroud)

项目.kt

data class Project(
    var id: Int? = null,
    var name: String = "",
    var createdAt: String = "",
    var updatedAt: String = ""
)
Run Code Online (Sandbox Code Playgroud)

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

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

mue*_*flo 7

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

布局文件

<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}" />
Run Code Online (Sandbox Code Playgroud)

app:projects绑定到val projects: List<Project>我的 ViewModel

app:selectedProject绑定到val entry: Entry具有Projectas 属性的类。

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

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

现在缺少的是 BindingAdapter 和 InverseBindingAdapter 来实现以下功能:

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

绑定适配器

    /**
     * 用所有可用的项目填充 Spinner。
     * 将 Spinner 选择设置为 selectedProject。
     * 如果选择改变,调用 InverseBindingAdapter
     */
    @BindingAdapter(value = ["projects", "selectedProject", "selectedProjectAttrChanged"], requireAll = false)
    fun setProjects(spinner: Spinner, projects: List?, selectedProject: Project, listener: InverseBindingListener) {
        if (projects == null) 返回
        spinner.adapter = NameAdapter(spinner.context, android.R.layout.simple_spinner_dropdown_item, 项目)
        setCurrentSelection(微调器,selectedProject)
        setSpinnerListener(微调器,监听器)
    }

您可以将 BindingAdapter 放在一个空文件中。它不必是任何类的一部分。重要的是它的参数。它们由 BindingAdapters 扣除value。在这种情况下,值为projectsselectedProjectselectedProjectAttrChanged。前两个参数对应我们自己定义的两个 layout-xml 属性。最后一个/第三个参数是数据绑定过程的一部分:对于具有双向数据绑定(即 @ = {)的每个 layout-xml 属性,将使用名称生成一个值<attribute-name>AttrChanged

这个特殊情况的另一个重要部分是NameAdapter它是我自己的 SpinnerAdapter,它能够将我的项目作为项目保存,并且只name在 UI 中显示它们的属性。这样我们总是可以访问整个项目实例,而不仅仅是一个字符串(默认 SpinnerAdapter 通常就是这种情况)。

这是我的自定义微调适配器的代码:

名称适配器

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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

反向绑定适配器

    @InverseBindingAdapter(attribute = "selectedProject")
    fun getSelectedProject(spinner: Spinner): 项目{
        返回 spinner.selectedItem 作为项目
    }

就是这样。大家一起工作很顺利。值得一提的是,如果您的 List 将包含大量数据,则不建议使用这种方法,因为所有这些数据都存储在适配器中。在我的情况下,它只是一些 String 字段,所以应该没问题。


为了完成,我想从 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
}
Run Code Online (Sandbox Code Playgroud)