I have a Fragment MyFragment
currently which has a Spinner my_spinner
. For testing my app, I originally populated the contents of my_spinner
manually by observing the property myLiveDataList
in the AndroidViewModel MyViewModel
as below:
my_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.fragments.MyFragment">
<Spinner
android:id="@+id/my_spinner"
android:layout_width="match_parent"
android:layout_height="100dp" />
</FrameLayout>
MyFragment.kt
import androidx.lifecycle.ViewModelProviders
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.lifecycle.Observer
import com.example.app.R
import com.example.app.data.room.entities.MyEntity
import com.example.app.ui.viewmodels.MyViewModel
import kotlinx.android.synthetic.main.my_fragment.*
class MyFragment : Fragment() {
companion object {
fun newInstance() = MyFragment()
}
private lateinit var viewModel: MyViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.my_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
val myAdapter = ArrayAdapter<MyEntity>(this.context!!, android.R.layout.simple_spinner_item)
// This is where I populate my_spinner
viewModel.myLiveDataList.observe(this, Observer<List<MyEntity>> { data ->
data?.forEach {
myAdapter.add(it)
}
})
my_spinner.adapter = myAdapter
}
}
MyViewModel.kt
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.toLiveData
import com.example.app.data.repositories.MyRepository
import com.example.app.data.room.entities.MyEntity
class MyViewModel(application: Application) : AndroidViewModel(application) {
private val myRepository = MyRepository(application)
val myLiveDataList: LiveData<List<MyEntity>>
get() = myRepository.getAllData().toLiveData()
}
This fills my_spinner
successfully when I navigate to MyFragment
:
Since it populates as expected, I went ahead to make the following changes to my_fragment.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.example.app.ui.viewmodels.MyViewModel" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.fragments.MyFragment">
<Spinner
android:id="@+id/my_spinner"
android:layout_width="match_parent"
android:layout_height="100dp"
app:entries="@{viewmodel.myLiveDataList}"/>
</FrameLayout>
</layout>
I've added in a Binding Adapter file BindingAdapterUtil
(following code was copied from this article):
import android.R
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import androidx.databinding.BindingAdapter
import androidx.databinding.InverseBindingAdapter
import androidx.databinding.InverseBindingListener
import com.example.app.ui.adapter.SpinnerExtensions.getSpinnerValue
import com.example.app.ui.adapter.SpinnerExtensions.setSpinnerEntries
import com.example.app.ui.adapter.SpinnerExtensions.setSpinnerInverseBindingListener
import com.example.app.ui.adapter.SpinnerExtensions.setSpinnerItemSelectedListener
import com.example.app.ui.adapter.SpinnerExtensions.setSpinnerValue
@BindingAdapter("entries")
fun Spinner.setEntries(entries: List<Any>?) {
setSpinnerEntries(entries)
}
@BindingAdapter("onItemSelected")
fun Spinner.setItemSelectedListener(itemSelectedListener: SpinnerExtensions.ItemSelectedListener?) {
setSpinnerItemSelectedListener(itemSelectedListener)
}
@BindingAdapter("newValue")
fun Spinner.setNewValue(newValue: Any?) {
setSpinnerValue(newValue)
}
@BindingAdapter("selectedValue")
fun Spinner.setSelectedValue(selectedValue: Any?) {
setSpinnerValue(selectedValue)
}
@BindingAdapter("selectedValueAttrChanged")
fun Spinner.setInverseBindingListener(inverseBindingListener: InverseBindingListener?) {
setSpinnerInverseBindingListener(inverseBindingListener)
}
@InverseBindingAdapter(attribute = "selectedValue", event = "selectedValueAttrChanged")
fun Spinner.getSelectedValue(): Any? {
return getSpinnerValue()
}
object SpinnerExtensions {
fun Spinner.setSpinnerEntries(entries: List<Any>?) {
if (entries != null) {
val arrayAdapter = ArrayAdapter(context, R.layout.simple_spinner_item, entries)
arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
adapter = arrayAdapter
}
}
fun Spinner.setSpinnerItemSelectedListener(listener: ItemSelectedListener?) {
if (listener == null) {
onItemSelectedListener = null
} else {
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
if (tag != position) {
listener.onItemSelected(parent.getItemAtPosition(position))
}
}
override fun onNothingSelected(parent: AdapterView<*>) {}
}
}
}
fun Spinner.setSpinnerInverseBindingListener(listener: InverseBindingListener?) {
if (listener == null) {
onItemSelectedListener = null
} else {
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
if (tag != position) {
listener.onChange()
}
}
override fun onNothingSelected(parent: AdapterView<*>) {}
}
}
}
fun Spinner.setSpinnerValue(value: Any?) {
if (adapter != null ) {
val position = (adapter as ArrayAdapter<Any>).getPosition(value)
setSelection(position, false)
tag = position
}
}
fun Spinner.getSpinnerValue(): Any? {
return selectedItem
}
interface ItemSelectedListener {
fun onItemSelected(item: Any)
}
}
And I've modified the onActivityCreated
in MyFragment
like so:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
DataBindingUtil.setContentView<MyFragmentBinding>(
this.activity!!, R.layout.my_fragment
).apply {
this.setLifecycleOwner(this@MyFragment)
this.viewmodel = viewModel
}
}
The result of this is that my_spinner
is no longer populating with the contents of MyViewModel.myLiveDataList
. To try to ascertain if the property was at fault, I created a new property in MyViewModel
like so:
val myList: List<String>?
get() = listOf("First", "Second", "Third")
And I have bound this property to my_spinner
just like MyViewModel.myLiveDataList
above with success this time.
The function in MyRepository.getAllData()
(which myLiveDataList
returns) returns a Flowable<List<MyEntity>>
(RxJava), which calls a Room DAO to get the data. My assumption here is that myLiveDataList
doesn't have anything to serve when it tries to bind the values for the first time, and never tries again.
Am I missing something when trying to bind a LiveData datasource to a Spinner?