How do you use the androidx databinding library to fill a Spinner with a list of custom objects (app:entries)? And how to create a proper selection callback for the Spinner (app:onItemSelected)?
My layout:
<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;
}
Project.kt
data class Project(
var id: Int? = null,
var name: String = "",
var createdAt: String = "",
var updatedAt: String = ""
)
The Spinner should display the names of each project and when I select a project it should be saved in viewModel.selectedProject. The use of LiveData is optional.
I guess that I have to write a @BindingAdapter for app:entries and an @InverseBindingAdapter for app:onItemSelected. But I can't figure out how to implement them without writing the usual boilerplate code for the Spinneradapter...
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:
entry
entry
automaticallyBindingAdapter
/** * 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
}
This question and replies have been very helpful as I muscled my way through last couple of days to solve a similar problem. In the sprit of sharing, here are all my files:
MainActivity.kt
package com.mandal.mvvmspinnerviewbindingexample
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import com.mandal.mvvmspinnerviewbindingexample.databinding.ActivityMainBinding
import com.mandal.mvvmspinnerviewbindingexample.viewmodel.UserViewModel
class MainActivity : AppCompatActivity() {
/**
* Lazily initialize our [UserViewModel].
*/
private val viewModel: UserViewModel by lazy {
ViewModelProvider(this).get(UserViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
enter code here
val binding: ActivityMainBinding =
DataBindingUtil.setContentView(this, R.layout.activity_main)
// Allows Data Binding to Observe LiveData with the lifecycle of this Activity
binding.lifecycleOwner = this
// Giving the binding access to the UserViewModel
binding.viewModel = viewModel
}
}
User.kt
package com.mandal.mvvmspinnerviewbindingexample.model
data class User(
val id: Int,
val name: String,
) {
override fun toString(): String = name
}
Entry.kt
package com.mandal.mvvmspinnerviewbindingexample.model
data class Entry (var user: User)
NameAdapter.kt
package com.mandal.mvvmspinnerviewbindingexample.adapter
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import com.mandal.mvvmspinnerviewbindingexample.model.User
class NameAdapter(context: Context, textViewResourceId: Int, private val values: List<User>) : ArrayAdapter<User>(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
}
}
UserViewModel.kt
package com.mandal.mvvmspinnerviewbindingexample.viewmodel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.mandal.mvvmspinnerviewbindingexample.model.Entry
import com.mandal.mvvmspinnerviewbindingexample.model.User
class UserViewModel: ViewModel (){
var users : List<User> = getUserList()
var entry: MutableLiveData<Entry> = MutableLiveData()
/**
* Sets the value of the status LiveData to the Mars API status.
*/
private fun getUserList() : List<User>{
//Setup Users
val user1 = User(1, "John")
val user2 = User(2, "Mary")
val user3 = User(2, "Patrick")
val user4 = User(2, "Amanda")
//Setup User List
val list = arrayListOf<User>(user1, user2, user3, user4)
return list
}
}
BindingAdapters.kt
package com.mandal.mvvmspinnerviewbindingexample
import android.R
import android.view.View
import android.widget.AdapterView
import android.widget.Spinner
import android.widget.Toast
import androidx.databinding.BindingAdapter
import androidx.databinding.InverseBindingAdapter
import androidx.databinding.InverseBindingListener
import com.mandal.mvvmspinnerviewbindingexample.adapter.NameAdapter
import com.mandal.mvvmspinnerviewbindingexample.model.User
/**
* fill the Spinner with all available projects.
* Set the Spinner selection to selectedProject.
* If the selection changes, call the InverseBindingAdapter
*/
@BindingAdapter(value = ["users", "selectedUser", "selectedUserAttrChanged"], requireAll = false)
fun setUsers(spinner: Spinner, users: List<User>?, selectedUser: User?, listener: InverseBindingListener) {
if (users == null) return
spinner.adapter = NameAdapter(spinner.context, R.layout.simple_spinner_dropdown_item, users)
setCurrentSelection(spinner, selectedUser)
setSpinnerListener(spinner, listener)
}
@InverseBindingAdapter(attribute = "selectedUser")
fun getSelectedUser(spinner: Spinner): User {
Toast.makeText(
spinner.context,
(spinner.selectedItem as User).name,
Toast.LENGTH_LONG
)
.show()
return spinner.selectedItem as User
}
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: User?): Boolean {
for (index in 0 until spinner.adapter.count) {
if (spinner.getItemAtPosition(index) == selectedItem?.name) {
spinner.setSelection(index)
return true
}
}
return false
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
>
<data>
<variable name="viewModel"
type="com.mandal.mvvmspinnerviewbindingexample.viewmodel.UserViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!--Spinner widget-->
<Spinner
android:id="@+id/userSpinner"
android:layout_width="160dp"
android:layout_height="50dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.17"
app:users="@{viewModel.users}"
app:selectedUser="@={viewModel.entry.user}"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mandal.mvvmspinnerviewbindingexample" >
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme" >
<activity android:name="com.mandal.mvvmspinnerviewbindingexample.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With